browser/devtools/shared/inplace-editor.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
michael@0 2 /* vim: set ts=2 et sw=2 tw=80: */
michael@0 3
michael@0 4 /**
michael@0 5 * This Source Code Form is subject to the terms of the Mozilla Public
michael@0 6 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
michael@0 8 *
michael@0 9 * Basic use:
michael@0 10 * let spanToEdit = document.getElementById("somespan");
michael@0 11 *
michael@0 12 * editableField({
michael@0 13 * element: spanToEdit,
michael@0 14 * done: function(value, commit) {
michael@0 15 * if (commit) {
michael@0 16 * spanToEdit.textContent = value;
michael@0 17 * }
michael@0 18 * },
michael@0 19 * trigger: "dblclick"
michael@0 20 * });
michael@0 21 *
michael@0 22 * See editableField() for more options.
michael@0 23 */
michael@0 24
michael@0 25 "use strict";
michael@0 26
michael@0 27 const {Ci, Cu, Cc} = require("chrome");
michael@0 28
michael@0 29 const HTML_NS = "http://www.w3.org/1999/xhtml";
michael@0 30 const CONTENT_TYPES = {
michael@0 31 PLAIN_TEXT: 0,
michael@0 32 CSS_VALUE: 1,
michael@0 33 CSS_MIXED: 2,
michael@0 34 CSS_PROPERTY: 3,
michael@0 35 };
michael@0 36 const MAX_POPUP_ENTRIES = 10;
michael@0 37
michael@0 38 const FOCUS_FORWARD = Ci.nsIFocusManager.MOVEFOCUS_FORWARD;
michael@0 39 const FOCUS_BACKWARD = Ci.nsIFocusManager.MOVEFOCUS_BACKWARD;
michael@0 40
michael@0 41 Cu.import("resource://gre/modules/Services.jsm");
michael@0 42 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 43 Cu.import("resource://gre/modules/devtools/event-emitter.js");
michael@0 44
michael@0 45 /**
michael@0 46 * Mark a span editable. |editableField| will listen for the span to
michael@0 47 * be focused and create an InlineEditor to handle text input.
michael@0 48 * Changes will be committed when the InlineEditor's input is blurred
michael@0 49 * or dropped when the user presses escape.
michael@0 50 *
michael@0 51 * @param {object} aOptions
michael@0 52 * Options for the editable field, including:
michael@0 53 * {Element} element:
michael@0 54 * (required) The span to be edited on focus.
michael@0 55 * {function} canEdit:
michael@0 56 * Will be called before creating the inplace editor. Editor
michael@0 57 * won't be created if canEdit returns false.
michael@0 58 * {function} start:
michael@0 59 * Will be called when the inplace editor is initialized.
michael@0 60 * {function} change:
michael@0 61 * Will be called when the text input changes. Will be called
michael@0 62 * with the current value of the text input.
michael@0 63 * {function} done:
michael@0 64 * Called when input is committed or blurred. Called with
michael@0 65 * current value and a boolean telling the caller whether to
michael@0 66 * commit the change. This function is called before the editor
michael@0 67 * has been torn down.
michael@0 68 * {function} destroy:
michael@0 69 * Called when the editor is destroyed and has been torn down.
michael@0 70 * {string} advanceChars:
michael@0 71 * If any characters in advanceChars are typed, focus will advance
michael@0 72 * to the next element.
michael@0 73 * {boolean} stopOnReturn:
michael@0 74 * If true, the return key will not advance the editor to the next
michael@0 75 * focusable element.
michael@0 76 * {string} trigger: The DOM event that should trigger editing,
michael@0 77 * defaults to "click"
michael@0 78 */
michael@0 79 function editableField(aOptions)
michael@0 80 {
michael@0 81 return editableItem(aOptions, function(aElement, aEvent) {
michael@0 82 new InplaceEditor(aOptions, aEvent);
michael@0 83 });
michael@0 84 }
michael@0 85
michael@0 86 exports.editableField = editableField;
michael@0 87
michael@0 88 /**
michael@0 89 * Handle events for an element that should respond to
michael@0 90 * clicks and sit in the editing tab order, and call
michael@0 91 * a callback when it is activated.
michael@0 92 *
michael@0 93 * @param {object} aOptions
michael@0 94 * The options for this editor, including:
michael@0 95 * {Element} element: The DOM element.
michael@0 96 * {string} trigger: The DOM event that should trigger editing,
michael@0 97 * defaults to "click"
michael@0 98 * @param {function} aCallback
michael@0 99 * Called when the editor is activated.
michael@0 100 */
michael@0 101 function editableItem(aOptions, aCallback)
michael@0 102 {
michael@0 103 let trigger = aOptions.trigger || "click"
michael@0 104 let element = aOptions.element;
michael@0 105 element.addEventListener(trigger, function(evt) {
michael@0 106 if (evt.target.nodeName !== "a") {
michael@0 107 let win = this.ownerDocument.defaultView;
michael@0 108 let selection = win.getSelection();
michael@0 109 if (trigger != "click" || selection.isCollapsed) {
michael@0 110 aCallback(element, evt);
michael@0 111 }
michael@0 112 evt.stopPropagation();
michael@0 113 }
michael@0 114 }, false);
michael@0 115
michael@0 116 // If focused by means other than a click, start editing by
michael@0 117 // pressing enter or space.
michael@0 118 element.addEventListener("keypress", function(evt) {
michael@0 119 if (evt.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN ||
michael@0 120 evt.charCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) {
michael@0 121 aCallback(element);
michael@0 122 }
michael@0 123 }, true);
michael@0 124
michael@0 125 // Ugly workaround - the element is focused on mousedown but
michael@0 126 // the editor is activated on click/mouseup. This leads
michael@0 127 // to an ugly flash of the focus ring before showing the editor.
michael@0 128 // So hide the focus ring while the mouse is down.
michael@0 129 element.addEventListener("mousedown", function(evt) {
michael@0 130 if (evt.target.nodeName !== "a") {
michael@0 131 let cleanup = function() {
michael@0 132 element.style.removeProperty("outline-style");
michael@0 133 element.removeEventListener("mouseup", cleanup, false);
michael@0 134 element.removeEventListener("mouseout", cleanup, false);
michael@0 135 };
michael@0 136 element.style.setProperty("outline-style", "none");
michael@0 137 element.addEventListener("mouseup", cleanup, false);
michael@0 138 element.addEventListener("mouseout", cleanup, false);
michael@0 139 }
michael@0 140 }, false);
michael@0 141
michael@0 142 // Mark the element editable field for tab
michael@0 143 // navigation while editing.
michael@0 144 element._editable = true;
michael@0 145 }
michael@0 146
michael@0 147 exports.editableItem = this.editableItem;
michael@0 148
michael@0 149 /*
michael@0 150 * Various API consumers (especially tests) sometimes want to grab the
michael@0 151 * inplaceEditor expando off span elements. However, when each global has its
michael@0 152 * own compartment, those expandos live on Xray wrappers that are only visible
michael@0 153 * within this JSM. So we provide a little workaround here.
michael@0 154 */
michael@0 155
michael@0 156 function getInplaceEditorForSpan(aSpan)
michael@0 157 {
michael@0 158 return aSpan.inplaceEditor;
michael@0 159 };
michael@0 160 exports.getInplaceEditorForSpan = getInplaceEditorForSpan;
michael@0 161
michael@0 162 function InplaceEditor(aOptions, aEvent)
michael@0 163 {
michael@0 164 this.elt = aOptions.element;
michael@0 165 let doc = this.elt.ownerDocument;
michael@0 166 this.doc = doc;
michael@0 167 this.elt.inplaceEditor = this;
michael@0 168
michael@0 169 this.change = aOptions.change;
michael@0 170 this.done = aOptions.done;
michael@0 171 this.destroy = aOptions.destroy;
michael@0 172 this.initial = aOptions.initial ? aOptions.initial : this.elt.textContent;
michael@0 173 this.multiline = aOptions.multiline || false;
michael@0 174 this.stopOnReturn = !!aOptions.stopOnReturn;
michael@0 175 this.contentType = aOptions.contentType || CONTENT_TYPES.PLAIN_TEXT;
michael@0 176 this.property = aOptions.property;
michael@0 177 this.popup = aOptions.popup;
michael@0 178
michael@0 179 this._onBlur = this._onBlur.bind(this);
michael@0 180 this._onKeyPress = this._onKeyPress.bind(this);
michael@0 181 this._onInput = this._onInput.bind(this);
michael@0 182 this._onKeyup = this._onKeyup.bind(this);
michael@0 183
michael@0 184 this._createInput();
michael@0 185 this._autosize();
michael@0 186 this.inputCharWidth = this._getInputCharWidth();
michael@0 187
michael@0 188 // Pull out character codes for advanceChars, listing the
michael@0 189 // characters that should trigger a blur.
michael@0 190 this._advanceCharCodes = {};
michael@0 191 let advanceChars = aOptions.advanceChars || '';
michael@0 192 for (let i = 0; i < advanceChars.length; i++) {
michael@0 193 this._advanceCharCodes[advanceChars.charCodeAt(i)] = true;
michael@0 194 }
michael@0 195
michael@0 196 // Hide the provided element and add our editor.
michael@0 197 this.originalDisplay = this.elt.style.display;
michael@0 198 this.elt.style.display = "none";
michael@0 199 this.elt.parentNode.insertBefore(this.input, this.elt);
michael@0 200
michael@0 201 if (typeof(aOptions.selectAll) == "undefined" || aOptions.selectAll) {
michael@0 202 this.input.select();
michael@0 203 }
michael@0 204 this.input.focus();
michael@0 205
michael@0 206 if (this.contentType == CONTENT_TYPES.CSS_VALUE && this.input.value == "") {
michael@0 207 this._maybeSuggestCompletion(true);
michael@0 208 }
michael@0 209
michael@0 210 this.input.addEventListener("blur", this._onBlur, false);
michael@0 211 this.input.addEventListener("keypress", this._onKeyPress, false);
michael@0 212 this.input.addEventListener("input", this._onInput, false);
michael@0 213
michael@0 214 this.input.addEventListener("dblclick",
michael@0 215 (e) => { e.stopPropagation(); }, false);
michael@0 216 this.input.addEventListener("mousedown",
michael@0 217 (e) => { e.stopPropagation(); }, false);
michael@0 218
michael@0 219 this.validate = aOptions.validate;
michael@0 220
michael@0 221 if (this.validate) {
michael@0 222 this.input.addEventListener("keyup", this._onKeyup, false);
michael@0 223 }
michael@0 224
michael@0 225 if (aOptions.start) {
michael@0 226 aOptions.start(this, aEvent);
michael@0 227 }
michael@0 228
michael@0 229 EventEmitter.decorate(this);
michael@0 230 }
michael@0 231
michael@0 232 exports.InplaceEditor = InplaceEditor;
michael@0 233
michael@0 234 InplaceEditor.CONTENT_TYPES = CONTENT_TYPES;
michael@0 235
michael@0 236 InplaceEditor.prototype = {
michael@0 237 _createInput: function InplaceEditor_createEditor()
michael@0 238 {
michael@0 239 this.input =
michael@0 240 this.doc.createElementNS(HTML_NS, this.multiline ? "textarea" : "input");
michael@0 241 this.input.inplaceEditor = this;
michael@0 242 this.input.classList.add("styleinspector-propertyeditor");
michael@0 243 this.input.value = this.initial;
michael@0 244
michael@0 245 copyTextStyles(this.elt, this.input);
michael@0 246 },
michael@0 247
michael@0 248 /**
michael@0 249 * Get rid of the editor.
michael@0 250 */
michael@0 251 _clear: function InplaceEditor_clear()
michael@0 252 {
michael@0 253 if (!this.input) {
michael@0 254 // Already cleared.
michael@0 255 return;
michael@0 256 }
michael@0 257
michael@0 258 this.input.removeEventListener("blur", this._onBlur, false);
michael@0 259 this.input.removeEventListener("keypress", this._onKeyPress, false);
michael@0 260 this.input.removeEventListener("keyup", this._onKeyup, false);
michael@0 261 this.input.removeEventListener("oninput", this._onInput, false);
michael@0 262 this._stopAutosize();
michael@0 263
michael@0 264 this.elt.style.display = this.originalDisplay;
michael@0 265 this.elt.focus();
michael@0 266
michael@0 267 this.elt.parentNode.removeChild(this.input);
michael@0 268 this.input = null;
michael@0 269
michael@0 270 delete this.elt.inplaceEditor;
michael@0 271 delete this.elt;
michael@0 272
michael@0 273 if (this.destroy) {
michael@0 274 this.destroy();
michael@0 275 }
michael@0 276 },
michael@0 277
michael@0 278 /**
michael@0 279 * Keeps the editor close to the size of its input string. This is pretty
michael@0 280 * crappy, suggestions for improvement welcome.
michael@0 281 */
michael@0 282 _autosize: function InplaceEditor_autosize()
michael@0 283 {
michael@0 284 // Create a hidden, absolutely-positioned span to measure the text
michael@0 285 // in the input. Boo.
michael@0 286
michael@0 287 // We can't just measure the original element because a) we don't
michael@0 288 // change the underlying element's text ourselves (we leave that
michael@0 289 // up to the client), and b) without tweaking the style of the
michael@0 290 // original element, it might wrap differently or something.
michael@0 291 this._measurement =
michael@0 292 this.doc.createElementNS(HTML_NS, this.multiline ? "pre" : "span");
michael@0 293 this._measurement.className = "autosizer";
michael@0 294 this.elt.parentNode.appendChild(this._measurement);
michael@0 295 let style = this._measurement.style;
michael@0 296 style.visibility = "hidden";
michael@0 297 style.position = "absolute";
michael@0 298 style.top = "0";
michael@0 299 style.left = "0";
michael@0 300 copyTextStyles(this.input, this._measurement);
michael@0 301 this._updateSize();
michael@0 302 },
michael@0 303
michael@0 304 /**
michael@0 305 * Clean up the mess created by _autosize().
michael@0 306 */
michael@0 307 _stopAutosize: function InplaceEditor_stopAutosize()
michael@0 308 {
michael@0 309 if (!this._measurement) {
michael@0 310 return;
michael@0 311 }
michael@0 312 this._measurement.parentNode.removeChild(this._measurement);
michael@0 313 delete this._measurement;
michael@0 314 },
michael@0 315
michael@0 316 /**
michael@0 317 * Size the editor to fit its current contents.
michael@0 318 */
michael@0 319 _updateSize: function InplaceEditor_updateSize()
michael@0 320 {
michael@0 321 // Replace spaces with non-breaking spaces. Otherwise setting
michael@0 322 // the span's textContent will collapse spaces and the measurement
michael@0 323 // will be wrong.
michael@0 324 this._measurement.textContent = this.input.value.replace(/ /g, '\u00a0');
michael@0 325
michael@0 326 // We add a bit of padding to the end. Should be enough to fit
michael@0 327 // any letter that could be typed, otherwise we'll scroll before
michael@0 328 // we get a chance to resize. Yuck.
michael@0 329 let width = this._measurement.offsetWidth + 10;
michael@0 330
michael@0 331 if (this.multiline) {
michael@0 332 // Make sure there's some content in the current line. This is a hack to
michael@0 333 // account for the fact that after adding a newline the <pre> doesn't grow
michael@0 334 // unless there's text content on the line.
michael@0 335 width += 15;
michael@0 336 this._measurement.textContent += "M";
michael@0 337 this.input.style.height = this._measurement.offsetHeight + "px";
michael@0 338 }
michael@0 339
michael@0 340 this.input.style.width = width + "px";
michael@0 341 },
michael@0 342
michael@0 343 /**
michael@0 344 * Get the width of a single character in the input to properly position the
michael@0 345 * autocompletion popup.
michael@0 346 */
michael@0 347 _getInputCharWidth: function InplaceEditor_getInputCharWidth()
michael@0 348 {
michael@0 349 // Just make the text content to be 'x' to get the width of any character in
michael@0 350 // a monospace font.
michael@0 351 this._measurement.textContent = "x";
michael@0 352 return this._measurement.offsetWidth;
michael@0 353 },
michael@0 354
michael@0 355 /**
michael@0 356 * Increment property values in rule view.
michael@0 357 *
michael@0 358 * @param {number} increment
michael@0 359 * The amount to increase/decrease the property value.
michael@0 360 * @return {bool} true if value has been incremented.
michael@0 361 */
michael@0 362 _incrementValue: function InplaceEditor_incrementValue(increment)
michael@0 363 {
michael@0 364 let value = this.input.value;
michael@0 365 let selectionStart = this.input.selectionStart;
michael@0 366 let selectionEnd = this.input.selectionEnd;
michael@0 367
michael@0 368 let newValue = this._incrementCSSValue(value, increment, selectionStart,
michael@0 369 selectionEnd);
michael@0 370
michael@0 371 if (!newValue) {
michael@0 372 return false;
michael@0 373 }
michael@0 374
michael@0 375 this.input.value = newValue.value;
michael@0 376 this.input.setSelectionRange(newValue.start, newValue.end);
michael@0 377 this._doValidation();
michael@0 378
michael@0 379 // Call the user's change handler if available.
michael@0 380 if (this.change) {
michael@0 381 this.change(this.input.value.trim());
michael@0 382 }
michael@0 383
michael@0 384 return true;
michael@0 385 },
michael@0 386
michael@0 387 /**
michael@0 388 * Increment the property value based on the property type.
michael@0 389 *
michael@0 390 * @param {string} value
michael@0 391 * Property value.
michael@0 392 * @param {number} increment
michael@0 393 * Amount to increase/decrease the property value.
michael@0 394 * @param {number} selStart
michael@0 395 * Starting index of the value.
michael@0 396 * @param {number} selEnd
michael@0 397 * Ending index of the value.
michael@0 398 * @return {object} object with properties 'value', 'start', and 'end'.
michael@0 399 */
michael@0 400 _incrementCSSValue: function InplaceEditor_incrementCSSValue(value, increment,
michael@0 401 selStart, selEnd)
michael@0 402 {
michael@0 403 let range = this._parseCSSValue(value, selStart);
michael@0 404 let type = (range && range.type) || "";
michael@0 405 let rawValue = (range ? value.substring(range.start, range.end) : "");
michael@0 406 let incrementedValue = null, selection;
michael@0 407
michael@0 408 if (type === "num") {
michael@0 409 let newValue = this._incrementRawValue(rawValue, increment);
michael@0 410 if (newValue !== null) {
michael@0 411 incrementedValue = newValue;
michael@0 412 selection = [0, incrementedValue.length];
michael@0 413 }
michael@0 414 } else if (type === "hex") {
michael@0 415 let exprOffset = selStart - range.start;
michael@0 416 let exprOffsetEnd = selEnd - range.start;
michael@0 417 let newValue = this._incHexColor(rawValue, increment, exprOffset,
michael@0 418 exprOffsetEnd);
michael@0 419 if (newValue) {
michael@0 420 incrementedValue = newValue.value;
michael@0 421 selection = newValue.selection;
michael@0 422 }
michael@0 423 } else {
michael@0 424 let info;
michael@0 425 if (type === "rgb" || type === "hsl") {
michael@0 426 info = {};
michael@0 427 let part = value.substring(range.start, selStart).split(",").length - 1;
michael@0 428 if (part === 3) { // alpha
michael@0 429 info.minValue = 0;
michael@0 430 info.maxValue = 1;
michael@0 431 } else if (type === "rgb") {
michael@0 432 info.minValue = 0;
michael@0 433 info.maxValue = 255;
michael@0 434 } else if (part !== 0) { // hsl percentage
michael@0 435 info.minValue = 0;
michael@0 436 info.maxValue = 100;
michael@0 437
michael@0 438 // select the previous number if the selection is at the end of a
michael@0 439 // percentage sign.
michael@0 440 if (value.charAt(selStart - 1) === "%") {
michael@0 441 --selStart;
michael@0 442 }
michael@0 443 }
michael@0 444 }
michael@0 445 return this._incrementGenericValue(value, increment, selStart, selEnd, info);
michael@0 446 }
michael@0 447
michael@0 448 if (incrementedValue === null) {
michael@0 449 return;
michael@0 450 }
michael@0 451
michael@0 452 let preRawValue = value.substr(0, range.start);
michael@0 453 let postRawValue = value.substr(range.end);
michael@0 454
michael@0 455 return {
michael@0 456 value: preRawValue + incrementedValue + postRawValue,
michael@0 457 start: range.start + selection[0],
michael@0 458 end: range.start + selection[1]
michael@0 459 };
michael@0 460 },
michael@0 461
michael@0 462 /**
michael@0 463 * Parses the property value and type.
michael@0 464 *
michael@0 465 * @param {string} value
michael@0 466 * Property value.
michael@0 467 * @param {number} offset
michael@0 468 * Starting index of value.
michael@0 469 * @return {object} object with properties 'value', 'start', 'end', and 'type'.
michael@0 470 */
michael@0 471 _parseCSSValue: function InplaceEditor_parseCSSValue(value, offset)
michael@0 472 {
michael@0 473 const reSplitCSS = /(url\("?[^"\)]+"?\)?)|(rgba?\([^)]*\)?)|(hsla?\([^)]*\)?)|(#[\dA-Fa-f]+)|(-?\d+(\.\d+)?(%|[a-z]{1,4})?)|"([^"]*)"?|'([^']*)'?|([^,\s\/!\(\)]+)|(!(.*)?)/;
michael@0 474 let start = 0;
michael@0 475 let m;
michael@0 476
michael@0 477 // retreive values from left to right until we find the one at our offset
michael@0 478 while ((m = reSplitCSS.exec(value)) &&
michael@0 479 (m.index + m[0].length < offset)) {
michael@0 480 value = value.substr(m.index + m[0].length);
michael@0 481 start += m.index + m[0].length;
michael@0 482 offset -= m.index + m[0].length;
michael@0 483 }
michael@0 484
michael@0 485 if (!m) {
michael@0 486 return;
michael@0 487 }
michael@0 488
michael@0 489 let type;
michael@0 490 if (m[1]) {
michael@0 491 type = "url";
michael@0 492 } else if (m[2]) {
michael@0 493 type = "rgb";
michael@0 494 } else if (m[3]) {
michael@0 495 type = "hsl";
michael@0 496 } else if (m[4]) {
michael@0 497 type = "hex";
michael@0 498 } else if (m[5]) {
michael@0 499 type = "num";
michael@0 500 }
michael@0 501
michael@0 502 return {
michael@0 503 value: m[0],
michael@0 504 start: start + m.index,
michael@0 505 end: start + m.index + m[0].length,
michael@0 506 type: type
michael@0 507 };
michael@0 508 },
michael@0 509
michael@0 510 /**
michael@0 511 * Increment the property value for types other than
michael@0 512 * number or hex, such as rgb, hsl, and file names.
michael@0 513 *
michael@0 514 * @param {string} value
michael@0 515 * Property value.
michael@0 516 * @param {number} increment
michael@0 517 * Amount to increment/decrement.
michael@0 518 * @param {number} offset
michael@0 519 * Starting index of the property value.
michael@0 520 * @param {number} offsetEnd
michael@0 521 * Ending index of the property value.
michael@0 522 * @param {object} info
michael@0 523 * Object with details about the property value.
michael@0 524 * @return {object} object with properties 'value', 'start', and 'end'.
michael@0 525 */
michael@0 526 _incrementGenericValue:
michael@0 527 function InplaceEditor_incrementGenericValue(value, increment, offset,
michael@0 528 offsetEnd, info)
michael@0 529 {
michael@0 530 // Try to find a number around the cursor to increment.
michael@0 531 let start, end;
michael@0 532 // Check if we are incrementing in a non-number context (such as a URL)
michael@0 533 if (/^-?[0-9.]/.test(value.substring(offset, offsetEnd)) &&
michael@0 534 !(/\d/.test(value.charAt(offset - 1) + value.charAt(offsetEnd)))) {
michael@0 535 // We have a number selected, possibly with a suffix, and we are not in
michael@0 536 // the disallowed case of just part of a known number being selected.
michael@0 537 // Use that number.
michael@0 538 start = offset;
michael@0 539 end = offsetEnd;
michael@0 540 } else {
michael@0 541 // Parse periods as belonging to the number only if we are in a known number
michael@0 542 // context. (This makes incrementing the 1 in 'image1.gif' work.)
michael@0 543 let pattern = "[" + (info ? "0-9." : "0-9") + "]*";
michael@0 544 let before = new RegExp(pattern + "$").exec(value.substr(0, offset))[0].length;
michael@0 545 let after = new RegExp("^" + pattern).exec(value.substr(offset))[0].length;
michael@0 546
michael@0 547 start = offset - before;
michael@0 548 end = offset + after;
michael@0 549
michael@0 550 // Expand the number to contain an initial minus sign if it seems
michael@0 551 // free-standing.
michael@0 552 if (value.charAt(start - 1) === "-" &&
michael@0 553 (start - 1 === 0 || /[ (:,='"]/.test(value.charAt(start - 2)))) {
michael@0 554 --start;
michael@0 555 }
michael@0 556 }
michael@0 557
michael@0 558 if (start !== end)
michael@0 559 {
michael@0 560 // Include percentages as part of the incremented number (they are
michael@0 561 // common enough).
michael@0 562 if (value.charAt(end) === "%") {
michael@0 563 ++end;
michael@0 564 }
michael@0 565
michael@0 566 let first = value.substr(0, start);
michael@0 567 let mid = value.substring(start, end);
michael@0 568 let last = value.substr(end);
michael@0 569
michael@0 570 mid = this._incrementRawValue(mid, increment, info);
michael@0 571
michael@0 572 if (mid !== null) {
michael@0 573 return {
michael@0 574 value: first + mid + last,
michael@0 575 start: start,
michael@0 576 end: start + mid.length
michael@0 577 };
michael@0 578 }
michael@0 579 }
michael@0 580 },
michael@0 581
michael@0 582 /**
michael@0 583 * Increment the property value for numbers.
michael@0 584 *
michael@0 585 * @param {string} rawValue
michael@0 586 * Raw value to increment.
michael@0 587 * @param {number} increment
michael@0 588 * Amount to increase/decrease the raw value.
michael@0 589 * @param {object} info
michael@0 590 * Object with info about the property value.
michael@0 591 * @return {string} the incremented value.
michael@0 592 */
michael@0 593 _incrementRawValue:
michael@0 594 function InplaceEditor_incrementRawValue(rawValue, increment, info)
michael@0 595 {
michael@0 596 let num = parseFloat(rawValue);
michael@0 597
michael@0 598 if (isNaN(num)) {
michael@0 599 return null;
michael@0 600 }
michael@0 601
michael@0 602 let number = /\d+(\.\d+)?/.exec(rawValue);
michael@0 603 let units = rawValue.substr(number.index + number[0].length);
michael@0 604
michael@0 605 // avoid rounding errors
michael@0 606 let newValue = Math.round((num + increment) * 1000) / 1000;
michael@0 607
michael@0 608 if (info && "minValue" in info) {
michael@0 609 newValue = Math.max(newValue, info.minValue);
michael@0 610 }
michael@0 611 if (info && "maxValue" in info) {
michael@0 612 newValue = Math.min(newValue, info.maxValue);
michael@0 613 }
michael@0 614
michael@0 615 newValue = newValue.toString();
michael@0 616
michael@0 617 return newValue + units;
michael@0 618 },
michael@0 619
michael@0 620 /**
michael@0 621 * Increment the property value for hex.
michael@0 622 *
michael@0 623 * @param {string} value
michael@0 624 * Property value.
michael@0 625 * @param {number} increment
michael@0 626 * Amount to increase/decrease the property value.
michael@0 627 * @param {number} offset
michael@0 628 * Starting index of the property value.
michael@0 629 * @param {number} offsetEnd
michael@0 630 * Ending index of the property value.
michael@0 631 * @return {object} object with properties 'value' and 'selection'.
michael@0 632 */
michael@0 633 _incHexColor:
michael@0 634 function InplaceEditor_incHexColor(rawValue, increment, offset, offsetEnd)
michael@0 635 {
michael@0 636 // Return early if no part of the rawValue is selected.
michael@0 637 if (offsetEnd > rawValue.length && offset >= rawValue.length) {
michael@0 638 return;
michael@0 639 }
michael@0 640 if (offset < 1 && offsetEnd <= 1) {
michael@0 641 return;
michael@0 642 }
michael@0 643 // Ignore the leading #.
michael@0 644 rawValue = rawValue.substr(1);
michael@0 645 --offset;
michael@0 646 --offsetEnd;
michael@0 647
michael@0 648 // Clamp the selection to within the actual value.
michael@0 649 offset = Math.max(offset, 0);
michael@0 650 offsetEnd = Math.min(offsetEnd, rawValue.length);
michael@0 651 offsetEnd = Math.max(offsetEnd, offset);
michael@0 652
michael@0 653 // Normalize #ABC -> #AABBCC.
michael@0 654 if (rawValue.length === 3) {
michael@0 655 rawValue = rawValue.charAt(0) + rawValue.charAt(0) +
michael@0 656 rawValue.charAt(1) + rawValue.charAt(1) +
michael@0 657 rawValue.charAt(2) + rawValue.charAt(2);
michael@0 658 offset *= 2;
michael@0 659 offsetEnd *= 2;
michael@0 660 }
michael@0 661
michael@0 662 if (rawValue.length !== 6) {
michael@0 663 return;
michael@0 664 }
michael@0 665
michael@0 666 // If no selection, increment an adjacent color, preferably one to the left.
michael@0 667 if (offset === offsetEnd) {
michael@0 668 if (offset === 0) {
michael@0 669 offsetEnd = 1;
michael@0 670 } else {
michael@0 671 offset = offsetEnd - 1;
michael@0 672 }
michael@0 673 }
michael@0 674
michael@0 675 // Make the selection cover entire parts.
michael@0 676 offset -= offset % 2;
michael@0 677 offsetEnd += offsetEnd % 2;
michael@0 678
michael@0 679 // Remap the increments from [0.1, 1, 10] to [1, 1, 16].
michael@0 680 if (-1 < increment && increment < 1) {
michael@0 681 increment = (increment < 0 ? -1 : 1);
michael@0 682 }
michael@0 683 if (Math.abs(increment) === 10) {
michael@0 684 increment = (increment < 0 ? -16 : 16);
michael@0 685 }
michael@0 686
michael@0 687 let isUpper = (rawValue.toUpperCase() === rawValue);
michael@0 688
michael@0 689 for (let pos = offset; pos < offsetEnd; pos += 2) {
michael@0 690 // Increment the part in [pos, pos+2).
michael@0 691 let mid = rawValue.substr(pos, 2);
michael@0 692 let value = parseInt(mid, 16);
michael@0 693
michael@0 694 if (isNaN(value)) {
michael@0 695 return;
michael@0 696 }
michael@0 697
michael@0 698 mid = Math.min(Math.max(value + increment, 0), 255).toString(16);
michael@0 699
michael@0 700 while (mid.length < 2) {
michael@0 701 mid = "0" + mid;
michael@0 702 }
michael@0 703 if (isUpper) {
michael@0 704 mid = mid.toUpperCase();
michael@0 705 }
michael@0 706
michael@0 707 rawValue = rawValue.substr(0, pos) + mid + rawValue.substr(pos + 2);
michael@0 708 }
michael@0 709
michael@0 710 return {
michael@0 711 value: "#" + rawValue,
michael@0 712 selection: [offset + 1, offsetEnd + 1]
michael@0 713 };
michael@0 714 },
michael@0 715
michael@0 716 /**
michael@0 717 * Cycle through the autocompletion suggestions in the popup.
michael@0 718 *
michael@0 719 * @param {boolean} aReverse
michael@0 720 * true to select previous item from the popup.
michael@0 721 * @param {boolean} aNoSelect
michael@0 722 * true to not select the text after selecting the newly selectedItem
michael@0 723 * from the popup.
michael@0 724 */
michael@0 725 _cycleCSSSuggestion:
michael@0 726 function InplaceEditor_cycleCSSSuggestion(aReverse, aNoSelect)
michael@0 727 {
michael@0 728 // selectedItem can be null when nothing is selected in an empty editor.
michael@0 729 let {label, preLabel} = this.popup.selectedItem || {label: "", preLabel: ""};
michael@0 730 if (aReverse) {
michael@0 731 this.popup.selectPreviousItem();
michael@0 732 } else {
michael@0 733 this.popup.selectNextItem();
michael@0 734 }
michael@0 735 this._selectedIndex = this.popup.selectedIndex;
michael@0 736 let input = this.input;
michael@0 737 let pre = "";
michael@0 738 if (input.selectionStart < input.selectionEnd) {
michael@0 739 pre = input.value.slice(0, input.selectionStart);
michael@0 740 }
michael@0 741 else {
michael@0 742 pre = input.value.slice(0, input.selectionStart - label.length +
michael@0 743 preLabel.length);
michael@0 744 }
michael@0 745 let post = input.value.slice(input.selectionEnd, input.value.length);
michael@0 746 let item = this.popup.selectedItem;
michael@0 747 let toComplete = item.label.slice(item.preLabel.length);
michael@0 748 input.value = pre + toComplete + post;
michael@0 749 if (!aNoSelect) {
michael@0 750 input.setSelectionRange(pre.length, pre.length + toComplete.length);
michael@0 751 }
michael@0 752 else {
michael@0 753 input.setSelectionRange(pre.length + toComplete.length,
michael@0 754 pre.length + toComplete.length);
michael@0 755 }
michael@0 756 this._updateSize();
michael@0 757 // This emit is mainly for the purpose of making the test flow simpler.
michael@0 758 this.emit("after-suggest");
michael@0 759 },
michael@0 760
michael@0 761 /**
michael@0 762 * Call the client's done handler and clear out.
michael@0 763 */
michael@0 764 _apply: function InplaceEditor_apply(aEvent)
michael@0 765 {
michael@0 766 if (this._applied) {
michael@0 767 return;
michael@0 768 }
michael@0 769
michael@0 770 this._applied = true;
michael@0 771
michael@0 772 if (this.done) {
michael@0 773 let val = this.input.value.trim();
michael@0 774 return this.done(this.cancelled ? this.initial : val, !this.cancelled);
michael@0 775 }
michael@0 776
michael@0 777 return null;
michael@0 778 },
michael@0 779
michael@0 780 /**
michael@0 781 * Handle loss of focus by calling done if it hasn't been called yet.
michael@0 782 */
michael@0 783 _onBlur: function InplaceEditor_onBlur(aEvent, aDoNotClear)
michael@0 784 {
michael@0 785 if (aEvent && this.popup && this.popup.isOpen &&
michael@0 786 this.popup.selectedIndex >= 0) {
michael@0 787 let label, preLabel;
michael@0 788 if (this._selectedIndex === undefined) {
michael@0 789 ({label, preLabel}) = this.popup.getItemAtIndex(this.popup.selectedIndex);
michael@0 790 }
michael@0 791 else {
michael@0 792 ({label, preLabel}) = this.popup.getItemAtIndex(this._selectedIndex);
michael@0 793 }
michael@0 794 let input = this.input;
michael@0 795 let pre = "";
michael@0 796 if (input.selectionStart < input.selectionEnd) {
michael@0 797 pre = input.value.slice(0, input.selectionStart);
michael@0 798 }
michael@0 799 else {
michael@0 800 pre = input.value.slice(0, input.selectionStart - label.length +
michael@0 801 preLabel.length);
michael@0 802 }
michael@0 803 let post = input.value.slice(input.selectionEnd, input.value.length);
michael@0 804 let item = this.popup.selectedItem;
michael@0 805 this._selectedIndex = this.popup.selectedIndex;
michael@0 806 let toComplete = item.label.slice(item.preLabel.length);
michael@0 807 input.value = pre + toComplete + post;
michael@0 808 input.setSelectionRange(pre.length + toComplete.length,
michael@0 809 pre.length + toComplete.length);
michael@0 810 this._updateSize();
michael@0 811 // Wait for the popup to hide and then focus input async otherwise it does
michael@0 812 // not work.
michael@0 813 let onPopupHidden = () => {
michael@0 814 this.popup._panel.removeEventListener("popuphidden", onPopupHidden);
michael@0 815 this.doc.defaultView.setTimeout(()=> {
michael@0 816 input.focus();
michael@0 817 this.emit("after-suggest");
michael@0 818 }, 0);
michael@0 819 };
michael@0 820 this.popup._panel.addEventListener("popuphidden", onPopupHidden);
michael@0 821 this.popup.hidePopup();
michael@0 822 // Content type other than CSS_MIXED is used in rule-view where the values
michael@0 823 // are live previewed. So we apply the value before returning.
michael@0 824 if (this.contentType != CONTENT_TYPES.CSS_MIXED) {
michael@0 825 this._apply();
michael@0 826 }
michael@0 827 return;
michael@0 828 }
michael@0 829 this._apply();
michael@0 830 if (!aDoNotClear) {
michael@0 831 this._clear();
michael@0 832 }
michael@0 833 },
michael@0 834
michael@0 835 /**
michael@0 836 * Handle the input field's keypress event.
michael@0 837 */
michael@0 838 _onKeyPress: function InplaceEditor_onKeyPress(aEvent)
michael@0 839 {
michael@0 840 let prevent = false;
michael@0 841
michael@0 842 const largeIncrement = 100;
michael@0 843 const mediumIncrement = 10;
michael@0 844 const smallIncrement = 0.1;
michael@0 845
michael@0 846 let increment = 0;
michael@0 847
michael@0 848 if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_UP
michael@0 849 || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP) {
michael@0 850 increment = 1;
michael@0 851 } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DOWN
michael@0 852 || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
michael@0 853 increment = -1;
michael@0 854 }
michael@0 855
michael@0 856 if (aEvent.shiftKey && !aEvent.altKey) {
michael@0 857 if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_UP
michael@0 858 || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_PAGE_DOWN) {
michael@0 859 increment *= largeIncrement;
michael@0 860 } else {
michael@0 861 increment *= mediumIncrement;
michael@0 862 }
michael@0 863 } else if (aEvent.altKey && !aEvent.shiftKey) {
michael@0 864 increment *= smallIncrement;
michael@0 865 }
michael@0 866
michael@0 867 let cycling = false;
michael@0 868 if (increment && this._incrementValue(increment) ) {
michael@0 869 this._updateSize();
michael@0 870 prevent = true;
michael@0 871 cycling = true;
michael@0 872 } else if (increment && this.popup && this.popup.isOpen) {
michael@0 873 cycling = true;
michael@0 874 prevent = true;
michael@0 875 this._cycleCSSSuggestion(increment > 0);
michael@0 876 this._doValidation();
michael@0 877 }
michael@0 878
michael@0 879 if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_BACK_SPACE ||
michael@0 880 aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DELETE ||
michael@0 881 aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_LEFT ||
michael@0 882 aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RIGHT) {
michael@0 883 if (this.popup && this.popup.isOpen) {
michael@0 884 this.popup.hidePopup();
michael@0 885 }
michael@0 886 } else if (!cycling && !aEvent.metaKey && !aEvent.altKey && !aEvent.ctrlKey) {
michael@0 887 this._maybeSuggestCompletion();
michael@0 888 }
michael@0 889
michael@0 890 if (this.multiline &&
michael@0 891 aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN &&
michael@0 892 aEvent.shiftKey) {
michael@0 893 prevent = false;
michael@0 894 } else if (aEvent.charCode in this._advanceCharCodes
michael@0 895 || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN
michael@0 896 || aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB) {
michael@0 897 prevent = true;
michael@0 898
michael@0 899 let direction = FOCUS_FORWARD;
michael@0 900 if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB &&
michael@0 901 aEvent.shiftKey) {
michael@0 902 direction = FOCUS_BACKWARD;
michael@0 903 }
michael@0 904 if (this.stopOnReturn && aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_RETURN) {
michael@0 905 direction = null;
michael@0 906 }
michael@0 907
michael@0 908 // Now we don't want to suggest anything as we are moving out.
michael@0 909 this._preventSuggestions = true;
michael@0 910 // But we still want to show suggestions for css values. i.e. moving out
michael@0 911 // of css property input box in forward direction
michael@0 912 if (this.contentType == CONTENT_TYPES.CSS_PROPERTY &&
michael@0 913 direction == FOCUS_FORWARD) {
michael@0 914 this._preventSuggestions = false;
michael@0 915 }
michael@0 916
michael@0 917 let input = this.input;
michael@0 918
michael@0 919 if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_TAB &&
michael@0 920 this.contentType == CONTENT_TYPES.CSS_MIXED) {
michael@0 921 if (this.popup && input.selectionStart < input.selectionEnd) {
michael@0 922 aEvent.preventDefault();
michael@0 923 input.setSelectionRange(input.selectionEnd, input.selectionEnd);
michael@0 924 this.emit("after-suggest");
michael@0 925 return;
michael@0 926 }
michael@0 927 else if (this.popup && this.popup.isOpen) {
michael@0 928 aEvent.preventDefault();
michael@0 929 this._cycleCSSSuggestion(aEvent.shiftKey, true);
michael@0 930 return;
michael@0 931 }
michael@0 932 }
michael@0 933
michael@0 934 this._apply();
michael@0 935
michael@0 936 // Close the popup if open
michael@0 937 if (this.popup && this.popup.isOpen) {
michael@0 938 this.popup.hidePopup();
michael@0 939 }
michael@0 940
michael@0 941 if (direction !== null && focusManager.focusedElement === input) {
michael@0 942 // If the focused element wasn't changed by the done callback,
michael@0 943 // move the focus as requested.
michael@0 944 let next = moveFocus(this.doc.defaultView, direction);
michael@0 945
michael@0 946 // If the next node to be focused has been tagged as an editable
michael@0 947 // node, send it a click event to trigger
michael@0 948 if (next && next.ownerDocument === this.doc && next._editable) {
michael@0 949 next.click();
michael@0 950 }
michael@0 951 }
michael@0 952
michael@0 953 this._clear();
michael@0 954 } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_ESCAPE) {
michael@0 955 // Cancel and blur ourselves.
michael@0 956 // Now we don't want to suggest anything as we are moving out.
michael@0 957 this._preventSuggestions = true;
michael@0 958 // Close the popup if open
michael@0 959 if (this.popup && this.popup.isOpen) {
michael@0 960 this.popup.hidePopup();
michael@0 961 }
michael@0 962 prevent = true;
michael@0 963 this.cancelled = true;
michael@0 964 this._apply();
michael@0 965 this._clear();
michael@0 966 aEvent.stopPropagation();
michael@0 967 } else if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_SPACE) {
michael@0 968 // No need for leading spaces here. This is particularly
michael@0 969 // noticable when adding a property: it's very natural to type
michael@0 970 // <name>: (which advances to the next property) then spacebar.
michael@0 971 prevent = !this.input.value;
michael@0 972 }
michael@0 973
michael@0 974 if (prevent) {
michael@0 975 aEvent.preventDefault();
michael@0 976 }
michael@0 977 },
michael@0 978
michael@0 979 /**
michael@0 980 * Handle the input field's keyup event.
michael@0 981 */
michael@0 982 _onKeyup: function(aEvent) {
michael@0 983 this._applied = false;
michael@0 984 },
michael@0 985
michael@0 986 /**
michael@0 987 * Handle changes to the input text.
michael@0 988 */
michael@0 989 _onInput: function InplaceEditor_onInput(aEvent)
michael@0 990 {
michael@0 991 // Validate the entered value.
michael@0 992 this._doValidation();
michael@0 993
michael@0 994 // Update size if we're autosizing.
michael@0 995 if (this._measurement) {
michael@0 996 this._updateSize();
michael@0 997 }
michael@0 998
michael@0 999 // Call the user's change handler if available.
michael@0 1000 if (this.change) {
michael@0 1001 this.change(this.input.value.trim());
michael@0 1002 }
michael@0 1003 },
michael@0 1004
michael@0 1005 /**
michael@0 1006 * Fire validation callback with current input
michael@0 1007 */
michael@0 1008 _doValidation: function()
michael@0 1009 {
michael@0 1010 if (this.validate && this.input) {
michael@0 1011 this.validate(this.input.value);
michael@0 1012 }
michael@0 1013 },
michael@0 1014
michael@0 1015 /**
michael@0 1016 * Handles displaying suggestions based on the current input.
michael@0 1017 *
michael@0 1018 * @param {boolean} aNoAutoInsert
michael@0 1019 * true if you don't want to automatically insert the first suggestion
michael@0 1020 */
michael@0 1021 _maybeSuggestCompletion: function(aNoAutoInsert) {
michael@0 1022 // Input can be null in cases when you intantaneously switch out of it.
michael@0 1023 if (!this.input) {
michael@0 1024 return;
michael@0 1025 }
michael@0 1026 let preTimeoutQuery = this.input.value;
michael@0 1027 // Since we are calling this method from a keypress event handler, the
michael@0 1028 // |input.value| does not include currently typed character. Thus we perform
michael@0 1029 // this method async.
michael@0 1030 this.doc.defaultView.setTimeout(() => {
michael@0 1031 if (this._preventSuggestions) {
michael@0 1032 this._preventSuggestions = false;
michael@0 1033 return;
michael@0 1034 }
michael@0 1035 if (this.contentType == CONTENT_TYPES.PLAIN_TEXT) {
michael@0 1036 return;
michael@0 1037 }
michael@0 1038 if (!this.input) {
michael@0 1039 return;
michael@0 1040 }
michael@0 1041 let input = this.input;
michael@0 1042 // The length of input.value should be increased by 1
michael@0 1043 if (input.value.length - preTimeoutQuery.length > 1) {
michael@0 1044 return;
michael@0 1045 }
michael@0 1046 let query = input.value.slice(0, input.selectionStart);
michael@0 1047 let startCheckQuery = query;
michael@0 1048 if (query == null) {
michael@0 1049 return;
michael@0 1050 }
michael@0 1051 // If nothing is selected and there is a non-space character after the
michael@0 1052 // cursor, do not autocomplete.
michael@0 1053 if (input.selectionStart == input.selectionEnd &&
michael@0 1054 input.selectionStart < input.value.length &&
michael@0 1055 input.value.slice(input.selectionStart)[0] != " ") {
michael@0 1056 // This emit is mainly to make the test flow simpler.
michael@0 1057 this.emit("after-suggest", "nothing to autocomplete");
michael@0 1058 return;
michael@0 1059 }
michael@0 1060 let list = [];
michael@0 1061 if (this.contentType == CONTENT_TYPES.CSS_PROPERTY) {
michael@0 1062 list = CSSPropertyList;
michael@0 1063 } else if (this.contentType == CONTENT_TYPES.CSS_VALUE) {
michael@0 1064 // Get the last query to be completed before the caret.
michael@0 1065 let match = /([^\s,.\/]+$)/.exec(query);
michael@0 1066 if (match) {
michael@0 1067 startCheckQuery = match[0];
michael@0 1068 } else {
michael@0 1069 startCheckQuery = "";
michael@0 1070 }
michael@0 1071
michael@0 1072 list =
michael@0 1073 ["!important", ...domUtils.getCSSValuesForProperty(this.property.name)];
michael@0 1074
michael@0 1075 if (query == "") {
michael@0 1076 // Do not suggest '!important' without any manually typed character.
michael@0 1077 list.splice(0, 1);
michael@0 1078 }
michael@0 1079 } else if (this.contentType == CONTENT_TYPES.CSS_MIXED &&
michael@0 1080 /^\s*style\s*=/.test(query)) {
michael@0 1081 // Detecting if cursor is at property or value;
michael@0 1082 let match = query.match(/([:;"'=]?)\s*([^"';:=]+)?$/);
michael@0 1083 if (match && match.length >= 2) {
michael@0 1084 if (match[1] == ":") { // We are in CSS value completion
michael@0 1085 let propertyName =
michael@0 1086 query.match(/[;"'=]\s*([^"';:= ]+)\s*:\s*[^"';:=]*$/)[1];
michael@0 1087 list =
michael@0 1088 ["!important;", ...domUtils.getCSSValuesForProperty(propertyName)];
michael@0 1089 let matchLastQuery = /([^\s,.\/]+$)/.exec(match[2] || "");
michael@0 1090 if (matchLastQuery) {
michael@0 1091 startCheckQuery = matchLastQuery[0];
michael@0 1092 } else {
michael@0 1093 startCheckQuery = "";
michael@0 1094 }
michael@0 1095 if (!match[2]) {
michael@0 1096 // Don't suggest '!important' without any manually typed character
michael@0 1097 list.splice(0, 1);
michael@0 1098 }
michael@0 1099 } else if (match[1]) { // We are in CSS property name completion
michael@0 1100 list = CSSPropertyList;
michael@0 1101 startCheckQuery = match[2];
michael@0 1102 }
michael@0 1103 if (startCheckQuery == null) {
michael@0 1104 // This emit is mainly to make the test flow simpler.
michael@0 1105 this.emit("after-suggest", "nothing to autocomplete");
michael@0 1106 return;
michael@0 1107 }
michael@0 1108 }
michael@0 1109 }
michael@0 1110 if (!aNoAutoInsert) {
michael@0 1111 list.some(item => {
michael@0 1112 if (startCheckQuery != null && item.startsWith(startCheckQuery)) {
michael@0 1113 input.value = query + item.slice(startCheckQuery.length) +
michael@0 1114 input.value.slice(query.length);
michael@0 1115 input.setSelectionRange(query.length, query.length + item.length -
michael@0 1116 startCheckQuery.length);
michael@0 1117 this._updateSize();
michael@0 1118 return true;
michael@0 1119 }
michael@0 1120 });
michael@0 1121 }
michael@0 1122
michael@0 1123 if (!this.popup) {
michael@0 1124 // This emit is mainly to make the test flow simpler.
michael@0 1125 this.emit("after-suggest", "no popup");
michael@0 1126 return;
michael@0 1127 }
michael@0 1128 let finalList = [];
michael@0 1129 let length = list.length;
michael@0 1130 for (let i = 0, count = 0; i < length && count < MAX_POPUP_ENTRIES; i++) {
michael@0 1131 if (startCheckQuery != null && list[i].startsWith(startCheckQuery)) {
michael@0 1132 count++;
michael@0 1133 finalList.push({
michael@0 1134 preLabel: startCheckQuery,
michael@0 1135 label: list[i]
michael@0 1136 });
michael@0 1137 }
michael@0 1138 else if (count > 0) {
michael@0 1139 // Since count was incremented, we had already crossed the entries
michael@0 1140 // which would have started with query, assuming that list is sorted.
michael@0 1141 break;
michael@0 1142 }
michael@0 1143 else if (startCheckQuery != null && list[i][0] > startCheckQuery[0]) {
michael@0 1144 // We have crossed all possible matches alphabetically.
michael@0 1145 break;
michael@0 1146 }
michael@0 1147 }
michael@0 1148
michael@0 1149 if (finalList.length > 1) {
michael@0 1150 // Calculate the offset for the popup to be opened.
michael@0 1151 let x = (this.input.selectionStart - startCheckQuery.length) *
michael@0 1152 this.inputCharWidth;
michael@0 1153 this.popup.setItems(finalList);
michael@0 1154 this.popup.openPopup(this.input, x);
michael@0 1155 if (aNoAutoInsert) {
michael@0 1156 this.popup.selectedIndex = -1;
michael@0 1157 }
michael@0 1158 } else {
michael@0 1159 this.popup.hidePopup();
michael@0 1160 }
michael@0 1161 // This emit is mainly for the purpose of making the test flow simpler.
michael@0 1162 this.emit("after-suggest");
michael@0 1163 this._doValidation();
michael@0 1164 }, 0);
michael@0 1165 }
michael@0 1166 };
michael@0 1167
michael@0 1168 /**
michael@0 1169 * Copy text-related styles from one element to another.
michael@0 1170 */
michael@0 1171 function copyTextStyles(aFrom, aTo)
michael@0 1172 {
michael@0 1173 let win = aFrom.ownerDocument.defaultView;
michael@0 1174 let style = win.getComputedStyle(aFrom);
michael@0 1175 aTo.style.fontFamily = style.getPropertyCSSValue("font-family").cssText;
michael@0 1176 aTo.style.fontSize = style.getPropertyCSSValue("font-size").cssText;
michael@0 1177 aTo.style.fontWeight = style.getPropertyCSSValue("font-weight").cssText;
michael@0 1178 aTo.style.fontStyle = style.getPropertyCSSValue("font-style").cssText;
michael@0 1179 }
michael@0 1180
michael@0 1181 /**
michael@0 1182 * Trigger a focus change similar to pressing tab/shift-tab.
michael@0 1183 */
michael@0 1184 function moveFocus(aWin, aDirection)
michael@0 1185 {
michael@0 1186 return focusManager.moveFocus(aWin, null, aDirection, 0);
michael@0 1187 }
michael@0 1188
michael@0 1189
michael@0 1190 XPCOMUtils.defineLazyGetter(this, "focusManager", function() {
michael@0 1191 return Services.focus;
michael@0 1192 });
michael@0 1193
michael@0 1194 XPCOMUtils.defineLazyGetter(this, "CSSPropertyList", function() {
michael@0 1195 return domUtils.getCSSPropertyNames(domUtils.INCLUDE_ALIASES).sort();
michael@0 1196 });
michael@0 1197
michael@0 1198 XPCOMUtils.defineLazyGetter(this, "domUtils", function() {
michael@0 1199 return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
michael@0 1200 });

mercurial