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