michael@0: /* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80: */ michael@0: michael@0: /** michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: * michael@0: * Basic use: michael@0: * let spanToEdit = document.getElementById("somespan"); michael@0: * michael@0: * editableField({ michael@0: * element: spanToEdit, michael@0: * done: function(value, commit) { michael@0: * if (commit) { michael@0: * spanToEdit.textContent = value; michael@0: * } michael@0: * }, michael@0: * trigger: "dblclick" michael@0: * }); michael@0: * michael@0: * See editableField() for more options. michael@0: */ michael@0: michael@0: "use strict"; michael@0: michael@0: const {Ci, Cu, Cc} = require("chrome"); michael@0: michael@0: const HTML_NS = "http://www.w3.org/1999/xhtml"; michael@0: const CONTENT_TYPES = { michael@0: PLAIN_TEXT: 0, michael@0: CSS_VALUE: 1, michael@0: CSS_MIXED: 2, michael@0: CSS_PROPERTY: 3, michael@0: }; michael@0: const MAX_POPUP_ENTRIES = 10; michael@0: michael@0: const FOCUS_FORWARD = Ci.nsIFocusManager.MOVEFOCUS_FORWARD; michael@0: const FOCUS_BACKWARD = Ci.nsIFocusManager.MOVEFOCUS_BACKWARD; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/devtools/event-emitter.js"); michael@0: michael@0: /** michael@0: * Mark a span editable. |editableField| will listen for the span to michael@0: * be focused and create an InlineEditor to handle text input. michael@0: * Changes will be committed when the InlineEditor's input is blurred michael@0: * or dropped when the user presses escape. michael@0: * michael@0: * @param {object} aOptions michael@0: * Options for the editable field, including: michael@0: * {Element} element: michael@0: * (required) The span to be edited on focus. michael@0: * {function} canEdit: michael@0: * Will be called before creating the inplace editor. Editor michael@0: * won't be created if canEdit returns false. michael@0: * {function} start: michael@0: * Will be called when the inplace editor is initialized. michael@0: * {function} change: michael@0: * Will be called when the text input changes. Will be called michael@0: * with the current value of the text input. michael@0: * {function} done: michael@0: * Called when input is committed or blurred. Called with michael@0: * current value and a boolean telling the caller whether to michael@0: * commit the change. This function is called before the editor michael@0: * has been torn down. michael@0: * {function} destroy: michael@0: * Called when the editor is destroyed and has been torn down. michael@0: * {string} advanceChars: michael@0: * If any characters in advanceChars are typed, focus will advance michael@0: * to the next element. michael@0: * {boolean} stopOnReturn: michael@0: * If true, the return key will not advance the editor to the next michael@0: * focusable element. michael@0: * {string} trigger: The DOM event that should trigger editing, michael@0: * defaults to "click" michael@0: */ michael@0: function editableField(aOptions) michael@0: { michael@0: return editableItem(aOptions, function(aElement, aEvent) { michael@0: new InplaceEditor(aOptions, aEvent); michael@0: }); michael@0: } michael@0: michael@0: exports.editableField = editableField; michael@0: michael@0: /** michael@0: * Handle events for an element that should respond to michael@0: * clicks and sit in the editing tab order, and call michael@0: * a callback when it is activated. michael@0: * michael@0: * @param {object} aOptions michael@0: * The options for this editor, including: michael@0: * {Element} element: The DOM element. michael@0: * {string} trigger: The DOM event that should trigger editing, michael@0: * defaults to "click" michael@0: * @param {function} aCallback michael@0: * Called when the editor is activated. michael@0: */ michael@0: function editableItem(aOptions, aCallback) michael@0: { michael@0: let trigger = aOptions.trigger || "click" michael@0: let element = aOptions.element; michael@0: element.addEventListener(trigger, function(evt) { michael@0: if (evt.target.nodeName !== "a") { michael@0: let win = this.ownerDocument.defaultView; michael@0: let selection = win.getSelection(); michael@0: if (trigger != "click" || selection.isCollapsed) { michael@0: aCallback(element, evt); michael@0: } michael@0: evt.stopPropagation(); michael@0: } michael@0: }, false); michael@0: michael@0: // If focused by means other than a click, start editing by michael@0: // pressing enter or space. michael@0: element.addEventListener("keypress", function(evt) { michael@0: if (evt.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN || michael@0: evt.charCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) { michael@0: aCallback(element); michael@0: } michael@0: }, true); michael@0: michael@0: // Ugly workaround - the element is focused on mousedown but michael@0: // the editor is activated on click/mouseup. This leads michael@0: // to an ugly flash of the focus ring before showing the editor. michael@0: // So hide the focus ring while the mouse is down. michael@0: element.addEventListener("mousedown", function(evt) { michael@0: if (evt.target.nodeName !== "a") { michael@0: let cleanup = function() { michael@0: element.style.removeProperty("outline-style"); michael@0: element.removeEventListener("mouseup", cleanup, false); michael@0: element.removeEventListener("mouseout", cleanup, false); michael@0: }; michael@0: element.style.setProperty("outline-style", "none"); michael@0: element.addEventListener("mouseup", cleanup, false); michael@0: element.addEventListener("mouseout", cleanup, false); michael@0: } michael@0: }, false); michael@0: michael@0: // Mark the element editable field for tab michael@0: // navigation while editing. michael@0: element._editable = true; michael@0: } michael@0: michael@0: exports.editableItem = this.editableItem; michael@0: michael@0: /* michael@0: * Various API consumers (especially tests) sometimes want to grab the michael@0: * inplaceEditor expando off span elements. However, when each global has its michael@0: * own compartment, those expandos live on Xray wrappers that are only visible michael@0: * within this JSM. So we provide a little workaround here. michael@0: */ michael@0: michael@0: function getInplaceEditorForSpan(aSpan) michael@0: { michael@0: return aSpan.inplaceEditor; michael@0: }; michael@0: exports.getInplaceEditorForSpan = getInplaceEditorForSpan; michael@0: michael@0: function InplaceEditor(aOptions, aEvent) michael@0: { michael@0: this.elt = aOptions.element; michael@0: let doc = this.elt.ownerDocument; michael@0: this.doc = doc; michael@0: this.elt.inplaceEditor = this; michael@0: michael@0: this.change = aOptions.change; michael@0: this.done = aOptions.done; michael@0: this.destroy = aOptions.destroy; michael@0: this.initial = aOptions.initial ? aOptions.initial : this.elt.textContent; michael@0: this.multiline = aOptions.multiline || false; michael@0: this.stopOnReturn = !!aOptions.stopOnReturn; michael@0: this.contentType = aOptions.contentType || CONTENT_TYPES.PLAIN_TEXT; michael@0: this.property = aOptions.property; michael@0: this.popup = aOptions.popup; michael@0: michael@0: this._onBlur = this._onBlur.bind(this); michael@0: this._onKeyPress = this._onKeyPress.bind(this); michael@0: this._onInput = this._onInput.bind(this); michael@0: this._onKeyup = this._onKeyup.bind(this); michael@0: michael@0: this._createInput(); michael@0: this._autosize(); michael@0: this.inputCharWidth = this._getInputCharWidth(); michael@0: michael@0: // Pull out character codes for advanceChars, listing the michael@0: // characters that should trigger a blur. michael@0: this._advanceCharCodes = {}; michael@0: let advanceChars = aOptions.advanceChars || ''; michael@0: for (let i = 0; i < advanceChars.length; i++) { michael@0: this._advanceCharCodes[advanceChars.charCodeAt(i)] = true; michael@0: } michael@0: michael@0: // Hide the provided element and add our editor. michael@0: this.originalDisplay = this.elt.style.display; michael@0: this.elt.style.display = "none"; michael@0: this.elt.parentNode.insertBefore(this.input, this.elt); michael@0: michael@0: if (typeof(aOptions.selectAll) == "undefined" || aOptions.selectAll) { michael@0: this.input.select(); michael@0: } michael@0: this.input.focus(); michael@0: michael@0: if (this.contentType == CONTENT_TYPES.CSS_VALUE && this.input.value == "") { michael@0: this._maybeSuggestCompletion(true); michael@0: } michael@0: michael@0: this.input.addEventListener("blur", this._onBlur, false); michael@0: this.input.addEventListener("keypress", this._onKeyPress, false); michael@0: this.input.addEventListener("input", this._onInput, false); michael@0: michael@0: this.input.addEventListener("dblclick", michael@0: (e) => { e.stopPropagation(); }, false); michael@0: this.input.addEventListener("mousedown", michael@0: (e) => { e.stopPropagation(); }, false); michael@0: michael@0: this.validate = aOptions.validate; michael@0: michael@0: if (this.validate) { michael@0: this.input.addEventListener("keyup", this._onKeyup, false); michael@0: } michael@0: michael@0: if (aOptions.start) { michael@0: aOptions.start(this, aEvent); michael@0: } michael@0: michael@0: EventEmitter.decorate(this); michael@0: } michael@0: michael@0: exports.InplaceEditor = InplaceEditor; michael@0: michael@0: InplaceEditor.CONTENT_TYPES = CONTENT_TYPES; michael@0: michael@0: InplaceEditor.prototype = { michael@0: _createInput: function InplaceEditor_createEditor() michael@0: { michael@0: this.input = michael@0: this.doc.createElementNS(HTML_NS, this.multiline ? "textarea" : "input"); michael@0: this.input.inplaceEditor = this; michael@0: this.input.classList.add("styleinspector-propertyeditor"); michael@0: this.input.value = this.initial; michael@0: michael@0: copyTextStyles(this.elt, this.input); michael@0: }, michael@0: michael@0: /** michael@0: * Get rid of the editor. michael@0: */ michael@0: _clear: function InplaceEditor_clear() michael@0: { michael@0: if (!this.input) { michael@0: // Already cleared. michael@0: return; michael@0: } michael@0: michael@0: this.input.removeEventListener("blur", this._onBlur, false); michael@0: this.input.removeEventListener("keypress", this._onKeyPress, false); michael@0: this.input.removeEventListener("keyup", this._onKeyup, false); michael@0: this.input.removeEventListener("oninput", this._onInput, false); michael@0: this._stopAutosize(); michael@0: michael@0: this.elt.style.display = this.originalDisplay; michael@0: this.elt.focus(); michael@0: michael@0: this.elt.parentNode.removeChild(this.input); michael@0: this.input = null; michael@0: michael@0: delete this.elt.inplaceEditor; michael@0: delete this.elt; michael@0: michael@0: if (this.destroy) { michael@0: this.destroy(); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Keeps the editor close to the size of its input string. This is pretty michael@0: * crappy, suggestions for improvement welcome. michael@0: */ michael@0: _autosize: function InplaceEditor_autosize() michael@0: { michael@0: // Create a hidden, absolutely-positioned span to measure the text michael@0: // in the input. Boo. michael@0: michael@0: // We can't just measure the original element because a) we don't michael@0: // change the underlying element's text ourselves (we leave that michael@0: // up to the client), and b) without tweaking the style of the michael@0: // original element, it might wrap differently or something. michael@0: this._measurement = michael@0: this.doc.createElementNS(HTML_NS, this.multiline ? "pre" : "span"); michael@0: this._measurement.className = "autosizer"; michael@0: this.elt.parentNode.appendChild(this._measurement); michael@0: let style = this._measurement.style; michael@0: style.visibility = "hidden"; michael@0: style.position = "absolute"; michael@0: style.top = "0"; michael@0: style.left = "0"; michael@0: copyTextStyles(this.input, this._measurement); michael@0: this._updateSize(); michael@0: }, michael@0: michael@0: /** michael@0: * Clean up the mess created by _autosize(). michael@0: */ michael@0: _stopAutosize: function InplaceEditor_stopAutosize() michael@0: { michael@0: if (!this._measurement) { michael@0: return; michael@0: } michael@0: this._measurement.parentNode.removeChild(this._measurement); michael@0: delete this._measurement; michael@0: }, michael@0: michael@0: /** michael@0: * Size the editor to fit its current contents. michael@0: */ michael@0: _updateSize: function InplaceEditor_updateSize() michael@0: { michael@0: // Replace spaces with non-breaking spaces. Otherwise setting michael@0: // the span's textContent will collapse spaces and the measurement michael@0: // will be wrong. michael@0: this._measurement.textContent = this.input.value.replace(/ /g, '\u00a0'); michael@0: michael@0: // We add a bit of padding to the end. Should be enough to fit michael@0: // any letter that could be typed, otherwise we'll scroll before michael@0: // we get a chance to resize. Yuck. michael@0: let width = this._measurement.offsetWidth + 10; michael@0: michael@0: if (this.multiline) { michael@0: // Make sure there's some content in the current line. This is a hack to michael@0: // account for the fact that after adding a newline the
 doesn't grow
