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 +});