browser/devtools/shared/inplace-editor.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/devtools/shared/inplace-editor.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1200 @@
     1.4 +/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     1.5 +/* vim: set ts=2 et sw=2 tw=80: */
     1.6 +
     1.7 +/**
     1.8 + * This Source Code Form is subject to the terms of the Mozilla Public
     1.9 + * License, v. 2.0. If a copy of the MPL was not distributed with this
    1.10 + * file, You can obtain one at http://mozilla.org/MPL/2.0/.
    1.11 + *
    1.12 + * Basic use:
    1.13 + * let spanToEdit = document.getElementById("somespan");
    1.14 + *
    1.15 + * editableField({
    1.16 + *   element: spanToEdit,
    1.17 + *   done: function(value, commit) {
    1.18 + *     if (commit) {
    1.19 + *       spanToEdit.textContent = value;
    1.20 + *     }
    1.21 + *   },
    1.22 + *   trigger: "dblclick"
    1.23 + * });
    1.24 + *
    1.25 + * See editableField() for more options.
    1.26 + */
    1.27 +
    1.28 +"use strict";
    1.29 +
    1.30 +const {Ci, Cu, Cc} = require("chrome");
    1.31 +
    1.32 +const HTML_NS = "http://www.w3.org/1999/xhtml";
    1.33 +const CONTENT_TYPES = {
    1.34 +  PLAIN_TEXT: 0,
    1.35 +  CSS_VALUE: 1,
    1.36 +  CSS_MIXED: 2,
    1.37 +  CSS_PROPERTY: 3,
    1.38 +};
    1.39 +const MAX_POPUP_ENTRIES = 10;
    1.40 +
    1.41 +const FOCUS_FORWARD = Ci.nsIFocusManager.MOVEFOCUS_FORWARD;
    1.42 +const FOCUS_BACKWARD = Ci.nsIFocusManager.MOVEFOCUS_BACKWARD;
    1.43 +
    1.44 +Cu.import("resource://gre/modules/Services.jsm");
    1.45 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.46 +Cu.import("resource://gre/modules/devtools/event-emitter.js");
    1.47 +
    1.48 +/**
    1.49 + * Mark a span editable.  |editableField| will listen for the span to
    1.50 + * be focused and create an InlineEditor to handle text input.
    1.51 + * Changes will be committed when the InlineEditor's input is blurred
    1.52 + * or dropped when the user presses escape.
    1.53 + *
    1.54 + * @param {object} aOptions
    1.55 + *    Options for the editable field, including:
    1.56 + *    {Element} element:
    1.57 + *      (required) The span to be edited on focus.
    1.58 + *    {function} canEdit:
    1.59 + *       Will be called before creating the inplace editor.  Editor
    1.60 + *       won't be created if canEdit returns false.
    1.61 + *    {function} start:
    1.62 + *       Will be called when the inplace editor is initialized.
    1.63 + *    {function} change:
    1.64 + *       Will be called when the text input changes.  Will be called
    1.65 + *       with the current value of the text input.
    1.66 + *    {function} done:
    1.67 + *       Called when input is committed or blurred.  Called with
    1.68 + *       current value and a boolean telling the caller whether to
    1.69 + *       commit the change.  This function is called before the editor
    1.70 + *       has been torn down.
    1.71 + *    {function} destroy:
    1.72 + *       Called when the editor is destroyed and has been torn down.
    1.73 + *    {string} advanceChars:
    1.74 + *       If any characters in advanceChars are typed, focus will advance
    1.75 + *       to the next element.
    1.76 + *    {boolean} stopOnReturn:
    1.77 + *       If true, the return key will not advance the editor to the next
    1.78 + *       focusable element.
    1.79 + *    {string} trigger: The DOM event that should trigger editing,
    1.80 + *      defaults to "click"
    1.81 + */
    1.82 +function editableField(aOptions)
    1.83 +{
    1.84 +  return editableItem(aOptions, function(aElement, aEvent) {
    1.85 +    new InplaceEditor(aOptions, aEvent);
    1.86 +  });
    1.87 +}
    1.88 +
    1.89 +exports.editableField = editableField;
    1.90 +
    1.91 +/**
    1.92 + * Handle events for an element that should respond to
    1.93 + * clicks and sit in the editing tab order, and call
    1.94 + * a callback when it is activated.
    1.95 + *
    1.96 + * @param {object} aOptions
    1.97 + *    The options for this editor, including:
    1.98 + *    {Element} element: The DOM element.
    1.99 + *    {string} trigger: The DOM event that should trigger editing,
   1.100 + *      defaults to "click"
   1.101 + * @param {function} aCallback
   1.102 + *        Called when the editor is activated.
   1.103 + */
   1.104 +function editableItem(aOptions, aCallback)
   1.105 +{
   1.106 +  let trigger = aOptions.trigger || "click"
   1.107 +  let element = aOptions.element;
   1.108 +  element.addEventListener(trigger, function(evt) {
   1.109 +    if (evt.target.nodeName !== "a") {
   1.110 +      let win = this.ownerDocument.defaultView;
   1.111 +      let selection = win.getSelection();
   1.112 +      if (trigger != "click" || selection.isCollapsed) {
   1.113 +        aCallback(element, evt);
   1.114 +      }
   1.115 +      evt.stopPropagation();
   1.116 +    }
   1.117 +  }, false);
   1.118 +
   1.119 +  // If focused by means other than a click, start editing by
   1.120 +  // pressing enter or space.
   1.121 +  element.addEventListener("keypress", function(evt) {
   1.122 +    if (evt.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN ||
   1.123 +        evt.charCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) {
   1.124 +      aCallback(element);
   1.125 +    }
   1.126 +  }, true);
   1.127 +
   1.128 +  // Ugly workaround - the element is focused on mousedown but
   1.129 +  // the editor is activated on click/mouseup.  This leads
   1.130 +  // to an ugly flash of the focus ring before showing the editor.
   1.131 +  // So hide the focus ring while the mouse is down.
   1.132 +  element.addEventListener("mousedown", function(evt) {
   1.133 +    if (evt.target.nodeName !== "a") {
   1.134 +      let cleanup = function() {
   1.135 +        element.style.removeProperty("outline-style");
   1.136 +        element.removeEventListener("mouseup", cleanup, false);
   1.137 +        element.removeEventListener("mouseout", cleanup, false);
   1.138 +      };
   1.139 +      element.style.setProperty("outline-style", "none");
   1.140 +      element.addEventListener("mouseup", cleanup, false);
   1.141 +      element.addEventListener("mouseout", cleanup, false);
   1.142 +    }
   1.143 +  }, false);
   1.144 +
   1.145 +  // Mark the element editable field for tab
   1.146 +  // navigation while editing.
   1.147 +  element._editable = true;
   1.148 +}
   1.149 +
   1.150 +exports.editableItem = this.editableItem;
   1.151 +
   1.152 +/*
   1.153 + * Various API consumers (especially tests) sometimes want to grab the
   1.154 + * inplaceEditor expando off span elements. However, when each global has its
   1.155 + * own compartment, those expandos live on Xray wrappers that are only visible
   1.156 + * within this JSM. So we provide a little workaround here.
   1.157 + */
   1.158 +
   1.159 +function getInplaceEditorForSpan(aSpan)
   1.160 +{
   1.161 +  return aSpan.inplaceEditor;
   1.162 +};
   1.163 +exports.getInplaceEditorForSpan = getInplaceEditorForSpan;
   1.164 +
   1.165 +function InplaceEditor(aOptions, aEvent)
   1.166 +{
   1.167 +  this.elt = aOptions.element;
   1.168 +  let doc = this.elt.ownerDocument;
   1.169 +  this.doc = doc;
   1.170 +  this.elt.inplaceEditor = this;
   1.171 +
   1.172 +  this.change = aOptions.change;
   1.173 +  this.done = aOptions.done;
   1.174 +  this.destroy = aOptions.destroy;
   1.175 +  this.initial = aOptions.initial ? aOptions.initial : this.elt.textContent;
   1.176 +  this.multiline = aOptions.multiline || false;
   1.177 +  this.stopOnReturn = !!aOptions.stopOnReturn;
   1.178 +  this.contentType = aOptions.contentType || CONTENT_TYPES.PLAIN_TEXT;
   1.179 +  this.property = aOptions.property;
   1.180 +  this.popup = aOptions.popup;
   1.181 +
   1.182 +  this._onBlur = this._onBlur.bind(this);
   1.183 +  this._onKeyPress = this._onKeyPress.bind(this);
   1.184 +  this._onInput = this._onInput.bind(this);
   1.185 +  this._onKeyup = this._onKeyup.bind(this);
   1.186 +
   1.187 +  this._createInput();
   1.188 +  this._autosize();
   1.189 +  this.inputCharWidth = this._getInputCharWidth();
   1.190 +
   1.191 +  // Pull out character codes for advanceChars, listing the
   1.192 +  // characters that should trigger a blur.
   1.193 +  this._advanceCharCodes = {};
   1.194 +  let advanceChars = aOptions.advanceChars || '';
   1.195 +  for (let i = 0; i < advanceChars.length; i++) {
   1.196 +    this._advanceCharCodes[advanceChars.charCodeAt(i)] = true;
   1.197 +  }
   1.198 +
   1.199 +  // Hide the provided element and add our editor.
   1.200 +  this.originalDisplay = this.elt.style.display;
   1.201 +  this.elt.style.display = "none";
   1.202 +  this.elt.parentNode.insertBefore(this.input, this.elt);
   1.203 +
   1.204 +  if (typeof(aOptions.selectAll) == "undefined" || aOptions.selectAll) {
   1.205 +    this.input.select();
   1.206 +  }
   1.207 +  this.input.focus();
   1.208 +
   1.209 +  if (this.contentType == CONTENT_TYPES.CSS_VALUE && this.input.value == "") {
   1.210 +    this._maybeSuggestCompletion(true);
   1.211 +  }
   1.212 +
   1.213 +  this.input.addEventListener("blur", this._onBlur, false);
   1.214 +  this.input.addEventListener("keypress", this._onKeyPress, false);
   1.215 +  this.input.addEventListener("input", this._onInput, false);
   1.216 +
   1.217 +  this.input.addEventListener("dblclick",
   1.218 +    (e) => { e.stopPropagation(); }, false);
   1.219 +  this.input.addEventListener("mousedown",
   1.220 +    (e) => { e.stopPropagation(); }, false);
   1.221 +
   1.222 +  this.validate = aOptions.validate;
   1.223 +
   1.224 +  if (this.validate) {
   1.225 +    this.input.addEventListener("keyup", this._onKeyup, false);
   1.226 +  }
   1.227 +
   1.228 +  if (aOptions.start) {
   1.229 +    aOptions.start(this, aEvent);
   1.230 +  }
   1.231 +
   1.232 +  EventEmitter.decorate(this);
   1.233 +}
   1.234 +
   1.235 +exports.InplaceEditor = InplaceEditor;
   1.236 +
   1.237 +InplaceEditor.CONTENT_TYPES = CONTENT_TYPES;
   1.238 +
   1.239 +InplaceEditor.prototype = {
   1.240 +  _createInput: function InplaceEditor_createEditor()
   1.241 +  {
   1.242 +    this.input =
   1.243 +      this.doc.createElementNS(HTML_NS, this.multiline ? "textarea" : "input");
   1.244 +    this.input.inplaceEditor = this;
   1.245 +    this.input.classList.add("styleinspector-propertyeditor");
   1.246 +    this.input.value = this.initial;
   1.247 +
   1.248 +    copyTextStyles(this.elt, this.input);
   1.249 +  },
   1.250 +
   1.251 +  /**
   1.252 +   * Get rid of the editor.
   1.253 +   */
   1.254 +  _clear: function InplaceEditor_clear()
   1.255 +  {
   1.256 +    if (!this.input) {
   1.257 +      // Already cleared.
   1.258 +      return;
   1.259 +    }
   1.260 +
   1.261 +    this.input.removeEventListener("blur", this._onBlur, false);
   1.262 +    this.input.removeEventListener("keypress", this._onKeyPress, false);
   1.263 +    this.input.removeEventListener("keyup", this._onKeyup, false);
   1.264 +    this.input.removeEventListener("oninput", this._onInput, false);
   1.265 +    this._stopAutosize();
   1.266 +
   1.267 +    this.elt.style.display = this.originalDisplay;
   1.268 +    this.elt.focus();
   1.269 +
   1.270 +    this.elt.parentNode.removeChild(this.input);
   1.271 +    this.input = null;
   1.272 +
   1.273 +    delete this.elt.inplaceEditor;
   1.274 +    delete this.elt;
   1.275 +
   1.276 +    if (this.destroy) {
   1.277 +      this.destroy();
   1.278 +    }
   1.279 +  },
   1.280 +
   1.281 +  /**
   1.282 +   * Keeps the editor close to the size of its input string.  This is pretty
   1.283 +   * crappy, suggestions for improvement welcome.
   1.284 +   */
   1.285 +  _autosize: function InplaceEditor_autosize()
   1.286 +  {
   1.287 +    // Create a hidden, absolutely-positioned span to measure the text
   1.288 +    // in the input.  Boo.
   1.289 +
   1.290 +    // We can't just measure the original element because a) we don't
   1.291 +    // change the underlying element's text ourselves (we leave that
   1.292 +    // up to the client), and b) without tweaking the style of the
   1.293 +    // original element, it might wrap differently or something.
   1.294 +    this._measurement =
   1.295 +      this.doc.createElementNS(HTML_NS, this.multiline ? "pre" : "span");
   1.296 +    this._measurement.className = "autosizer";
   1.297 +    this.elt.parentNode.appendChild(this._measurement);
   1.298 +    let style = this._measurement.style;
   1.299 +    style.visibility = "hidden";
   1.300 +    style.position = "absolute";
   1.301 +    style.top = "0";
   1.302 +    style.left = "0";
   1.303 +    copyTextStyles(this.input, this._measurement);
   1.304 +    this._updateSize();
   1.305 +  },
   1.306 +
   1.307 +  /**
   1.308 +   * Clean up the mess created by _autosize().
   1.309 +   */
   1.310 +  _stopAutosize: function InplaceEditor_stopAutosize()
   1.311 +  {
   1.312 +    if (!this._measurement) {
   1.313 +      return;
   1.314 +    }
   1.315 +    this._measurement.parentNode.removeChild(this._measurement);
   1.316 +    delete this._measurement;
   1.317 +  },
   1.318 +
   1.319 +  /**
   1.320 +   * Size the editor to fit its current contents.
   1.321 +   */
   1.322 +  _updateSize: function InplaceEditor_updateSize()
   1.323 +  {
   1.324 +    // Replace spaces with non-breaking spaces.  Otherwise setting
   1.325 +    // the span's textContent will collapse spaces and the measurement
   1.326 +    // will be wrong.
   1.327 +    this._measurement.textContent = this.input.value.replace(/ /g, '\u00a0');
   1.328 +
   1.329 +    // We add a bit of padding to the end.  Should be enough to fit
   1.330 +    // any letter that could be typed, otherwise we'll scroll before
   1.331 +    // we get a chance to resize.  Yuck.
   1.332 +    let width = this._measurement.offsetWidth + 10;
   1.333 +
   1.334 +    if (this.multiline) {
   1.335 +      // Make sure there's some content in the current line.  This is a hack to
   1.336 +      // account for the fact that after adding a newline the <pre> doesn't grow
   1.337 +      // unless there's text content on the line.
   1.338 +      width += 15;
   1.339 +      this._measurement.textContent += "M";
   1.340 +      this.input.style.height = this._measurement.offsetHeight + "px";
   1.341 +    }
   1.342 +
   1.343 +    this.input.style.width = width + "px";
   1.344 +  },
   1.345 +
   1.346 +  /**
   1.347 +   * Get the width of a single character in the input to properly position the
   1.348 +   * autocompletion popup.
   1.349 +   */
   1.350 +  _getInputCharWidth: function InplaceEditor_getInputCharWidth()
   1.351 +  {
   1.352 +    // Just make the text content to be 'x' to get the width of any character in
   1.353 +    // a monospace font.
   1.354 +    this._measurement.textContent = "x";
   1.355 +    return this._measurement.offsetWidth;
   1.356 +  },
   1.357 +
   1.358 +   /**
   1.359 +   * Increment property values in rule view.
   1.360 +   *
   1.361 +   * @param {number} increment
   1.362 +   *        The amount to increase/decrease the property value.
   1.363 +   * @return {bool} true if value has been incremented.
   1.364 +   */
   1.365 +  _incrementValue: function InplaceEditor_incrementValue(increment)
   1.366 +  {
   1.367 +    let value = this.input.value;
   1.368 +    let selectionStart = this.input.selectionStart;
   1.369 +    let selectionEnd = this.input.selectionEnd;
   1.370 +
   1.371 +    let newValue = this._incrementCSSValue(value, increment, selectionStart,
   1.372 +                                           selectionEnd);
   1.373 +
   1.374 +    if (!newValue) {
   1.375 +      return false;
   1.376 +    }
   1.377 +
   1.378 +    this.input.value = newValue.value;
   1.379 +    this.input.setSelectionRange(newValue.start, newValue.end);
   1.380 +    this._doValidation();
   1.381 +
   1.382 +    // Call the user's change handler if available.
   1.383 +    if (this.change) {
   1.384 +      this.change(this.input.value.trim());
   1.385 +    }
   1.386 +
   1.387 +    return true;
   1.388 +  },
   1.389 +
   1.390 +  /**
   1.391 +   * Increment the property value based on the property type.
   1.392 +   *
   1.393 +   * @param {string} value
   1.394 +   *        Property value.
   1.395 +   * @param {number} increment
   1.396 +   *        Amount to increase/decrease the property value.
   1.397 +   * @param {number} selStart
   1.398 +   *        Starting index of the value.
   1.399 +   * @param {number} selEnd
   1.400 +   *        Ending index of the value.
   1.401 +   * @return {object} object with properties 'value', 'start', and 'end'.
   1.402 +   */
   1.403 +  _incrementCSSValue: function InplaceEditor_incrementCSSValue(value, increment,
   1.404 +                                                               selStart, selEnd)
   1.405 +  {
   1.406 +    let range = this._parseCSSValue(value, selStart);
   1.407 +    let type = (range && range.type) || "";
   1.408 +    let rawValue = (range ? value.substring(range.start, range.end) : "");
   1.409 +    let incrementedValue = null, selection;
   1.410 +
   1.411 +    if (type === "num") {
   1.412 +      let newValue = this._incrementRawValue(rawValue, increment);
   1.413 +      if (newValue !== null) {
   1.414 +        incrementedValue = newValue;
   1.415 +        selection = [0, incrementedValue.length];
   1.416 +      }
   1.417 +    } else if (type === "hex") {
   1.418 +      let exprOffset = selStart - range.start;
   1.419 +      let exprOffsetEnd = selEnd - range.start;
   1.420 +      let newValue = this._incHexColor(rawValue, increment, exprOffset,
   1.421 +                                       exprOffsetEnd);
   1.422 +      if (newValue) {
   1.423 +        incrementedValue = newValue.value;
   1.424 +        selection = newValue.selection;
   1.425 +      }
   1.426 +    } else {
   1.427 +      let info;
   1.428 +      if (type === "rgb" || type === "hsl") {
   1.429 +        info = {};
   1.430 +        let part = value.substring(range.start, selStart).split(",").length - 1;
   1.431 +        if (part === 3) { // alpha
   1.432 +          info.minValue = 0;
   1.433 +          info.maxValue = 1;
   1.434 +        } else if (type === "rgb") {
   1.435 +          info.minValue = 0;
   1.436 +          info.maxValue = 255;
   1.437 +        } else if (part !== 0) { // hsl percentage
   1.438 +          info.minValue = 0;
   1.439 +          info.maxValue = 100;
   1.440 +
   1.441 +          // select the previous number if the selection is at the end of a
   1.442 +          // percentage sign.
   1.443 +          if (value.charAt(selStart - 1) === "%") {
   1.444 +            --selStart;
   1.445 +          }
   1.446 +        }
   1.447 +      }
   1.448 +      return this._incrementGenericValue(value, increment, selStart, selEnd, info);
   1.449 +    }
   1.450 +
   1.451 +    if (incrementedValue === null) {
   1.452 +      return;
   1.453 +    }
   1.454 +
   1.455 +    let preRawValue = value.substr(0, range.start);
   1.456 +    let postRawValue = value.substr(range.end);
   1.457 +
   1.458 +    return {
   1.459 +      value: preRawValue + incrementedValue + postRawValue,
   1.460 +      start: range.start + selection[0],
   1.461 +      end: range.start + selection[1]
   1.462 +    };
   1.463 +  },
   1.464 +
   1.465 +  /**
   1.466 +   * Parses the property value and type.
   1.467 +   *
   1.468 +   * @param {string} value
   1.469 +   *        Property value.
   1.470 +   * @param {number} offset
   1.471 +   *        Starting index of value.
   1.472 +   * @return {object} object with properties 'value', 'start', 'end', and 'type'.
   1.473 +   */
   1.474 +   _parseCSSValue: function InplaceEditor_parseCSSValue(value, offset)
   1.475 +  {
   1.476 +    const reSplitCSS = /(url\("?[^"\)]+"?\)?)|(rgba?\([^)]*\)?)|(hsla?\([^)]*\)?)|(#[\dA-Fa-f]+)|(-?\d+(\.\d+)?(%|[a-z]{1,4})?)|"([^"]*)"?|'([^']*)'?|([^,\s\/!\(\)]+)|(!(.*)?)/;
   1.477 +    let start = 0;
   1.478 +    let m;
   1.479 +
   1.480 +    // retreive values from left to right until we find the one at our offset
   1.481 +    while ((m = reSplitCSS.exec(value)) &&
   1.482 +          (m.index + m[0].length < offset)) {
   1.483 +      value = value.substr(m.index + m[0].length);
   1.484 +      start += m.index + m[0].length;
   1.485 +      offset -= m.index + m[0].length;
   1.486 +    }
   1.487 +
   1.488 +    if (!m) {
   1.489 +      return;
   1.490 +    }
   1.491 +
   1.492 +    let type;
   1.493 +    if (m[1]) {
   1.494 +      type = "url";
   1.495 +    } else if (m[2]) {
   1.496 +      type = "rgb";
   1.497 +    } else if (m[3]) {
   1.498 +      type = "hsl";
   1.499 +    } else if (m[4]) {
   1.500 +      type = "hex";
   1.501 +    } else if (m[5]) {
   1.502 +      type = "num";
   1.503 +    }
   1.504 +
   1.505 +    return {
   1.506 +      value: m[0],
   1.507 +      start: start + m.index,
   1.508 +      end: start + m.index + m[0].length,
   1.509 +      type: type
   1.510 +    };
   1.511 +  },
   1.512 +
   1.513 +  /**
   1.514 +   * Increment the property value for types other than
   1.515 +   * number or hex, such as rgb, hsl, and file names.
   1.516 +   *
   1.517 +   * @param {string} value
   1.518 +   *        Property value.
   1.519 +   * @param {number} increment
   1.520 +   *        Amount to increment/decrement.
   1.521 +   * @param {number} offset
   1.522 +   *        Starting index of the property value.
   1.523 +   * @param {number} offsetEnd
   1.524 +   *        Ending index of the property value.
   1.525 +   * @param {object} info
   1.526 +   *        Object with details about the property value.
   1.527 +   * @return {object} object with properties 'value', 'start', and 'end'.
   1.528 +   */
   1.529 +  _incrementGenericValue:
   1.530 +  function InplaceEditor_incrementGenericValue(value, increment, offset,
   1.531 +                                               offsetEnd, info)
   1.532 +  {
   1.533 +    // Try to find a number around the cursor to increment.
   1.534 +    let start, end;
   1.535 +    // Check if we are incrementing in a non-number context (such as a URL)
   1.536 +    if (/^-?[0-9.]/.test(value.substring(offset, offsetEnd)) &&
   1.537 +      !(/\d/.test(value.charAt(offset - 1) + value.charAt(offsetEnd)))) {
   1.538 +      // We have a number selected, possibly with a suffix, and we are not in
   1.539 +      // the disallowed case of just part of a known number being selected.
   1.540 +      // Use that number.
   1.541 +      start = offset;
   1.542 +      end = offsetEnd;
   1.543 +    } else {
   1.544 +      // Parse periods as belonging to the number only if we are in a known number
   1.545 +      // context. (This makes incrementing the 1 in 'image1.gif' work.)
   1.546 +      let pattern = "[" + (info ? "0-9." : "0-9") + "]*";
   1.547 +      let before = new RegExp(pattern + "$").exec(value.substr(0, offset))[0].length;
   1.548 +      let after = new RegExp("^" + pattern).exec(value.substr(offset))[0].length;
   1.549 +
   1.550 +      start = offset - before;
   1.551 +      end = offset + after;
   1.552 +
   1.553 +      // Expand the number to contain an initial minus sign if it seems
   1.554 +      // free-standing.
   1.555 +      if (value.charAt(start - 1) === "-" &&
   1.556 +         (start - 1 === 0 || /[ (:,='"]/.test(value.charAt(start - 2)))) {
   1.557 +        --start;
   1.558 +      }
   1.559 +    }
   1.560 +
   1.561 +    if (start !== end)
   1.562 +    {
   1.563 +      // Include percentages as part of the incremented number (they are
   1.564 +      // common enough).
   1.565 +      if (value.charAt(end) === "%") {
   1.566 +        ++end;
   1.567 +      }
   1.568 +
   1.569 +      let first = value.substr(0, start);
   1.570 +      let mid = value.substring(start, end);
   1.571 +      let last = value.substr(end);
   1.572 +
   1.573 +      mid = this._incrementRawValue(mid, increment, info);
   1.574 +
   1.575 +      if (mid !== null) {
   1.576 +        return {
   1.577 +          value: first + mid + last,
   1.578 +          start: start,
   1.579 +          end: start + mid.length
   1.580 +        };
   1.581 +      }
   1.582 +    }
   1.583 +  },
   1.584 +
   1.585 +  /**
   1.586 +   * Increment the property value for numbers.
   1.587 +   *
   1.588 +   * @param {string} rawValue
   1.589 +   *        Raw value to increment.
   1.590 +   * @param {number} increment
   1.591 +   *        Amount to increase/decrease the raw value.
   1.592 +   * @param {object} info
   1.593 +   *        Object with info about the property value.
   1.594 +   * @return {string} the incremented value.
   1.595 +   */
   1.596 +  _incrementRawValue:
   1.597 +  function InplaceEditor_incrementRawValue(rawValue, increment, info)
   1.598 +  {
   1.599 +    let num = parseFloat(rawValue);
   1.600 +
   1.601 +    if (isNaN(num)) {
   1.602 +      return null;
   1.603 +    }
   1.604 +
   1.605 +    let number = /\d+(\.\d+)?/.exec(rawValue);
   1.606 +    let units = rawValue.substr(number.index + number[0].length);
   1.607 +
   1.608 +    // avoid rounding errors
   1.609 +    let newValue = Math.round((num + increment) * 1000) / 1000;
   1.610 +
   1.611 +    if (info && "minValue" in info) {
   1.612 +      newValue = Math.max(newValue, info.minValue);
   1.613 +    }
   1.614 +    if (info && "maxValue" in info) {
   1.615 +      newValue = Math.min(newValue, info.maxValue);
   1.616 +    }
   1.617 +
   1.618 +    newValue = newValue.toString();
   1.619 +
   1.620 +    return newValue + units;
   1.621 +  },
   1.622 +
   1.623 +  /**
   1.624 +   * Increment the property value for hex.
   1.625 +   *
   1.626 +   * @param {string} value
   1.627 +   *        Property value.
   1.628 +   * @param {number} increment
   1.629 +   *        Amount to increase/decrease the property value.
   1.630 +   * @param {number} offset
   1.631 +   *        Starting index of the property value.
   1.632 +   * @param {number} offsetEnd
   1.633 +   *        Ending index of the property value.
   1.634 +   * @return {object} object with properties 'value' and 'selection'.
   1.635 +   */
   1.636 +  _incHexColor:
   1.637 +  function InplaceEditor_incHexColor(rawValue, increment, offset, offsetEnd)
   1.638 +  {
   1.639 +    // Return early if no part of the rawValue is selected.
   1.640 +    if (offsetEnd > rawValue.length && offset >= rawValue.length) {
   1.641 +      return;
   1.642 +    }
   1.643 +    if (offset < 1 && offsetEnd <= 1) {
   1.644 +      return;
   1.645 +    }
   1.646 +    // Ignore the leading #.
   1.647 +    rawValue = rawValue.substr(1);
   1.648 +    --offset;
   1.649 +    --offsetEnd;
   1.650 +
   1.651 +    // Clamp the selection to within the actual value.
   1.652 +    offset = Math.max(offset, 0);
   1.653 +    offsetEnd = Math.min(offsetEnd, rawValue.length);
   1.654 +    offsetEnd = Math.max(offsetEnd, offset);
   1.655 +
   1.656 +    // Normalize #ABC -> #AABBCC.
   1.657 +    if (rawValue.length === 3) {
   1.658 +      rawValue = rawValue.charAt(0) + rawValue.charAt(0) +
   1.659 +                 rawValue.charAt(1) + rawValue.charAt(1) +
   1.660 +                 rawValue.charAt(2) + rawValue.charAt(2);
   1.661 +      offset *= 2;
   1.662 +      offsetEnd *= 2;
   1.663 +    }
   1.664 +
   1.665 +    if (rawValue.length !== 6) {
   1.666 +      return;
   1.667 +    }
   1.668 +
   1.669 +    // If no selection, increment an adjacent color, preferably one to the left.
   1.670 +    if (offset === offsetEnd) {
   1.671 +      if (offset === 0) {
   1.672 +        offsetEnd = 1;
   1.673 +      } else {
   1.674 +        offset = offsetEnd - 1;
   1.675 +      }
   1.676 +    }
   1.677 +
   1.678 +    // Make the selection cover entire parts.
   1.679 +    offset -= offset % 2;
   1.680 +    offsetEnd += offsetEnd % 2;
   1.681 +
   1.682 +    // Remap the increments from [0.1, 1, 10] to [1, 1, 16].
   1.683 +    if (-1 < increment && increment < 1) {
   1.684 +      increment = (increment < 0 ? -1 : 1);
   1.685 +    }
   1.686 +    if (Math.abs(increment) === 10) {
   1.687 +      increment = (increment < 0 ? -16 : 16);
   1.688 +    }
   1.689 +
   1.690 +    let isUpper = (rawValue.toUpperCase() === rawValue);
   1.691 +
   1.692 +    for (let pos = offset; pos < offsetEnd; pos += 2) {
   1.693 +      // Increment the part in [pos, pos+2).
   1.694 +      let mid = rawValue.substr(pos, 2);
   1.695 +      let value = parseInt(mid, 16);
   1.696 +
   1.697 +      if (isNaN(value)) {
   1.698 +        return;
   1.699 +      }
   1.700 +
   1.701 +      mid = Math.min(Math.max(value + increment, 0), 255).toString(16);
   1.702 +
   1.703 +      while (mid.length < 2) {
   1.704 +        mid = "0" + mid;
   1.705 +      }
   1.706 +      if (isUpper) {
   1.707 +        mid = mid.toUpperCase();
   1.708 +      }
   1.709 +
   1.710 +      rawValue = rawValue.substr(0, pos) + mid + rawValue.substr(pos + 2);
   1.711 +    }
   1.712 +
   1.713 +    return {
   1.714 +      value: "#" + rawValue,
   1.715 +      selection: [offset + 1, offsetEnd + 1]
   1.716 +    };
   1.717 +  },
   1.718 +
   1.719 +  /**
   1.720 +   * Cycle through the autocompletion suggestions in the popup.
   1.721 +   *
   1.722 +   * @param {boolean} aReverse
   1.723 +   *        true to select previous item from the popup.
   1.724 +   * @param {boolean} aNoSelect
   1.725 +   *        true to not select the text after selecting the newly selectedItem
   1.726 +   *        from the popup.
   1.727 +   */
   1.728 +  _cycleCSSSuggestion:
   1.729 +  function InplaceEditor_cycleCSSSuggestion(aReverse, aNoSelect)
   1.730 +  {
   1.731 +    // selectedItem can be null when nothing is selected in an empty editor.
   1.732 +    let {label, preLabel} = this.popup.selectedItem || {label: "", preLabel: ""};
   1.733 +    if (aReverse) {
   1.734 +      this.popup.selectPreviousItem();
   1.735 +    } else {
   1.736 +      this.popup.selectNextItem();
   1.737 +    }
   1.738 +    this._selectedIndex = this.popup.selectedIndex;
   1.739 +    let input = this.input;
   1.740 +    let pre = "";
   1.741 +    if (input.selectionStart < input.selectionEnd) {
   1.742 +      pre = input.value.slice(0, input.selectionStart);
   1.743 +    }
   1.744 +    else {
   1.745 +      pre = input.value.slice(0, input.selectionStart - label.length +
   1.746 +                                 preLabel.length);
   1.747 +    }
   1.748 +    let post = input.value.slice(input.selectionEnd, input.value.length);
   1.749 +    let item = this.popup.selectedItem;
   1.750 +    let toComplete = item.label.slice(item.preLabel.length);
   1.751 +    input.value = pre + toComplete + post;
   1.752 +    if (!aNoSelect) {
   1.753 +      input.setSelectionRange(pre.length, pre.length + toComplete.length);
   1.754 +    }
   1.755 +    else {
   1.756 +      input.setSelectionRange(pre.length + toComplete.length,
   1.757 +                              pre.length + toComplete.length);
   1.758 +    }
   1.759 +    this._updateSize();
   1.760 +    // This emit is mainly for the purpose of making the test flow simpler.
   1.761 +    this.emit("after-suggest");
   1.762 +  },
   1.763 +
   1.764 +  /**
   1.765 +   * Call the client's done handler and clear out.
   1.766 +   */
   1.767 +  _apply: function InplaceEditor_apply(aEvent)
   1.768 +  {
   1.769 +    if (this._applied) {
   1.770 +      return;
   1.771 +    }
   1.772 +
   1.773 +    this._applied = true;
   1.774 +
   1.775 +    if (this.done) {
   1.776 +      let val = this.input.value.trim();
   1.777 +      return this.done(this.cancelled ? this.initial : val, !this.cancelled);
   1.778 +    }
   1.779 +
   1.780 +    return null;
   1.781 +  },
   1.782 +
   1.783 +  /**
   1.784 +   * Handle loss of focus by calling done if it hasn't been called yet.
   1.785 +   */
   1.786 +  _onBlur: function InplaceEditor_onBlur(aEvent, aDoNotClear)
   1.787 +  {
   1.788 +    if (aEvent && this.popup && this.popup.isOpen &&
   1.789 +        this.popup.selectedIndex >= 0) {
   1.790 +      let label, preLabel;
   1.791 +      if (this._selectedIndex === undefined) {
   1.792 +        ({label, preLabel}) = this.popup.getItemAtIndex(this.popup.selectedIndex);
   1.793 +      }
   1.794 +      else {
   1.795 +        ({label, preLabel}) = this.popup.getItemAtIndex(this._selectedIndex);
   1.796 +      }
   1.797 +      let input = this.input;
   1.798 +      let pre = "";
   1.799 +      if (input.selectionStart < input.selectionEnd) {
   1.800 +        pre = input.value.slice(0, input.selectionStart);
   1.801 +      }
   1.802 +      else {
   1.803 +        pre = input.value.slice(0, input.selectionStart - label.length +
   1.804 +                                   preLabel.length);
   1.805 +      }
   1.806 +      let post = input.value.slice(input.selectionEnd, input.value.length);
   1.807 +      let item = this.popup.selectedItem;
   1.808 +      this._selectedIndex = this.popup.selectedIndex;
   1.809 +      let toComplete = item.label.slice(item.preLabel.length);
   1.810 +      input.value = pre + toComplete + post;
   1.811 +      input.setSelectionRange(pre.length + toComplete.length,
   1.812 +                              pre.length + toComplete.length);
   1.813 +      this._updateSize();
   1.814 +      // Wait for the popup to hide and then focus input async otherwise it does
   1.815 +      // not work.
   1.816 +      let onPopupHidden = () => {
   1.817 +        this.popup._panel.removeEventListener("popuphidden", onPopupHidden);
   1.818 +        this.doc.defaultView.setTimeout(()=> {
   1.819 +          input.focus();
   1.820 +          this.emit("after-suggest");
   1.821 +        }, 0);
   1.822 +      };
   1.823 +      this.popup._panel.addEventListener("popuphidden", onPopupHidden);
   1.824 +      this.popup.hidePopup();
   1.825 +      // Content type other than CSS_MIXED is used in rule-view where the values
   1.826 +      // are live previewed. So we apply the value before returning.
   1.827 +      if (this.contentType != CONTENT_TYPES.CSS_MIXED) {
   1.828 +        this._apply();
   1.829 +      }
   1.830 +      return;
   1.831 +    }
   1.832 +    this._apply();
   1.833 +    if (!aDoNotClear) {
   1.834 +      this._clear();
   1.835 +    }
   1.836 +  },
   1.837 +
   1.838 +  /**
   1.839 +   * Handle the input field's keypress event.
   1.840 +   */
   1.841 +  _onKeyPress: function InplaceEditor_onKeyPress(aEvent)
   1.842 +  {
   1.843 +    let prevent = false;
   1.844 +
   1.845 +    const largeIncrement = 100;
   1.846 +    const mediumIncrement = 10;
   1.847 +    const smallIncrement = 0.1;
   1.848 +
   1.849 +    let increment = 0;
   1.850 +
   1.851 +    if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_UP
   1.852 +       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP) {
   1.853 +      increment = 1;
   1.854 +    } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DOWN
   1.855 +       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
   1.856 +      increment = -1;
   1.857 +    }
   1.858 +
   1.859 +    if (aEvent.shiftKey && !aEvent.altKey) {
   1.860 +      if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP
   1.861 +           ||  aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
   1.862 +        increment *= largeIncrement;
   1.863 +      } else {
   1.864 +        increment *= mediumIncrement;
   1.865 +      }
   1.866 +    } else if (aEvent.altKey && !aEvent.shiftKey) {
   1.867 +      increment *= smallIncrement;
   1.868 +    }
   1.869 +
   1.870 +    let cycling = false;
   1.871 +    if (increment && this._incrementValue(increment) ) {
   1.872 +      this._updateSize();
   1.873 +      prevent = true;
   1.874 +      cycling = true;
   1.875 +    } else if (increment && this.popup && this.popup.isOpen) {
   1.876 +      cycling = true;
   1.877 +      prevent = true;
   1.878 +      this._cycleCSSSuggestion(increment > 0);
   1.879 +      this._doValidation();
   1.880 +    }
   1.881 +
   1.882 +    if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_BACK_SPACE ||
   1.883 +        aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DELETE ||
   1.884 +        aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_LEFT ||
   1.885 +        aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RIGHT) {
   1.886 +      if (this.popup && this.popup.isOpen) {
   1.887 +        this.popup.hidePopup();
   1.888 +      }
   1.889 +    } else if (!cycling && !aEvent.metaKey && !aEvent.altKey && !aEvent.ctrlKey) {
   1.890 +      this._maybeSuggestCompletion();
   1.891 +    }
   1.892 +
   1.893 +    if (this.multiline &&
   1.894 +        aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN &&
   1.895 +        aEvent.shiftKey) {
   1.896 +      prevent = false;
   1.897 +    } else if (aEvent.charCode in this._advanceCharCodes
   1.898 +       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN
   1.899 +       || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB) {
   1.900 +      prevent = true;
   1.901 +
   1.902 +      let direction = FOCUS_FORWARD;
   1.903 +      if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB &&
   1.904 +          aEvent.shiftKey) {
   1.905 +        direction = FOCUS_BACKWARD;
   1.906 +      }
   1.907 +      if (this.stopOnReturn && aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
   1.908 +        direction = null;
   1.909 +      }
   1.910 +
   1.911 +      // Now we don't want to suggest anything as we are moving out.
   1.912 +      this._preventSuggestions = true;
   1.913 +      // But we still want to show suggestions for css values. i.e. moving out
   1.914 +      // of css property input box in forward direction
   1.915 +      if (this.contentType == CONTENT_TYPES.CSS_PROPERTY &&
   1.916 +          direction == FOCUS_FORWARD) {
   1.917 +        this._preventSuggestions = false;
   1.918 +      }
   1.919 +
   1.920 +      let input = this.input;
   1.921 +
   1.922 +      if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB &&
   1.923 +          this.contentType == CONTENT_TYPES.CSS_MIXED) {
   1.924 +        if (this.popup && input.selectionStart < input.selectionEnd) {
   1.925 +          aEvent.preventDefault();
   1.926 +          input.setSelectionRange(input.selectionEnd, input.selectionEnd);
   1.927 +          this.emit("after-suggest");
   1.928 +          return;
   1.929 +        }
   1.930 +        else if (this.popup && this.popup.isOpen) {
   1.931 +          aEvent.preventDefault();
   1.932 +          this._cycleCSSSuggestion(aEvent.shiftKey, true);
   1.933 +          return;
   1.934 +        }
   1.935 +      }
   1.936 +
   1.937 +      this._apply();
   1.938 +
   1.939 +      // Close the popup if open
   1.940 +      if (this.popup && this.popup.isOpen) {
   1.941 +        this.popup.hidePopup();
   1.942 +      }
   1.943 +
   1.944 +      if (direction !== null && focusManager.focusedElement === input) {
   1.945 +        // If the focused element wasn't changed by the done callback,
   1.946 +        // move the focus as requested.
   1.947 +        let next = moveFocus(this.doc.defaultView, direction);
   1.948 +
   1.949 +        // If the next node to be focused has been tagged as an editable
   1.950 +        // node, send it a click event to trigger
   1.951 +        if (next && next.ownerDocument === this.doc && next._editable) {
   1.952 +          next.click();
   1.953 +        }
   1.954 +      }
   1.955 +
   1.956 +      this._clear();
   1.957 +    } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE) {
   1.958 +      // Cancel and blur ourselves.
   1.959 +      // Now we don't want to suggest anything as we are moving out.
   1.960 +      this._preventSuggestions = true;
   1.961 +      // Close the popup if open
   1.962 +      if (this.popup && this.popup.isOpen) {
   1.963 +        this.popup.hidePopup();
   1.964 +      }
   1.965 +      prevent = true;
   1.966 +      this.cancelled = true;
   1.967 +      this._apply();
   1.968 +      this._clear();
   1.969 +      aEvent.stopPropagation();
   1.970 +    } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) {
   1.971 +      // No need for leading spaces here.  This is particularly
   1.972 +      // noticable when adding a property: it's very natural to type
   1.973 +      // <name>: (which advances to the next property) then spacebar.
   1.974 +      prevent = !this.input.value;
   1.975 +    }
   1.976 +
   1.977 +    if (prevent) {
   1.978 +      aEvent.preventDefault();
   1.979 +    }
   1.980 +  },
   1.981 +
   1.982 +  /**
   1.983 +   * Handle the input field's keyup event.
   1.984 +   */
   1.985 +  _onKeyup: function(aEvent) {
   1.986 +    this._applied = false;
   1.987 +  },
   1.988 +
   1.989 +  /**
   1.990 +   * Handle changes to the input text.
   1.991 +   */
   1.992 +  _onInput: function InplaceEditor_onInput(aEvent)
   1.993 +  {
   1.994 +    // Validate the entered value.
   1.995 +    this._doValidation();
   1.996 +
   1.997 +    // Update size if we're autosizing.
   1.998 +    if (this._measurement) {
   1.999 +      this._updateSize();
  1.1000 +    }
  1.1001 +
  1.1002 +    // Call the user's change handler if available.
  1.1003 +    if (this.change) {
  1.1004 +      this.change(this.input.value.trim());
  1.1005 +    }
  1.1006 +  },
  1.1007 +
  1.1008 +  /**
  1.1009 +   * Fire validation callback with current input
  1.1010 +   */
  1.1011 +  _doValidation: function()
  1.1012 +  {
  1.1013 +    if (this.validate && this.input) {
  1.1014 +      this.validate(this.input.value);
  1.1015 +    }
  1.1016 +  },
  1.1017 +
  1.1018 +  /**
  1.1019 +   * Handles displaying suggestions based on the current input.
  1.1020 +   *
  1.1021 +   * @param {boolean} aNoAutoInsert
  1.1022 +   *        true if you don't want to automatically insert the first suggestion
  1.1023 +   */
  1.1024 +  _maybeSuggestCompletion: function(aNoAutoInsert) {
  1.1025 +    // Input can be null in cases when you intantaneously switch out of it.
  1.1026 +    if (!this.input) {
  1.1027 +      return;
  1.1028 +    }
  1.1029 +    let preTimeoutQuery = this.input.value;
  1.1030 +    // Since we are calling this method from a keypress event handler, the
  1.1031 +    // |input.value| does not include currently typed character. Thus we perform
  1.1032 +    // this method async.
  1.1033 +    this.doc.defaultView.setTimeout(() => {
  1.1034 +      if (this._preventSuggestions) {
  1.1035 +        this._preventSuggestions = false;
  1.1036 +        return;
  1.1037 +      }
  1.1038 +      if (this.contentType == CONTENT_TYPES.PLAIN_TEXT) {
  1.1039 +        return;
  1.1040 +      }
  1.1041 +      if (!this.input) {
  1.1042 +        return;
  1.1043 +      }
  1.1044 +      let input = this.input;
  1.1045 +      // The length of input.value should be increased by 1
  1.1046 +      if (input.value.length - preTimeoutQuery.length > 1) {
  1.1047 +        return;
  1.1048 +      }
  1.1049 +      let query = input.value.slice(0, input.selectionStart);
  1.1050 +      let startCheckQuery = query;
  1.1051 +      if (query == null) {
  1.1052 +        return;
  1.1053 +      }
  1.1054 +      // If nothing is selected and there is a non-space character after the
  1.1055 +      // cursor, do not autocomplete.
  1.1056 +      if (input.selectionStart == input.selectionEnd &&
  1.1057 +          input.selectionStart < input.value.length &&
  1.1058 +          input.value.slice(input.selectionStart)[0] != " ") {
  1.1059 +        // This emit is mainly to make the test flow simpler.
  1.1060 +        this.emit("after-suggest", "nothing to autocomplete");
  1.1061 +        return;
  1.1062 +      }
  1.1063 +      let list = [];
  1.1064 +      if (this.contentType == CONTENT_TYPES.CSS_PROPERTY) {
  1.1065 +        list = CSSPropertyList;
  1.1066 +      } else if (this.contentType == CONTENT_TYPES.CSS_VALUE) {
  1.1067 +        // Get the last query to be completed before the caret.
  1.1068 +        let match = /([^\s,.\/]+$)/.exec(query);
  1.1069 +        if (match) {
  1.1070 +          startCheckQuery = match[0];
  1.1071 +        } else {
  1.1072 +          startCheckQuery = "";
  1.1073 +        }
  1.1074 +
  1.1075 +        list =
  1.1076 +          ["!important", ...domUtils.getCSSValuesForProperty(this.property.name)];
  1.1077 +
  1.1078 +        if (query == "") {
  1.1079 +          // Do not suggest '!important' without any manually typed character.
  1.1080 +          list.splice(0, 1);
  1.1081 +        }
  1.1082 +      } else if (this.contentType == CONTENT_TYPES.CSS_MIXED &&
  1.1083 +                 /^\s*style\s*=/.test(query)) {
  1.1084 +        // Detecting if cursor is at property or value;
  1.1085 +        let match = query.match(/([:;"'=]?)\s*([^"';:=]+)?$/);
  1.1086 +        if (match && match.length >= 2) {
  1.1087 +          if (match[1] == ":") { // We are in CSS value completion
  1.1088 +            let propertyName =
  1.1089 +              query.match(/[;"'=]\s*([^"';:= ]+)\s*:\s*[^"';:=]*$/)[1];
  1.1090 +            list =
  1.1091 +              ["!important;", ...domUtils.getCSSValuesForProperty(propertyName)];
  1.1092 +            let matchLastQuery = /([^\s,.\/]+$)/.exec(match[2] || "");
  1.1093 +            if (matchLastQuery) {
  1.1094 +              startCheckQuery = matchLastQuery[0];
  1.1095 +            } else {
  1.1096 +              startCheckQuery = "";
  1.1097 +            }
  1.1098 +            if (!match[2]) {
  1.1099 +              // Don't suggest '!important' without any manually typed character
  1.1100 +              list.splice(0, 1);
  1.1101 +            }
  1.1102 +          } else if (match[1]) { // We are in CSS property name completion
  1.1103 +            list = CSSPropertyList;
  1.1104 +            startCheckQuery = match[2];
  1.1105 +          }
  1.1106 +          if (startCheckQuery == null) {
  1.1107 +            // This emit is mainly to make the test flow simpler.
  1.1108 +            this.emit("after-suggest", "nothing to autocomplete");
  1.1109 +            return;
  1.1110 +          }
  1.1111 +        }
  1.1112 +      }
  1.1113 +      if (!aNoAutoInsert) {
  1.1114 +        list.some(item => {
  1.1115 +          if (startCheckQuery != null && item.startsWith(startCheckQuery)) {
  1.1116 +            input.value = query + item.slice(startCheckQuery.length) +
  1.1117 +                          input.value.slice(query.length);
  1.1118 +            input.setSelectionRange(query.length, query.length + item.length -
  1.1119 +                                                  startCheckQuery.length);
  1.1120 +            this._updateSize();
  1.1121 +            return true;
  1.1122 +          }
  1.1123 +        });
  1.1124 +      }
  1.1125 +
  1.1126 +      if (!this.popup) {
  1.1127 +        // This emit is mainly to make the test flow simpler.
  1.1128 +        this.emit("after-suggest", "no popup");
  1.1129 +        return;
  1.1130 +      }
  1.1131 +      let finalList = [];
  1.1132 +      let length = list.length;
  1.1133 +      for (let i = 0, count = 0; i < length && count < MAX_POPUP_ENTRIES; i++) {
  1.1134 +        if (startCheckQuery != null && list[i].startsWith(startCheckQuery)) {
  1.1135 +          count++;
  1.1136 +          finalList.push({
  1.1137 +            preLabel: startCheckQuery,
  1.1138 +            label: list[i]
  1.1139 +          });
  1.1140 +        }
  1.1141 +        else if (count > 0) {
  1.1142 +          // Since count was incremented, we had already crossed the entries
  1.1143 +          // which would have started with query, assuming that list is sorted.
  1.1144 +          break;
  1.1145 +        }
  1.1146 +        else if (startCheckQuery != null && list[i][0] > startCheckQuery[0]) {
  1.1147 +          // We have crossed all possible matches alphabetically.
  1.1148 +          break;
  1.1149 +        }
  1.1150 +      }
  1.1151 +
  1.1152 +      if (finalList.length > 1) {
  1.1153 +        // Calculate the offset for the popup to be opened.
  1.1154 +        let x = (this.input.selectionStart - startCheckQuery.length) *
  1.1155 +                this.inputCharWidth;
  1.1156 +        this.popup.setItems(finalList);
  1.1157 +        this.popup.openPopup(this.input, x);
  1.1158 +        if (aNoAutoInsert) {
  1.1159 +          this.popup.selectedIndex = -1;
  1.1160 +        }
  1.1161 +      } else {
  1.1162 +        this.popup.hidePopup();
  1.1163 +      }
  1.1164 +      // This emit is mainly for the purpose of making the test flow simpler.
  1.1165 +      this.emit("after-suggest");
  1.1166 +      this._doValidation();
  1.1167 +    }, 0);
  1.1168 +  }
  1.1169 +};
  1.1170 +
  1.1171 +/**
  1.1172 + * Copy text-related styles from one element to another.
  1.1173 + */
  1.1174 +function copyTextStyles(aFrom, aTo)
  1.1175 +{
  1.1176 +  let win = aFrom.ownerDocument.defaultView;
  1.1177 +  let style = win.getComputedStyle(aFrom);
  1.1178 +  aTo.style.fontFamily = style.getPropertyCSSValue("font-family").cssText;
  1.1179 +  aTo.style.fontSize = style.getPropertyCSSValue("font-size").cssText;
  1.1180 +  aTo.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText;
  1.1181 +  aTo.style.fontStyle = style.getPropertyCSSValue("font-style").cssText;
  1.1182 +}
  1.1183 +
  1.1184 +/**
  1.1185 + * Trigger a focus change similar to pressing tab/shift-tab.
  1.1186 + */
  1.1187 +function moveFocus(aWin, aDirection)
  1.1188 +{
  1.1189 +  return focusManager.moveFocus(aWin, null, aDirection, 0);
  1.1190 +}
  1.1191 +
  1.1192 +
  1.1193 +XPCOMUtils.defineLazyGetter(this, "focusManager", function() {
  1.1194 +  return Services.focus;
  1.1195 +});
  1.1196 +
  1.1197 +XPCOMUtils.defineLazyGetter(this, "CSSPropertyList", function() {
  1.1198 +  return domUtils.getCSSPropertyNames(domUtils.INCLUDE_ALIASES).sort();
  1.1199 +});
  1.1200 +
  1.1201 +XPCOMUtils.defineLazyGetter(this, "domUtils", function() {
  1.1202 +  return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
  1.1203 +});

mercurial