michael@0:       // unless there's text content on the line.
michael@0:       width += 15;
michael@0:       this._measurement.textContent += "M";
michael@0:       this.input.style.height = this._measurement.offsetHeight + "px";
michael@0:     }
michael@0: 
michael@0:     this.input.style.width = width + "px";
michael@0:   },
michael@0: 
michael@0:   /**
michael@0:    * Get the width of a single character in the input to properly position the
michael@0:    * autocompletion popup.
michael@0:    */
michael@0:   _getInputCharWidth: function InplaceEditor_getInputCharWidth()
michael@0:   {
michael@0:     // Just make the text content to be 'x' to get the width of any character in
michael@0:     // a monospace font.
michael@0:     this._measurement.textContent = "x";
michael@0:     return this._measurement.offsetWidth;
michael@0:   },
michael@0: 
michael@0:    /**
michael@0:    * Increment property values in rule view.
michael@0:    *
michael@0:    * @param {number} increment
michael@0:    *        The amount to increase/decrease the property value.
michael@0:    * @return {bool} true if value has been incremented.
michael@0:    */
michael@0:   _incrementValue: function InplaceEditor_incrementValue(increment)
michael@0:   {
michael@0:     let value = this.input.value;
michael@0:     let selectionStart = this.input.selectionStart;
michael@0:     let selectionEnd = this.input.selectionEnd;
michael@0: 
michael@0:     let newValue = this._incrementCSSValue(value, increment, selectionStart,
michael@0:                                            selectionEnd);
michael@0: 
michael@0:     if (!newValue) {
michael@0:       return false;
michael@0:     }
michael@0: 
michael@0:     this.input.value = newValue.value;
michael@0:     this.input.setSelectionRange(newValue.start, newValue.end);
michael@0:     this._doValidation();
michael@0: 
michael@0:     // Call the user's change handler if available.
michael@0:     if (this.change) {
michael@0:       this.change(this.input.value.trim());
michael@0:     }
michael@0: 
michael@0:     return true;
michael@0:   },
michael@0: 
michael@0:   /**
michael@0:    * Increment the property value based on the property type.
michael@0:    *
michael@0:    * @param {string} value
michael@0:    *        Property value.
michael@0:    * @param {number} increment
michael@0:    *        Amount to increase/decrease the property value.
michael@0:    * @param {number} selStart
michael@0:    *        Starting index of the value.
michael@0:    * @param {number} selEnd
michael@0:    *        Ending index of the value.
michael@0:    * @return {object} object with properties 'value', 'start', and 'end'.
michael@0:    */
michael@0:   _incrementCSSValue: function InplaceEditor_incrementCSSValue(value, increment,
michael@0:                                                                selStart, selEnd)
michael@0:   {
michael@0:     let range = this._parseCSSValue(value, selStart);
michael@0:     let type = (range && range.type) || "";
michael@0:     let rawValue = (range ? value.substring(range.start, range.end) : "");
michael@0:     let incrementedValue = null, selection;
michael@0: 
michael@0:     if (type === "num") {
michael@0:       let newValue = this._incrementRawValue(rawValue, increment);
michael@0:       if (newValue !== null) {
michael@0:         incrementedValue = newValue;
michael@0:         selection = [0, incrementedValue.length];
michael@0:       }
michael@0:     } else if (type === "hex") {
michael@0:       let exprOffset = selStart - range.start;
michael@0:       let exprOffsetEnd = selEnd - range.start;
michael@0:       let newValue = this._incHexColor(rawValue, increment, exprOffset,
michael@0:                                        exprOffsetEnd);
michael@0:       if (newValue) {
michael@0:         incrementedValue = newValue.value;
michael@0:         selection = newValue.selection;
michael@0:       }
michael@0:     } else {
michael@0:       let info;
michael@0:       if (type === "rgb" || type === "hsl") {
michael@0:         info = {};
michael@0:         let part = value.substring(range.start, selStart).split(",").length - 1;
michael@0:         if (part === 3) { // alpha
michael@0:           info.minValue = 0;
michael@0:           info.maxValue = 1;
michael@0:         } else if (type === "rgb") {
michael@0:           info.minValue = 0;
michael@0:           info.maxValue = 255;
michael@0:         } else if (part !== 0) { // hsl percentage
michael@0:           info.minValue = 0;
michael@0:           info.maxValue = 100;
michael@0: 
michael@0:           // select the previous number if the selection is at the end of a
michael@0:           // percentage sign.
michael@0:           if (value.charAt(selStart - 1) === "%") {
michael@0:             --selStart;
michael@0:           }
michael@0:         }
michael@0:       }
michael@0:       return this._incrementGenericValue(value, increment, selStart, selEnd, info);
michael@0:     }
michael@0: 
michael@0:     if (incrementedValue === null) {
michael@0:       return;
michael@0:     }
michael@0: 
michael@0:     let preRawValue = value.substr(0, range.start);
michael@0:     let postRawValue = value.substr(range.end);
michael@0: 
michael@0:     return {
michael@0:       value: preRawValue + incrementedValue + postRawValue,
michael@0:       start: range.start + selection[0],
michael@0:       end: range.start + selection[1]
michael@0:     };
michael@0:   },
michael@0: 
michael@0:   /**
michael@0:    * Parses the property value and type.
michael@0:    *
michael@0:    * @param {string} value
michael@0:    *        Property value.
michael@0:    * @param {number} offset
michael@0:    *        Starting index of value.
michael@0:    * @return {object} object with properties 'value', 'start', 'end', and 'type'.
michael@0:    */
michael@0:    _parseCSSValue: function InplaceEditor_parseCSSValue(value, offset)
michael@0:   {
michael@0:     const reSplitCSS = /(url\("?[^"\)]+"?\)?)|(rgba?\([^)]*\)?)|(hsla?\([^)]*\)?)|(#[\dA-Fa-f]+)|(-?\d+(\.\d+)?(%|[a-z]{1,4})?)|"([^"]*)"?|'([^']*)'?|([^,\s\/!\(\)]+)|(!(.*)?)/;
michael@0:     let start = 0;
michael@0:     let m;
michael@0: 
michael@0:     // retreive values from left to right until we find the one at our offset
michael@0:     while ((m = reSplitCSS.exec(value)) &&
michael@0:           (m.index + m[0].length < offset)) {
michael@0:       value = value.substr(m.index + m[0].length);
michael@0:       start += m.index + m[0].length;
michael@0:       offset -= m.index + m[0].length;
michael@0:     }
michael@0: 
michael@0:     if (!m) {
michael@0:       return;
michael@0:     }
michael@0: 
michael@0:     let type;
michael@0:     if (m[1]) {
michael@0:       type = "url";
michael@0:     } else if (m[2]) {
michael@0:       type = "rgb";
michael@0:     } else if (m[3]) {
michael@0:       type = "hsl";
michael@0:     } else if (m[4]) {
michael@0:       type = "hex";
michael@0:     } else if (m[5]) {
michael@0:       type = "num";
michael@0:     }
michael@0: 
michael@0:     return {
michael@0:       value: m[0],
michael@0:       start: start + m.index,
michael@0:       end: start + m.index + m[0].length,
michael@0:       type: type
michael@0:     };
michael@0:   },
michael@0: 
michael@0:   /**
michael@0:    * Increment the property value for types other than
michael@0:    * number or hex, such as rgb, hsl, and file names.
michael@0:    *
michael@0:    * @param {string} value
michael@0:    *        Property value.
michael@0:    * @param {number} increment
michael@0:    *        Amount to increment/decrement.
michael@0:    * @param {number} offset
michael@0:    *        Starting index of the property value.
michael@0:    * @param {number} offsetEnd
michael@0:    *        Ending index of the property value.
michael@0:    * @param {object} info
michael@0:    *        Object with details about the property value.
michael@0:    * @return {object} object with properties 'value', 'start', and 'end'.
michael@0:    */
michael@0:   _incrementGenericValue:
michael@0:   function InplaceEditor_incrementGenericValue(value, increment, offset,
michael@0:                                                offsetEnd, info)
michael@0:   {
michael@0:     // Try to find a number around the cursor to increment.
michael@0:     let start, end;
michael@0:     // Check if we are incrementing in a non-number context (such as a URL)
michael@0:     if (/^-?[0-9.]/.test(value.substring(offset, offsetEnd)) &&
michael@0:       !(/\d/.test(value.charAt(offset - 1) + value.charAt(offsetEnd)))) {
michael@0:       // We have a number selected, possibly with a suffix, and we are not in
michael@0:       // the disallowed case of just part of a known number being selected.
michael@0:       // Use that number.
michael@0:       start = offset;
michael@0:       end = offsetEnd;
michael@0:     } else {
michael@0:       // Parse periods as belonging to the number only if we are in a known number
michael@0:       // context. (This makes incrementing the 1 in 'image1.gif' work.)
michael@0:       let pattern = "[" + (info ? "0-9." : "0-9") + "]*";
michael@0:       let before = new RegExp(pattern + "$").exec(value.substr(0, offset))[0].length;
michael@0:       let after = new RegExp("^" + pattern).exec(value.substr(offset))[0].length;
michael@0: 
michael@0:       start = offset - before;
michael@0:       end = offset + after;
michael@0: 
michael@0:       // Expand the number to contain an initial minus sign if it seems
michael@0:       // free-standing.
michael@0:       if (value.charAt(start - 1) === "-" &&
michael@0:          (start - 1 === 0 || /[ (:,='"]/.test(value.charAt(start - 2)))) {
michael@0:         --start;
michael@0:       }
michael@0:     }
michael@0: 
michael@0:     if (start !== end)
michael@0:     {
michael@0:       // Include percentages as part of the incremented number (they are
michael@0:       // common enough).
michael@0:       if (value.charAt(end) === "%") {
michael@0:         ++end;
michael@0:       }
michael@0: 
michael@0:       let first = value.substr(0, start);
michael@0:       let mid = value.substring(start, end);
michael@0:       let last = value.substr(end);
michael@0: 
michael@0:       mid = this._incrementRawValue(mid, increment, info);
michael@0: 
michael@0:       if (mid !== null) {
michael@0:         return {
michael@0:           value: first + mid + last,
michael@0:           start: start,
michael@0:           end: start + mid.length
michael@0:         };
michael@0:       }
michael@0:     }
michael@0:   },
michael@0: 
michael@0:   /**
michael@0:    * Increment the property value for numbers.
michael@0:    *
michael@0:    * @param {string} rawValue
michael@0:    *        Raw value to increment.
michael@0:    * @param {number} increment
michael@0:    *        Amount to increase/decrease the raw value.
michael@0:    * @param {object} info
michael@0:    *        Object with info about the property value.
michael@0:    * @return {string} the incremented value.
michael@0:    */
michael@0:   _incrementRawValue:
michael@0:   function InplaceEditor_incrementRawValue(rawValue, increment, info)
michael@0:   {
michael@0:     let num = parseFloat(rawValue);
michael@0: 
michael@0:     if (isNaN(num)) {
michael@0:       return null;
michael@0:     }
michael@0: 
michael@0:     let number = /\d+(\.\d+)?/.exec(rawValue);
michael@0:     let units = rawValue.substr(number.index + number[0].length);
michael@0: 
michael@0:     // avoid rounding errors
michael@0:     let newValue = Math.round((num + increment) * 1000) / 1000;
michael@0: 
michael@0:     if (info && "minValue" in info) {
michael@0:       newValue = Math.max(newValue, info.minValue);
michael@0:     }
michael@0:     if (info && "maxValue" in info) {
michael@0:       newValue = Math.min(newValue, info.maxValue);
michael@0:     }
michael@0: 
michael@0:     newValue = newValue.toString();
michael@0: 
michael@0:     return newValue + units;
michael@0:   },
michael@0: 
michael@0:   /**
michael@0:    * Increment the property value for hex.
michael@0:    *
michael@0:    * @param {string} value
michael@0:    *        Property value.
michael@0:    * @param {number} increment
michael@0:    *        Amount to increase/decrease the property value.
michael@0:    * @param {number} offset
michael@0:    *        Starting index of the property value.
michael@0:    * @param {number} offsetEnd
michael@0:    *        Ending index of the property value.
michael@0:    * @return {object} object with properties 'value' and 'selection'.
michael@0:    */
michael@0:   _incHexColor:
michael@0:   function InplaceEditor_incHexColor(rawValue, increment, offset, offsetEnd)
michael@0:   {
michael@0:     // Return early if no part of the rawValue is selected.
michael@0:     if (offsetEnd > rawValue.length && offset >= rawValue.length) {
michael@0:       return;
michael@0:     }
michael@0:     if (offset < 1 && offsetEnd <= 1) {
michael@0:       return;
michael@0:     }
michael@0:     // Ignore the leading #.
michael@0:     rawValue = rawValue.substr(1);
michael@0:     --offset;
michael@0:     --offsetEnd;
michael@0: 
michael@0:     // Clamp the selection to within the actual value.
michael@0:     offset = Math.max(offset, 0);
michael@0:     offsetEnd = Math.min(offsetEnd, rawValue.length);
michael@0:     offsetEnd = Math.max(offsetEnd, offset);
michael@0: 
michael@0:     // Normalize #ABC -> #AABBCC.
michael@0:     if (rawValue.length === 3) {
michael@0:       rawValue = rawValue.charAt(0) + rawValue.charAt(0) +
michael@0:                  rawValue.charAt(1) + rawValue.charAt(1) +
michael@0:                  rawValue.charAt(2) + rawValue.charAt(2);
michael@0:       offset *= 2;
michael@0:       offsetEnd *= 2;
michael@0:     }
michael@0: 
michael@0:     if (rawValue.length !== 6) {
michael@0:       return;
michael@0:     }
michael@0: 
michael@0:     // If no selection, increment an adjacent color, preferably one to the left.
michael@0:     if (offset === offsetEnd) {
michael@0:       if (offset === 0) {
michael@0:         offsetEnd = 1;
michael@0:       } else {
michael@0:         offset = offsetEnd - 1;
michael@0:       }
michael@0:     }
michael@0: 
michael@0:     // Make the selection cover entire parts.
michael@0:     offset -= offset % 2;
michael@0:     offsetEnd += offsetEnd % 2;
michael@0: 
michael@0:     // Remap the increments from [0.1, 1, 10] to [1, 1, 16].
michael@0:     if (-1 < increment && increment < 1) {
michael@0:       increment = (increment < 0 ? -1 : 1);
michael@0:     }
michael@0:     if (Math.abs(increment) === 10) {
michael@0:       increment = (increment < 0 ? -16 : 16);
michael@0:     }
michael@0: 
michael@0:     let isUpper = (rawValue.toUpperCase() === rawValue);
michael@0: 
michael@0:     for (let pos = offset; pos < offsetEnd; pos += 2) {
michael@0:       // Increment the part in [pos, pos+2).
michael@0:       let mid = rawValue.substr(pos, 2);
michael@0:       let value = parseInt(mid, 16);
michael@0: 
michael@0:       if (isNaN(value)) {
michael@0:         return;
michael@0:       }
michael@0: 
michael@0:       mid = Math.min(Math.max(value + increment, 0), 255).toString(16);
michael@0: 
michael@0:       while (mid.length < 2) {
michael@0:         mid = "0" + mid;
michael@0:       }
michael@0:       if (isUpper) {
michael@0:         mid = mid.toUpperCase();
michael@0:       }
michael@0: 
michael@0:       rawValue = rawValue.substr(0, pos) + mid + rawValue.substr(pos + 2);
michael@0:     }
michael@0: 
michael@0:     return {
michael@0:       value: "#" + rawValue,
michael@0:       selection: [offset + 1, offsetEnd + 1]
michael@0:     };
michael@0:   },
michael@0: 
michael@0:   /**
michael@0:    * Cycle through the autocompletion suggestions in the popup.
michael@0:    *
michael@0:    * @param {boolean} aReverse
michael@0:    *        true to select previous item from the popup.
michael@0:    * @param {boolean} aNoSelect
michael@0:    *        true to not select the text after selecting the newly selectedItem
michael@0:    *        from the popup.
michael@0:    */
michael@0:   _cycleCSSSuggestion:
michael@0:   function InplaceEditor_cycleCSSSuggestion(aReverse, aNoSelect)
michael@0:   {
michael@0:     // selectedItem can be null when nothing is selected in an empty editor.
michael@0:     let {label, preLabel} = this.popup.selectedItem || {label: "", preLabel: ""};
michael@0:     if (aReverse) {
michael@0:       this.popup.selectPreviousItem();
michael@0:     } else {
michael@0:       this.popup.selectNextItem();
michael@0:     }
michael@0:     this._selectedIndex = this.popup.selectedIndex;
michael@0:     let input = this.input;
michael@0:     let pre = "";
michael@0:     if (input.selectionStart < input.selectionEnd) {
michael@0:       pre = input.value.slice(0, input.selectionStart);
michael@0:     }
michael@0:     else {
michael@0:       pre = input.value.slice(0, input.selectionStart - label.length +
michael@0:                                  preLabel.length);
michael@0:     }
michael@0:     let post = input.value.slice(input.selectionEnd, input.value.length);
michael@0:     let item = this.popup.selectedItem;
michael@0:     let toComplete = item.label.slice(item.preLabel.length);
michael@0:     input.value = pre + toComplete + post;
michael@0:     if (!aNoSelect) {
michael@0:       input.setSelectionRange(pre.length, pre.length + toComplete.length);
michael@0:     }
michael@0:     else {
michael@0:       input.setSelectionRange(pre.length + toComplete.length,
michael@0:                               pre.length + toComplete.length);
michael@0:     }
michael@0:     this._updateSize();
michael@0:     // This emit is mainly for the purpose of making the test flow simpler.
michael@0:     this.emit("after-suggest");
michael@0:   },
michael@0: 
michael@0:   /**
michael@0:    * Call the client's done handler and clear out.
michael@0:    */
michael@0:   _apply: function InplaceEditor_apply(aEvent)
michael@0:   {
michael@0:     if (this._applied) {
michael@0:       return;
michael@0:     }
michael@0: 
michael@0:     this._applied = true;
michael@0: 
michael@0:     if (this.done) {
michael@0:       let val = this.input.value.trim();
michael@0:       return this.done(this.cancelled ? this.initial : val, !this.cancelled);
michael@0:     }
michael@0: 
michael@0:     return null;
michael@0:   },
michael@0: 
michael@0:   /**
michael@0:    * Handle loss of focus by calling done if it hasn't been called yet.
michael@0:    */
michael@0:   _onBlur: function InplaceEditor_onBlur(aEvent, aDoNotClear)
michael@0:   {
michael@0:     if (aEvent && this.popup && this.popup.isOpen &&
michael@0:         this.popup.selectedIndex >= 0) {
michael@0:       let label, preLabel;
michael@0:       if (this._selectedIndex === undefined) {
michael@0:         ({label, preLabel}) = this.popup.getItemAtIndex(this.popup.selectedIndex);
michael@0:       }
michael@0:       else {
michael@0:         ({label, preLabel}) = this.popup.getItemAtIndex(this._selectedIndex);
michael@0:       }
michael@0:       let input = this.input;
michael@0:       let pre = "";
michael@0:       if (input.selectionStart < input.selectionEnd) {
michael@0:         pre = input.value.slice(0, input.selectionStart);
michael@0:       }
michael@0:       else {
michael@0:         pre = input.value.slice(0, input.selectionStart - label.length +
michael@0:                                    preLabel.length);
michael@0:       }
michael@0:       let post = input.value.slice(input.selectionEnd, input.value.length);
michael@0:       let item = this.popup.selectedItem;
michael@0:       this._selectedIndex = this.popup.selectedIndex;
michael@0:       let toComplete = item.label.slice(item.preLabel.length);
michael@0:       input.value = pre + toComplete + post;
michael@0:       input.setSelectionRange(pre.length + toComplete.length,
michael@0:                               pre.length + toComplete.length);
michael@0:       this._updateSize();
michael@0:       // Wait for the popup to hide and then focus input async otherwise it does
michael@0:       // not work.
michael@0:       let onPopupHidden = () => {
michael@0:         this.popup._panel.removeEventListener("popuphidden", onPopupHidden);
michael@0:         this.doc.defaultView.setTimeout(()=> {
michael@0:           input.focus();
michael@0:           this.emit("after-suggest");
michael@0:         }, 0);
michael@0:       };
michael@0:       this.popup._panel.addEventListener("popuphidden", onPopupHidden);
michael@0:       this.popup.hidePopup();
michael@0:       // Content type other than CSS_MIXED is used in rule-view where the values
michael@0:       // are live previewed. So we apply the value before returning.
michael@0:       if (this.contentType != CONTENT_TYPES.CSS_MIXED) {
michael@0:         this._apply();
michael@0:       }
michael@0:       return;
michael@0:     }
michael@0:     this._apply();
michael@0:     if (!aDoNotClear) {
michael@0:       this._clear();
michael@0:     }
michael@0:   },
michael@0: 
michael@0:   /**
michael@0:    * Handle the input field's keypress event.
michael@0:    */
michael@0:   _onKeyPress: function InplaceEditor_onKeyPress(aEvent)
michael@0:   {
michael@0:     let prevent = false;
michael@0: 
michael@0:     const largeIncrement = 100;
michael@0:     const mediumIncrement = 10;
michael@0:     const smallIncrement = 0.1;
michael@0: 
michael@0:     let increment = 0;
michael@0: 
michael@0:     if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_UP
michael@0:        || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP) {
michael@0:       increment = 1;
michael@0:     } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DOWN
michael@0:        || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
michael@0:       increment = -1;
michael@0:     }
michael@0: 
michael@0:     if (aEvent.shiftKey && !aEvent.altKey) {
michael@0:       if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP
michael@0:            ||  aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
michael@0:         increment *= largeIncrement;
michael@0:       } else {
michael@0:         increment *= mediumIncrement;
michael@0:       }
michael@0:     } else if (aEvent.altKey && !aEvent.shiftKey) {
michael@0:       increment *= smallIncrement;
michael@0:     }
michael@0: 
michael@0:     let cycling = false;
michael@0:     if (increment && this._incrementValue(increment) ) {
michael@0:       this._updateSize();
michael@0:       prevent = true;
michael@0:       cycling = true;
michael@0:     } else if (increment && this.popup && this.popup.isOpen) {
michael@0:       cycling = true;
michael@0:       prevent = true;
michael@0:       this._cycleCSSSuggestion(increment > 0);
michael@0:       this._doValidation();
michael@0:     }
michael@0: 
michael@0:     if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_BACK_SPACE ||
michael@0:         aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DELETE ||
michael@0:         aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_LEFT ||
michael@0:         aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RIGHT) {
michael@0:       if (this.popup && this.popup.isOpen) {
michael@0:         this.popup.hidePopup();
michael@0:       }
michael@0:     } else if (!cycling && !aEvent.metaKey && !aEvent.altKey && !aEvent.ctrlKey) {
michael@0:       this._maybeSuggestCompletion();
michael@0:     }
michael@0: 
michael@0:     if (this.multiline &&
michael@0:         aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN &&
michael@0:         aEvent.shiftKey) {
michael@0:       prevent = false;
michael@0:     } else if (aEvent.charCode in this._advanceCharCodes
michael@0:        || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN
michael@0:        || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB) {
michael@0:       prevent = true;
michael@0: 
michael@0:       let direction = FOCUS_FORWARD;
michael@0:       if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB &&
michael@0:           aEvent.shiftKey) {
michael@0:         direction = FOCUS_BACKWARD;
michael@0:       }
michael@0:       if (this.stopOnReturn && aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
michael@0:         direction = null;
michael@0:       }
michael@0: 
michael@0:       // Now we don't want to suggest anything as we are moving out.
michael@0:       this._preventSuggestions = true;
michael@0:       // But we still want to show suggestions for css values. i.e. moving out
michael@0:       // of css property input box in forward direction
michael@0:       if (this.contentType == CONTENT_TYPES.CSS_PROPERTY &&
michael@0:           direction == FOCUS_FORWARD) {
michael@0:         this._preventSuggestions = false;
michael@0:       }
michael@0: 
michael@0:       let input = this.input;
michael@0: 
michael@0:       if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB &&
michael@0:           this.contentType == CONTENT_TYPES.CSS_MIXED) {
michael@0:         if (this.popup && input.selectionStart < input.selectionEnd) {
michael@0:           aEvent.preventDefault();
michael@0:           input.setSelectionRange(input.selectionEnd, input.selectionEnd);
michael@0:           this.emit("after-suggest");
michael@0:           return;
michael@0:         }
michael@0:         else if (this.popup && this.popup.isOpen) {
michael@0:           aEvent.preventDefault();
michael@0:           this._cycleCSSSuggestion(aEvent.shiftKey, true);
michael@0:           return;
michael@0:         }
michael@0:       }
michael@0: 
michael@0:       this._apply();
michael@0: 
michael@0:       // Close the popup if open
michael@0:       if (this.popup && this.popup.isOpen) {
michael@0:         this.popup.hidePopup();
michael@0:       }
michael@0: 
michael@0:       if (direction !== null && focusManager.focusedElement === input) {
michael@0:         // If the focused element wasn't changed by the done callback,
michael@0:         // move the focus as requested.
michael@0:         let next = moveFocus(this.doc.defaultView, direction);
michael@0: 
michael@0:         // If the next node to be focused has been tagged as an editable
michael@0:         // node, send it a click event to trigger
michael@0:         if (next && next.ownerDocument === this.doc && next._editable) {
michael@0:           next.click();
michael@0:         }
michael@0:       }
michael@0: 
michael@0:       this._clear();
michael@0:     } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE) {
michael@0:       // Cancel and blur ourselves.
michael@0:       // Now we don't want to suggest anything as we are moving out.
michael@0:       this._preventSuggestions = true;
michael@0:       // Close the popup if open
michael@0:       if (this.popup && this.popup.isOpen) {
michael@0:         this.popup.hidePopup();
michael@0:       }
michael@0:       prevent = true;
michael@0:       this.cancelled = true;
michael@0:       this._apply();
michael@0:       this._clear();
michael@0:       aEvent.stopPropagation();
michael@0:     } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) {
michael@0:       // No need for leading spaces here.  This is particularly
michael@0:       // noticable when adding a property: it's very natural to type
michael@0:       // : (which advances to the next property) then spacebar.
michael@0:       prevent = !this.input.value;
michael@0:     }
michael@0: 
michael@0:     if (prevent) {
michael@0:       aEvent.preventDefault();
michael@0:     }
michael@0:   },
michael@0: 
michael@0:   /**
michael@0:    * Handle the input field's keyup event.
michael@0:    */
michael@0:   _onKeyup: function(aEvent) {
michael@0:     this._applied = false;
michael@0:   },
michael@0: 
michael@0:   /**
michael@0:    * Handle changes to the input text.
michael@0:    */
michael@0:   _onInput: function InplaceEditor_onInput(aEvent)
michael@0:   {
michael@0:     // Validate the entered value.
michael@0:     this._doValidation();
michael@0: 
michael@0:     // Update size if we're autosizing.
michael@0:     if (this._measurement) {
michael@0:       this._updateSize();
michael@0:     }
michael@0: 
michael@0:     // Call the user's change handler if available.
michael@0:     if (this.change) {
michael@0:       this.change(this.input.value.trim());
michael@0:     }
michael@0:   },
michael@0: 
michael@0:   /**
michael@0:    * Fire validation callback with current input
michael@0:    */
michael@0:   _doValidation: function()
michael@0:   {
michael@0:     if (this.validate && this.input) {
michael@0:       this.validate(this.input.value);
michael@0:     }
michael@0:   },
michael@0: 
michael@0:   /**
michael@0:    * Handles displaying suggestions based on the current input.
michael@0:    *
michael@0:    * @param {boolean} aNoAutoInsert
michael@0:    *        true if you don't want to automatically insert the first suggestion
michael@0:    */
michael@0:   _maybeSuggestCompletion: function(aNoAutoInsert) {
michael@0:     // Input can be null in cases when you intantaneously switch out of it.
michael@0:     if (!this.input) {
michael@0:       return;
michael@0:     }
michael@0:     let preTimeoutQuery = this.input.value;
michael@0:     // Since we are calling this method from a keypress event handler, the
michael@0:     // |input.value| does not include currently typed character. Thus we perform
michael@0:     // this method async.
michael@0:     this.doc.defaultView.setTimeout(() => {
michael@0:       if (this._preventSuggestions) {
michael@0:         this._preventSuggestions = false;
michael@0:         return;
michael@0:       }
michael@0:       if (this.contentType == CONTENT_TYPES.PLAIN_TEXT) {
michael@0:         return;
michael@0:       }
michael@0:       if (!this.input) {
michael@0:         return;
michael@0:       }
michael@0:       let input = this.input;
michael@0:       // The length of input.value should be increased by 1
michael@0:       if (input.value.length - preTimeoutQuery.length > 1) {
michael@0:         return;
michael@0:       }
michael@0:       let query = input.value.slice(0, input.selectionStart);
michael@0:       let startCheckQuery = query;
michael@0:       if (query == null) {
michael@0:         return;
michael@0:       }
michael@0:       // If nothing is selected and there is a non-space character after the
michael@0:       // cursor, do not autocomplete.
michael@0:       if (input.selectionStart == input.selectionEnd &&
michael@0:           input.selectionStart < input.value.length &&
michael@0:           input.value.slice(input.selectionStart)[0] != " ") {
michael@0:         // This emit is mainly to make the test flow simpler.
michael@0:         this.emit("after-suggest", "nothing to autocomplete");
michael@0:         return;
michael@0:       }
michael@0:       let list = [];
michael@0:       if (this.contentType == CONTENT_TYPES.CSS_PROPERTY) {
michael@0:         list = CSSPropertyList;
michael@0:       } else if (this.contentType == CONTENT_TYPES.CSS_VALUE) {
michael@0:         // Get the last query to be completed before the caret.
michael@0:         let match = /([^\s,.\/]+$)/.exec(query);
michael@0:         if (match) {
michael@0:           startCheckQuery = match[0];
michael@0:         } else {
michael@0:           startCheckQuery = "";
michael@0:         }
michael@0: 
michael@0:         list =
michael@0:           ["!important", ...domUtils.getCSSValuesForProperty(this.property.name)];
michael@0: 
michael@0:         if (query == "") {
michael@0:           // Do not suggest '!important' without any manually typed character.
michael@0:           list.splice(0, 1);
michael@0:         }
michael@0:       } else if (this.contentType == CONTENT_TYPES.CSS_MIXED &&
michael@0:                  /^\s*style\s*=/.test(query)) {
michael@0:         // Detecting if cursor is at property or value;
michael@0:         let match = query.match(/([:;"'=]?)\s*([^"';:=]+)?$/);
michael@0:         if (match && match.length >= 2) {
michael@0:           if (match[1] == ":") { // We are in CSS value completion
michael@0:             let propertyName =
michael@0:               query.match(/[;"'=]\s*([^"';:= ]+)\s*:\s*[^"';:=]*$/)[1];
michael@0:             list =
michael@0:               ["!important;", ...domUtils.getCSSValuesForProperty(propertyName)];
michael@0:             let matchLastQuery = /([^\s,.\/]+$)/.exec(match[2] || "");
michael@0:             if (matchLastQuery) {
michael@0:               startCheckQuery = matchLastQuery[0];
michael@0:             } else {
michael@0:               startCheckQuery = "";
michael@0:             }
michael@0:             if (!match[2]) {
michael@0:               // Don't suggest '!important' without any manually typed character
michael@0:               list.splice(0, 1);
michael@0:             }
michael@0:           } else if (match[1]) { // We are in CSS property name completion
michael@0:             list = CSSPropertyList;
michael@0:             startCheckQuery = match[2];
michael@0:           }
michael@0:           if (startCheckQuery == null) {
michael@0:             // This emit is mainly to make the test flow simpler.
michael@0:             this.emit("after-suggest", "nothing to autocomplete");
michael@0:             return;
michael@0:           }
michael@0:         }
michael@0:       }
michael@0:       if (!aNoAutoInsert) {
michael@0:         list.some(item => {
michael@0:           if (startCheckQuery != null && item.startsWith(startCheckQuery)) {
michael@0:             input.value = query + item.slice(startCheckQuery.length) +
michael@0:                           input.value.slice(query.length);
michael@0:             input.setSelectionRange(query.length, query.length + item.length -
michael@0:                                                   startCheckQuery.length);
michael@0:             this._updateSize();
michael@0:             return true;
michael@0:           }
michael@0:         });
michael@0:       }
michael@0: 
michael@0:       if (!this.popup) {
michael@0:         // This emit is mainly to make the test flow simpler.
michael@0:         this.emit("after-suggest", "no popup");
michael@0:         return;
michael@0:       }
michael@0:       let finalList = [];
michael@0:       let length = list.length;
michael@0:       for (let i = 0, count = 0; i < length && count < MAX_POPUP_ENTRIES; i++) {
michael@0:         if (startCheckQuery != null && list[i].startsWith(startCheckQuery)) {
michael@0:           count++;
michael@0:           finalList.push({
michael@0:             preLabel: startCheckQuery,
michael@0:             label: list[i]
michael@0:           });
michael@0:         }
michael@0:         else if (count > 0) {
michael@0:           // Since count was incremented, we had already crossed the entries
michael@0:           // which would have started with query, assuming that list is sorted.
michael@0:           break;
michael@0:         }
michael@0:         else if (startCheckQuery != null && list[i][0] > startCheckQuery[0]) {
michael@0:           // We have crossed all possible matches alphabetically.
michael@0:           break;
michael@0:         }
michael@0:       }
michael@0: 
michael@0:       if (finalList.length > 1) {
michael@0:         // Calculate the offset for the popup to be opened.
michael@0:         let x = (this.input.selectionStart - startCheckQuery.length) *
michael@0:                 this.inputCharWidth;
michael@0:         this.popup.setItems(finalList);
michael@0:         this.popup.openPopup(this.input, x);
michael@0:         if (aNoAutoInsert) {
michael@0:           this.popup.selectedIndex = -1;
michael@0:         }
michael@0:       } else {
michael@0:         this.popup.hidePopup();
michael@0:       }
michael@0:       // This emit is mainly for the purpose of making the test flow simpler.
michael@0:       this.emit("after-suggest");
michael@0:       this._doValidation();
michael@0:     }, 0);
michael@0:   }
michael@0: };
michael@0: 
michael@0: /**
michael@0:  * Copy text-related styles from one element to another.
michael@0:  */
michael@0: function copyTextStyles(aFrom, aTo)
michael@0: {
michael@0:   let win = aFrom.ownerDocument.defaultView;
michael@0:   let style = win.getComputedStyle(aFrom);
michael@0:   aTo.style.fontFamily = style.getPropertyCSSValue("font-family").cssText;
michael@0:   aTo.style.fontSize = style.getPropertyCSSValue("font-size").cssText;
michael@0:   aTo.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText;
michael@0:   aTo.style.fontStyle = style.getPropertyCSSValue("font-style").cssText;
michael@0: }
michael@0: 
michael@0: /**
michael@0:  * Trigger a focus change similar to pressing tab/shift-tab.
michael@0:  */
michael@0: function moveFocus(aWin, aDirection)
michael@0: {
michael@0:   return focusManager.moveFocus(aWin, null, aDirection, 0);
michael@0: }
michael@0: 
michael@0: 
michael@0: XPCOMUtils.defineLazyGetter(this, "focusManager", function() {
michael@0:   return Services.focus;
michael@0: });
michael@0: 
michael@0: XPCOMUtils.defineLazyGetter(this, "CSSPropertyList", function() {
michael@0:   return domUtils.getCSSPropertyNames(domUtils.INCLUDE_ALIASES).sort();
michael@0: });
michael@0: 
michael@0: XPCOMUtils.defineLazyGetter(this, "domUtils", function() {
michael@0:   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
michael@0: });