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