|
1 /* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 const {Cc, Ci, Cu} = require("chrome"); |
|
8 |
|
9 const ToolDefinitions = require("main").Tools; |
|
10 const {CssLogic} = require("devtools/styleinspector/css-logic"); |
|
11 const {ELEMENT_STYLE} = require("devtools/server/actors/styles"); |
|
12 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); |
|
13 const {EventEmitter} = require("devtools/toolkit/event-emitter"); |
|
14 const {OutputParser} = require("devtools/output-parser"); |
|
15 const {Tooltip} = require("devtools/shared/widgets/Tooltip"); |
|
16 const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/styleeditor/utils"); |
|
17 const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); |
|
18 |
|
19 Cu.import("resource://gre/modules/Services.jsm"); |
|
20 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
21 Cu.import("resource://gre/modules/devtools/Templater.jsm"); |
|
22 |
|
23 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", |
|
24 "resource://gre/modules/PluralForm.jsm"); |
|
25 |
|
26 const FILTER_CHANGED_TIMEOUT = 300; |
|
27 const HTML_NS = "http://www.w3.org/1999/xhtml"; |
|
28 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; |
|
29 |
|
30 /** |
|
31 * Helper for long-running processes that should yield occasionally to |
|
32 * the mainloop. |
|
33 * |
|
34 * @param {Window} aWin |
|
35 * Timeouts will be set on this window when appropriate. |
|
36 * @param {Generator} aGenerator |
|
37 * Will iterate this generator. |
|
38 * @param {object} aOptions |
|
39 * Options for the update process: |
|
40 * onItem {function} Will be called with the value of each iteration. |
|
41 * onBatch {function} Will be called after each batch of iterations, |
|
42 * before yielding to the main loop. |
|
43 * onDone {function} Will be called when iteration is complete. |
|
44 * onCancel {function} Will be called if the process is canceled. |
|
45 * threshold {int} How long to process before yielding, in ms. |
|
46 * |
|
47 * @constructor |
|
48 */ |
|
49 function UpdateProcess(aWin, aGenerator, aOptions) |
|
50 { |
|
51 this.win = aWin; |
|
52 this.iter = _Iterator(aGenerator); |
|
53 this.onItem = aOptions.onItem || function() {}; |
|
54 this.onBatch = aOptions.onBatch || function () {}; |
|
55 this.onDone = aOptions.onDone || function() {}; |
|
56 this.onCancel = aOptions.onCancel || function() {}; |
|
57 this.threshold = aOptions.threshold || 45; |
|
58 |
|
59 this.canceled = false; |
|
60 } |
|
61 |
|
62 UpdateProcess.prototype = { |
|
63 /** |
|
64 * Schedule a new batch on the main loop. |
|
65 */ |
|
66 schedule: function UP_schedule() |
|
67 { |
|
68 if (this.canceled) { |
|
69 return; |
|
70 } |
|
71 this._timeout = this.win.setTimeout(this._timeoutHandler.bind(this), 0); |
|
72 }, |
|
73 |
|
74 /** |
|
75 * Cancel the running process. onItem will not be called again, |
|
76 * and onCancel will be called. |
|
77 */ |
|
78 cancel: function UP_cancel() |
|
79 { |
|
80 if (this._timeout) { |
|
81 this.win.clearTimeout(this._timeout); |
|
82 this._timeout = 0; |
|
83 } |
|
84 this.canceled = true; |
|
85 this.onCancel(); |
|
86 }, |
|
87 |
|
88 _timeoutHandler: function UP_timeoutHandler() { |
|
89 this._timeout = null; |
|
90 try { |
|
91 this._runBatch(); |
|
92 this.schedule(); |
|
93 } catch(e) { |
|
94 if (e instanceof StopIteration) { |
|
95 this.onBatch(); |
|
96 this.onDone(); |
|
97 return; |
|
98 } |
|
99 console.error(e); |
|
100 throw e; |
|
101 } |
|
102 }, |
|
103 |
|
104 _runBatch: function Y_runBatch() |
|
105 { |
|
106 let time = Date.now(); |
|
107 while(!this.canceled) { |
|
108 // Continue until iter.next() throws... |
|
109 let next = this.iter.next(); |
|
110 this.onItem(next[1]); |
|
111 if ((Date.now() - time) > this.threshold) { |
|
112 this.onBatch(); |
|
113 return; |
|
114 } |
|
115 } |
|
116 } |
|
117 }; |
|
118 |
|
119 /** |
|
120 * CssHtmlTree is a panel that manages the display of a table sorted by style. |
|
121 * There should be one instance of CssHtmlTree per style display (of which there |
|
122 * will generally only be one). |
|
123 * |
|
124 * @params {StyleInspector} aStyleInspector The owner of this CssHtmlTree |
|
125 * @param {PageStyleFront} aPageStyle |
|
126 * Front for the page style actor that will be providing |
|
127 * the style information. |
|
128 * |
|
129 * @constructor |
|
130 */ |
|
131 function CssHtmlTree(aStyleInspector, aPageStyle) |
|
132 { |
|
133 this.styleWindow = aStyleInspector.window; |
|
134 this.styleDocument = aStyleInspector.window.document; |
|
135 this.styleInspector = aStyleInspector; |
|
136 this.pageStyle = aPageStyle; |
|
137 this.propertyViews = []; |
|
138 |
|
139 this._outputParser = new OutputParser(); |
|
140 |
|
141 let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]. |
|
142 getService(Ci.nsIXULChromeRegistry); |
|
143 this.getRTLAttr = chromeReg.isLocaleRTL("global") ? "rtl" : "ltr"; |
|
144 |
|
145 // Create bound methods. |
|
146 this.focusWindow = this.focusWindow.bind(this); |
|
147 this._onContextMenu = this._onContextMenu.bind(this); |
|
148 this._contextMenuUpdate = this._contextMenuUpdate.bind(this); |
|
149 this._onSelectAll = this._onSelectAll.bind(this); |
|
150 this._onClick = this._onClick.bind(this); |
|
151 this._onCopy = this._onCopy.bind(this); |
|
152 |
|
153 this.styleDocument.addEventListener("copy", this._onCopy); |
|
154 this.styleDocument.addEventListener("mousedown", this.focusWindow); |
|
155 this.styleDocument.addEventListener("contextmenu", this._onContextMenu); |
|
156 |
|
157 // Nodes used in templating |
|
158 this.root = this.styleDocument.getElementById("root"); |
|
159 this.templateRoot = this.styleDocument.getElementById("templateRoot"); |
|
160 this.propertyContainer = this.styleDocument.getElementById("propertyContainer"); |
|
161 |
|
162 // Listen for click events |
|
163 this.propertyContainer.addEventListener("click", this._onClick, false); |
|
164 |
|
165 // No results text. |
|
166 this.noResults = this.styleDocument.getElementById("noResults"); |
|
167 |
|
168 // Refresh panel when color unit changed. |
|
169 this._handlePrefChange = this._handlePrefChange.bind(this); |
|
170 gDevTools.on("pref-changed", this._handlePrefChange); |
|
171 |
|
172 // Refresh panel when pref for showing original sources changes |
|
173 this._updateSourceLinks = this._updateSourceLinks.bind(this); |
|
174 this._prefObserver = new PrefObserver("devtools."); |
|
175 this._prefObserver.on(PREF_ORIG_SOURCES, this._updateSourceLinks); |
|
176 |
|
177 CssHtmlTree.processTemplate(this.templateRoot, this.root, this); |
|
178 |
|
179 // The element that we're inspecting, and the document that it comes from. |
|
180 this.viewedElement = null; |
|
181 |
|
182 // Properties preview tooltip |
|
183 this.tooltip = new Tooltip(this.styleInspector.inspector.panelDoc); |
|
184 this.tooltip.startTogglingOnHover(this.propertyContainer, |
|
185 this._onTooltipTargetHover.bind(this)); |
|
186 |
|
187 this._buildContextMenu(); |
|
188 this.createStyleViews(); |
|
189 } |
|
190 |
|
191 /** |
|
192 * Memoized lookup of a l10n string from a string bundle. |
|
193 * @param {string} aName The key to lookup. |
|
194 * @returns A localized version of the given key. |
|
195 */ |
|
196 CssHtmlTree.l10n = function CssHtmlTree_l10n(aName) |
|
197 { |
|
198 try { |
|
199 return CssHtmlTree._strings.GetStringFromName(aName); |
|
200 } catch (ex) { |
|
201 Services.console.logStringMessage("Error reading '" + aName + "'"); |
|
202 throw new Error("l10n error with " + aName); |
|
203 } |
|
204 }; |
|
205 |
|
206 /** |
|
207 * Clone the given template node, and process it by resolving ${} references |
|
208 * in the template. |
|
209 * |
|
210 * @param {nsIDOMElement} aTemplate the template note to use. |
|
211 * @param {nsIDOMElement} aDestination the destination node where the |
|
212 * processed nodes will be displayed. |
|
213 * @param {object} aData the data to pass to the template. |
|
214 * @param {Boolean} aPreserveDestination If true then the template will be |
|
215 * appended to aDestination's content else aDestination.innerHTML will be |
|
216 * cleared before the template is appended. |
|
217 */ |
|
218 CssHtmlTree.processTemplate = function CssHtmlTree_processTemplate(aTemplate, |
|
219 aDestination, aData, aPreserveDestination) |
|
220 { |
|
221 if (!aPreserveDestination) { |
|
222 aDestination.innerHTML = ""; |
|
223 } |
|
224 |
|
225 // All the templater does is to populate a given DOM tree with the given |
|
226 // values, so we need to clone the template first. |
|
227 let duplicated = aTemplate.cloneNode(true); |
|
228 |
|
229 // See https://github.com/mozilla/domtemplate/blob/master/README.md |
|
230 // for docs on the template() function |
|
231 template(duplicated, aData, { allowEval: true }); |
|
232 while (duplicated.firstChild) { |
|
233 aDestination.appendChild(duplicated.firstChild); |
|
234 } |
|
235 }; |
|
236 |
|
237 XPCOMUtils.defineLazyGetter(CssHtmlTree, "_strings", function() Services.strings |
|
238 .createBundle("chrome://global/locale/devtools/styleinspector.properties")); |
|
239 |
|
240 XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() { |
|
241 return Cc["@mozilla.org/widget/clipboardhelper;1"]. |
|
242 getService(Ci.nsIClipboardHelper); |
|
243 }); |
|
244 |
|
245 CssHtmlTree.prototype = { |
|
246 // Cache the list of properties that match the selected element. |
|
247 _matchedProperties: null, |
|
248 |
|
249 // Used for cancelling timeouts in the style filter. |
|
250 _filterChangedTimeout: null, |
|
251 |
|
252 // The search filter |
|
253 searchField: null, |
|
254 |
|
255 // Reference to the "Include browser styles" checkbox. |
|
256 includeBrowserStylesCheckbox: null, |
|
257 |
|
258 // Holds the ID of the panelRefresh timeout. |
|
259 _panelRefreshTimeout: null, |
|
260 |
|
261 // Toggle for zebra striping |
|
262 _darkStripe: true, |
|
263 |
|
264 // Number of visible properties |
|
265 numVisibleProperties: 0, |
|
266 |
|
267 setPageStyle: function(pageStyle) { |
|
268 this.pageStyle = pageStyle; |
|
269 }, |
|
270 |
|
271 get includeBrowserStyles() |
|
272 { |
|
273 return this.includeBrowserStylesCheckbox.checked; |
|
274 }, |
|
275 |
|
276 _handlePrefChange: function(event, data) { |
|
277 if (this._computed && (data.pref == "devtools.defaultColorUnit" || |
|
278 data.pref == PREF_ORIG_SOURCES)) { |
|
279 this.refreshPanel(); |
|
280 } |
|
281 }, |
|
282 |
|
283 /** |
|
284 * Update the highlighted element. The CssHtmlTree panel will show the style |
|
285 * information for the given element. |
|
286 * @param {nsIDOMElement} aElement The highlighted node to get styles for. |
|
287 * |
|
288 * @returns a promise that will be resolved when highlighting is complete. |
|
289 */ |
|
290 highlight: function(aElement) { |
|
291 if (!aElement) { |
|
292 this.viewedElement = null; |
|
293 this.noResults.hidden = false; |
|
294 |
|
295 if (this._refreshProcess) { |
|
296 this._refreshProcess.cancel(); |
|
297 } |
|
298 // Hiding all properties |
|
299 for (let propView of this.propertyViews) { |
|
300 propView.refresh(); |
|
301 } |
|
302 return promise.resolve(undefined); |
|
303 } |
|
304 |
|
305 this.tooltip.hide(); |
|
306 |
|
307 if (aElement === this.viewedElement) { |
|
308 return promise.resolve(undefined); |
|
309 } |
|
310 |
|
311 this.viewedElement = aElement; |
|
312 this.refreshSourceFilter(); |
|
313 |
|
314 return this.refreshPanel(); |
|
315 }, |
|
316 |
|
317 _createPropertyViews: function() |
|
318 { |
|
319 if (this._createViewsPromise) { |
|
320 return this._createViewsPromise; |
|
321 } |
|
322 |
|
323 let deferred = promise.defer(); |
|
324 this._createViewsPromise = deferred.promise; |
|
325 |
|
326 this.refreshSourceFilter(); |
|
327 this.numVisibleProperties = 0; |
|
328 let fragment = this.styleDocument.createDocumentFragment(); |
|
329 |
|
330 this._createViewsProcess = new UpdateProcess(this.styleWindow, CssHtmlTree.propertyNames, { |
|
331 onItem: (aPropertyName) => { |
|
332 // Per-item callback. |
|
333 let propView = new PropertyView(this, aPropertyName); |
|
334 fragment.appendChild(propView.buildMain()); |
|
335 fragment.appendChild(propView.buildSelectorContainer()); |
|
336 |
|
337 if (propView.visible) { |
|
338 this.numVisibleProperties++; |
|
339 } |
|
340 this.propertyViews.push(propView); |
|
341 }, |
|
342 onCancel: () => { |
|
343 deferred.reject("_createPropertyViews cancelled"); |
|
344 }, |
|
345 onDone: () => { |
|
346 // Completed callback. |
|
347 this.propertyContainer.appendChild(fragment); |
|
348 this.noResults.hidden = this.numVisibleProperties > 0; |
|
349 deferred.resolve(undefined); |
|
350 } |
|
351 }); |
|
352 |
|
353 this._createViewsProcess.schedule(); |
|
354 return deferred.promise; |
|
355 }, |
|
356 |
|
357 /** |
|
358 * Refresh the panel content. |
|
359 */ |
|
360 refreshPanel: function CssHtmlTree_refreshPanel() |
|
361 { |
|
362 if (!this.viewedElement) { |
|
363 return promise.resolve(); |
|
364 } |
|
365 |
|
366 return promise.all([ |
|
367 this._createPropertyViews(), |
|
368 this.pageStyle.getComputed(this.viewedElement, { |
|
369 filter: this._sourceFilter, |
|
370 onlyMatched: !this.includeBrowserStyles, |
|
371 markMatched: true |
|
372 }) |
|
373 ]).then(([createViews, computed]) => { |
|
374 this._matchedProperties = new Set; |
|
375 for (let name in computed) { |
|
376 if (computed[name].matched) { |
|
377 this._matchedProperties.add(name); |
|
378 } |
|
379 } |
|
380 this._computed = computed; |
|
381 |
|
382 if (this._refreshProcess) { |
|
383 this._refreshProcess.cancel(); |
|
384 } |
|
385 |
|
386 this.noResults.hidden = true; |
|
387 |
|
388 // Reset visible property count |
|
389 this.numVisibleProperties = 0; |
|
390 |
|
391 // Reset zebra striping. |
|
392 this._darkStripe = true; |
|
393 |
|
394 let deferred = promise.defer(); |
|
395 this._refreshProcess = new UpdateProcess(this.styleWindow, this.propertyViews, { |
|
396 onItem: (aPropView) => { |
|
397 aPropView.refresh(); |
|
398 }, |
|
399 onDone: () => { |
|
400 this._refreshProcess = null; |
|
401 this.noResults.hidden = this.numVisibleProperties > 0; |
|
402 this.styleInspector.inspector.emit("computed-view-refreshed"); |
|
403 deferred.resolve(undefined); |
|
404 } |
|
405 }); |
|
406 this._refreshProcess.schedule(); |
|
407 return deferred.promise; |
|
408 }).then(null, (err) => console.error(err)); |
|
409 }, |
|
410 |
|
411 /** |
|
412 * Called when the user enters a search term. |
|
413 * |
|
414 * @param {Event} aEvent the DOM Event object. |
|
415 */ |
|
416 filterChanged: function CssHtmlTree_filterChanged(aEvent) |
|
417 { |
|
418 let win = this.styleWindow; |
|
419 |
|
420 if (this._filterChangedTimeout) { |
|
421 win.clearTimeout(this._filterChangedTimeout); |
|
422 } |
|
423 |
|
424 this._filterChangedTimeout = win.setTimeout(function() { |
|
425 this.refreshPanel(); |
|
426 this._filterChangeTimeout = null; |
|
427 }.bind(this), FILTER_CHANGED_TIMEOUT); |
|
428 }, |
|
429 |
|
430 /** |
|
431 * The change event handler for the includeBrowserStyles checkbox. |
|
432 * |
|
433 * @param {Event} aEvent the DOM Event object. |
|
434 */ |
|
435 includeBrowserStylesChanged: |
|
436 function CssHtmltree_includeBrowserStylesChanged(aEvent) |
|
437 { |
|
438 this.refreshSourceFilter(); |
|
439 this.refreshPanel(); |
|
440 }, |
|
441 |
|
442 /** |
|
443 * When includeBrowserStyles.checked is false we only display properties that |
|
444 * have matched selectors and have been included by the document or one of the |
|
445 * document's stylesheets. If .checked is false we display all properties |
|
446 * including those that come from UA stylesheets. |
|
447 */ |
|
448 refreshSourceFilter: function CssHtmlTree_setSourceFilter() |
|
449 { |
|
450 this._matchedProperties = null; |
|
451 this._sourceFilter = this.includeBrowserStyles ? |
|
452 CssLogic.FILTER.UA : |
|
453 CssLogic.FILTER.USER; |
|
454 }, |
|
455 |
|
456 _updateSourceLinks: function CssHtmlTree__updateSourceLinks() |
|
457 { |
|
458 for (let propView of this.propertyViews) { |
|
459 propView.updateSourceLinks(); |
|
460 } |
|
461 }, |
|
462 |
|
463 /** |
|
464 * The CSS as displayed by the UI. |
|
465 */ |
|
466 createStyleViews: function CssHtmlTree_createStyleViews() |
|
467 { |
|
468 if (CssHtmlTree.propertyNames) { |
|
469 return; |
|
470 } |
|
471 |
|
472 CssHtmlTree.propertyNames = []; |
|
473 |
|
474 // Here we build and cache a list of css properties supported by the browser |
|
475 // We could use any element but let's use the main document's root element |
|
476 let styles = this.styleWindow.getComputedStyle(this.styleDocument.documentElement); |
|
477 let mozProps = []; |
|
478 for (let i = 0, numStyles = styles.length; i < numStyles; i++) { |
|
479 let prop = styles.item(i); |
|
480 if (prop.charAt(0) == "-") { |
|
481 mozProps.push(prop); |
|
482 } else { |
|
483 CssHtmlTree.propertyNames.push(prop); |
|
484 } |
|
485 } |
|
486 |
|
487 CssHtmlTree.propertyNames.sort(); |
|
488 CssHtmlTree.propertyNames.push.apply(CssHtmlTree.propertyNames, |
|
489 mozProps.sort()); |
|
490 |
|
491 this._createPropertyViews(); |
|
492 }, |
|
493 |
|
494 /** |
|
495 * Get a set of properties that have matched selectors. |
|
496 * |
|
497 * @return {Set} If a property name is in the set, it has matching selectors. |
|
498 */ |
|
499 get matchedProperties() |
|
500 { |
|
501 return this._matchedProperties || new Set; |
|
502 }, |
|
503 |
|
504 /** |
|
505 * Focus the window on mousedown. |
|
506 * |
|
507 * @param aEvent The event object |
|
508 */ |
|
509 focusWindow: function(aEvent) |
|
510 { |
|
511 let win = this.styleDocument.defaultView; |
|
512 win.focus(); |
|
513 }, |
|
514 |
|
515 /** |
|
516 * Executed by the tooltip when the pointer hovers over an element of the view. |
|
517 * Used to decide whether the tooltip should be shown or not and to actually |
|
518 * put content in it. |
|
519 * Checks if the hovered target is a css value we support tooltips for. |
|
520 */ |
|
521 _onTooltipTargetHover: function(target) |
|
522 { |
|
523 let inspector = this.styleInspector.inspector; |
|
524 |
|
525 // Test for image url |
|
526 if (target.classList.contains("theme-link") && inspector.hasUrlToImageDataResolver) { |
|
527 let propValue = target.parentNode; |
|
528 let propName = propValue.parentNode.querySelector(".property-name"); |
|
529 if (propName.textContent === "background-image") { |
|
530 let maxDim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize"); |
|
531 let uri = CssLogic.getBackgroundImageUriFromProperty(propValue.textContent); |
|
532 return this.tooltip.setRelativeImageContent(uri, inspector.inspector, maxDim); |
|
533 } |
|
534 } |
|
535 |
|
536 if (target.classList.contains("property-value")) { |
|
537 let propValue = target; |
|
538 let propName = target.parentNode.querySelector(".property-name"); |
|
539 |
|
540 // Test for css transform |
|
541 if (propName.textContent === "transform") { |
|
542 return this.tooltip.setCssTransformContent(propValue.textContent, |
|
543 this.pageStyle, this.viewedElement); |
|
544 } |
|
545 |
|
546 // Test for font family |
|
547 if (propName.textContent === "font-family") { |
|
548 this.tooltip.setFontFamilyContent(propValue.textContent); |
|
549 return true; |
|
550 } |
|
551 } |
|
552 |
|
553 // If the target isn't one that should receive a tooltip, signal it by rejecting |
|
554 // a promise |
|
555 return promise.reject(); |
|
556 }, |
|
557 |
|
558 /** |
|
559 * Create a context menu. |
|
560 */ |
|
561 _buildContextMenu: function() |
|
562 { |
|
563 let doc = this.styleDocument.defaultView.parent.document; |
|
564 |
|
565 this._contextmenu = this.styleDocument.createElementNS(XUL_NS, "menupopup"); |
|
566 this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate); |
|
567 this._contextmenu.id = "computed-view-context-menu"; |
|
568 |
|
569 // Select All |
|
570 this.menuitemSelectAll = createMenuItem(this._contextmenu, { |
|
571 label: "computedView.contextmenu.selectAll", |
|
572 accesskey: "computedView.contextmenu.selectAll.accessKey", |
|
573 command: this._onSelectAll |
|
574 }); |
|
575 |
|
576 // Copy |
|
577 this.menuitemCopy = createMenuItem(this._contextmenu, { |
|
578 label: "computedView.contextmenu.copy", |
|
579 accesskey: "computedView.contextmenu.copy.accessKey", |
|
580 command: this._onCopy |
|
581 }); |
|
582 |
|
583 // Show Original Sources |
|
584 this.menuitemSources= createMenuItem(this._contextmenu, { |
|
585 label: "ruleView.contextmenu.showOrigSources", |
|
586 accesskey: "ruleView.contextmenu.showOrigSources.accessKey", |
|
587 command: this._onToggleOrigSources |
|
588 }); |
|
589 |
|
590 let popupset = doc.documentElement.querySelector("popupset"); |
|
591 if (!popupset) { |
|
592 popupset = doc.createElementNS(XUL_NS, "popupset"); |
|
593 doc.documentElement.appendChild(popupset); |
|
594 } |
|
595 popupset.appendChild(this._contextmenu); |
|
596 }, |
|
597 |
|
598 /** |
|
599 * Update the context menu. This means enabling or disabling menuitems as |
|
600 * appropriate. |
|
601 */ |
|
602 _contextMenuUpdate: function() |
|
603 { |
|
604 let win = this.styleDocument.defaultView; |
|
605 let disable = win.getSelection().isCollapsed; |
|
606 this.menuitemCopy.disabled = disable; |
|
607 |
|
608 let label = "ruleView.contextmenu.showOrigSources"; |
|
609 if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) { |
|
610 label = "ruleView.contextmenu.showCSSSources"; |
|
611 } |
|
612 this.menuitemSources.setAttribute("label", |
|
613 CssHtmlTree.l10n(label)); |
|
614 |
|
615 let accessKey = label + ".accessKey"; |
|
616 this.menuitemSources.setAttribute("accesskey", |
|
617 CssHtmlTree.l10n(accessKey)); |
|
618 }, |
|
619 |
|
620 /** |
|
621 * Context menu handler. |
|
622 */ |
|
623 _onContextMenu: function(event) { |
|
624 try { |
|
625 this.styleDocument.defaultView.focus(); |
|
626 this._contextmenu.openPopupAtScreen(event.screenX, event.screenY, true); |
|
627 } catch(e) { |
|
628 console.error(e); |
|
629 } |
|
630 }, |
|
631 |
|
632 /** |
|
633 * Select all text. |
|
634 */ |
|
635 _onSelectAll: function() |
|
636 { |
|
637 try { |
|
638 let win = this.styleDocument.defaultView; |
|
639 let selection = win.getSelection(); |
|
640 |
|
641 selection.selectAllChildren(this.styleDocument.documentElement); |
|
642 } catch(e) { |
|
643 console.error(e); |
|
644 } |
|
645 }, |
|
646 |
|
647 _onClick: function(event) { |
|
648 let target = event.target; |
|
649 |
|
650 if (target.nodeName === "a") { |
|
651 event.stopPropagation(); |
|
652 event.preventDefault(); |
|
653 let browserWin = this.styleInspector.inspector.target |
|
654 .tab.ownerDocument.defaultView; |
|
655 browserWin.openUILinkIn(target.href, "tab"); |
|
656 } |
|
657 }, |
|
658 |
|
659 /** |
|
660 * Copy selected text. |
|
661 * |
|
662 * @param event The event object |
|
663 */ |
|
664 _onCopy: function(event) |
|
665 { |
|
666 try { |
|
667 let win = this.styleDocument.defaultView; |
|
668 let text = win.getSelection().toString().trim(); |
|
669 |
|
670 // Tidy up block headings by moving CSS property names and their values onto |
|
671 // the same line and inserting a colon between them. |
|
672 let textArray = text.split(/[\r\n]+/); |
|
673 let result = ""; |
|
674 |
|
675 // Parse text array to output string. |
|
676 if (textArray.length > 1) { |
|
677 for (let prop of textArray) { |
|
678 if (CssHtmlTree.propertyNames.indexOf(prop) !== -1) { |
|
679 // Property name |
|
680 result += prop; |
|
681 } else { |
|
682 // Property value |
|
683 result += ": " + prop; |
|
684 if (result.length > 0) { |
|
685 result += ";\n"; |
|
686 } |
|
687 } |
|
688 } |
|
689 } else { |
|
690 // Short text fragment. |
|
691 result = textArray[0]; |
|
692 } |
|
693 |
|
694 clipboardHelper.copyString(result, this.styleDocument); |
|
695 |
|
696 if (event) { |
|
697 event.preventDefault(); |
|
698 } |
|
699 } catch(e) { |
|
700 console.error(e); |
|
701 } |
|
702 }, |
|
703 |
|
704 /** |
|
705 * Toggle the original sources pref. |
|
706 */ |
|
707 _onToggleOrigSources: function() |
|
708 { |
|
709 let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); |
|
710 Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled); |
|
711 }, |
|
712 |
|
713 /** |
|
714 * Destructor for CssHtmlTree. |
|
715 */ |
|
716 destroy: function CssHtmlTree_destroy() |
|
717 { |
|
718 delete this.viewedElement; |
|
719 delete this._outputParser; |
|
720 |
|
721 // Remove event listeners |
|
722 this.includeBrowserStylesCheckbox.removeEventListener("command", |
|
723 this.includeBrowserStylesChanged); |
|
724 this.searchField.removeEventListener("command", this.filterChanged); |
|
725 gDevTools.off("pref-changed", this._handlePrefChange); |
|
726 |
|
727 this._prefObserver.off(PREF_ORIG_SOURCES, this._updateSourceLinks); |
|
728 this._prefObserver.destroy(); |
|
729 |
|
730 // Cancel tree construction |
|
731 if (this._createViewsProcess) { |
|
732 this._createViewsProcess.cancel(); |
|
733 } |
|
734 if (this._refreshProcess) { |
|
735 this._refreshProcess.cancel(); |
|
736 } |
|
737 |
|
738 this.propertyContainer.removeEventListener("click", this._onClick, false); |
|
739 |
|
740 // Remove context menu |
|
741 if (this._contextmenu) { |
|
742 // Destroy the Select All menuitem. |
|
743 this.menuitemCopy.removeEventListener("command", this._onCopy); |
|
744 this.menuitemCopy = null; |
|
745 |
|
746 // Destroy the Copy menuitem. |
|
747 this.menuitemSelectAll.removeEventListener("command", this._onSelectAll); |
|
748 this.menuitemSelectAll = null; |
|
749 |
|
750 // Destroy the context menu. |
|
751 this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate); |
|
752 this._contextmenu.parentNode.removeChild(this._contextmenu); |
|
753 this._contextmenu = null; |
|
754 } |
|
755 |
|
756 this.tooltip.stopTogglingOnHover(this.propertyContainer); |
|
757 this.tooltip.destroy(); |
|
758 |
|
759 // Remove bound listeners |
|
760 this.styleDocument.removeEventListener("contextmenu", this._onContextMenu); |
|
761 this.styleDocument.removeEventListener("copy", this._onCopy); |
|
762 this.styleDocument.removeEventListener("mousedown", this.focusWindow); |
|
763 |
|
764 // Nodes used in templating |
|
765 delete this.root; |
|
766 delete this.propertyContainer; |
|
767 delete this.panel; |
|
768 |
|
769 // The document in which we display the results (csshtmltree.xul). |
|
770 delete this.styleDocument; |
|
771 |
|
772 for (let propView of this.propertyViews) { |
|
773 propView.destroy(); |
|
774 } |
|
775 |
|
776 // The element that we're inspecting, and the document that it comes from. |
|
777 delete this.propertyViews; |
|
778 delete this.styleWindow; |
|
779 delete this.styleDocument; |
|
780 delete this.styleInspector; |
|
781 } |
|
782 }; |
|
783 |
|
784 function PropertyInfo(aTree, aName) { |
|
785 this.tree = aTree; |
|
786 this.name = aName; |
|
787 } |
|
788 PropertyInfo.prototype = { |
|
789 get value() { |
|
790 if (this.tree._computed) { |
|
791 let value = this.tree._computed[this.name].value; |
|
792 return value; |
|
793 } |
|
794 } |
|
795 }; |
|
796 |
|
797 function createMenuItem(aMenu, aAttributes) |
|
798 { |
|
799 let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem"); |
|
800 |
|
801 item.setAttribute("label", CssHtmlTree.l10n(aAttributes.label)); |
|
802 item.setAttribute("accesskey", CssHtmlTree.l10n(aAttributes.accesskey)); |
|
803 item.addEventListener("command", aAttributes.command); |
|
804 |
|
805 aMenu.appendChild(item); |
|
806 |
|
807 return item; |
|
808 } |
|
809 |
|
810 /** |
|
811 * A container to give easy access to property data from the template engine. |
|
812 * |
|
813 * @constructor |
|
814 * @param {CssHtmlTree} aTree the CssHtmlTree instance we are working with. |
|
815 * @param {string} aName the CSS property name for which this PropertyView |
|
816 * instance will render the rules. |
|
817 */ |
|
818 function PropertyView(aTree, aName) |
|
819 { |
|
820 this.tree = aTree; |
|
821 this.name = aName; |
|
822 this.getRTLAttr = aTree.getRTLAttr; |
|
823 |
|
824 this.link = "https://developer.mozilla.org/CSS/" + aName; |
|
825 |
|
826 this.templateMatchedSelectors = aTree.styleDocument.getElementById("templateMatchedSelectors"); |
|
827 this._propertyInfo = new PropertyInfo(aTree, aName); |
|
828 } |
|
829 |
|
830 PropertyView.prototype = { |
|
831 // The parent element which contains the open attribute |
|
832 element: null, |
|
833 |
|
834 // Property header node |
|
835 propertyHeader: null, |
|
836 |
|
837 // Destination for property names |
|
838 nameNode: null, |
|
839 |
|
840 // Destination for property values |
|
841 valueNode: null, |
|
842 |
|
843 // Are matched rules expanded? |
|
844 matchedExpanded: false, |
|
845 |
|
846 // Matched selector container |
|
847 matchedSelectorsContainer: null, |
|
848 |
|
849 // Matched selector expando |
|
850 matchedExpander: null, |
|
851 |
|
852 // Cache for matched selector views |
|
853 _matchedSelectorViews: null, |
|
854 |
|
855 // The previously selected element used for the selector view caches |
|
856 prevViewedElement: null, |
|
857 |
|
858 /** |
|
859 * Get the computed style for the current property. |
|
860 * |
|
861 * @return {string} the computed style for the current property of the |
|
862 * currently highlighted element. |
|
863 */ |
|
864 get value() |
|
865 { |
|
866 return this.propertyInfo.value; |
|
867 }, |
|
868 |
|
869 /** |
|
870 * An easy way to access the CssPropertyInfo behind this PropertyView. |
|
871 */ |
|
872 get propertyInfo() |
|
873 { |
|
874 return this._propertyInfo; |
|
875 }, |
|
876 |
|
877 /** |
|
878 * Does the property have any matched selectors? |
|
879 */ |
|
880 get hasMatchedSelectors() |
|
881 { |
|
882 return this.tree.matchedProperties.has(this.name); |
|
883 }, |
|
884 |
|
885 /** |
|
886 * Should this property be visible? |
|
887 */ |
|
888 get visible() |
|
889 { |
|
890 if (!this.tree.viewedElement) { |
|
891 return false; |
|
892 } |
|
893 |
|
894 if (!this.tree.includeBrowserStyles && !this.hasMatchedSelectors) { |
|
895 return false; |
|
896 } |
|
897 |
|
898 let searchTerm = this.tree.searchField.value.toLowerCase(); |
|
899 if (searchTerm && this.name.toLowerCase().indexOf(searchTerm) == -1 && |
|
900 this.value.toLowerCase().indexOf(searchTerm) == -1) { |
|
901 return false; |
|
902 } |
|
903 |
|
904 return true; |
|
905 }, |
|
906 |
|
907 /** |
|
908 * Returns the className that should be assigned to the propertyView. |
|
909 * @return string |
|
910 */ |
|
911 get propertyHeaderClassName() |
|
912 { |
|
913 if (this.visible) { |
|
914 let isDark = this.tree._darkStripe = !this.tree._darkStripe; |
|
915 return isDark ? "property-view theme-bg-darker" : "property-view"; |
|
916 } |
|
917 return "property-view-hidden"; |
|
918 }, |
|
919 |
|
920 /** |
|
921 * Returns the className that should be assigned to the propertyView content |
|
922 * container. |
|
923 * @return string |
|
924 */ |
|
925 get propertyContentClassName() |
|
926 { |
|
927 if (this.visible) { |
|
928 let isDark = this.tree._darkStripe; |
|
929 return isDark ? "property-content theme-bg-darker" : "property-content"; |
|
930 } |
|
931 return "property-content-hidden"; |
|
932 }, |
|
933 |
|
934 /** |
|
935 * Build the markup for on computed style |
|
936 * @return Element |
|
937 */ |
|
938 buildMain: function PropertyView_buildMain() |
|
939 { |
|
940 let doc = this.tree.styleDocument; |
|
941 |
|
942 // Build the container element |
|
943 this.onMatchedToggle = this.onMatchedToggle.bind(this); |
|
944 this.element = doc.createElementNS(HTML_NS, "div"); |
|
945 this.element.setAttribute("class", this.propertyHeaderClassName); |
|
946 this.element.addEventListener("dblclick", this.onMatchedToggle, false); |
|
947 |
|
948 // Make it keyboard navigable |
|
949 this.element.setAttribute("tabindex", "0"); |
|
950 this.onKeyDown = (aEvent) => { |
|
951 let keyEvent = Ci.nsIDOMKeyEvent; |
|
952 if (aEvent.keyCode == keyEvent.DOM_VK_F1) { |
|
953 this.mdnLinkClick(); |
|
954 } |
|
955 if (aEvent.keyCode == keyEvent.DOM_VK_RETURN || |
|
956 aEvent.keyCode == keyEvent.DOM_VK_SPACE) { |
|
957 this.onMatchedToggle(aEvent); |
|
958 } |
|
959 }; |
|
960 this.element.addEventListener("keydown", this.onKeyDown, false); |
|
961 |
|
962 // Build the twisty expand/collapse |
|
963 this.matchedExpander = doc.createElementNS(HTML_NS, "div"); |
|
964 this.matchedExpander.className = "expander theme-twisty"; |
|
965 this.matchedExpander.addEventListener("click", this.onMatchedToggle, false); |
|
966 this.element.appendChild(this.matchedExpander); |
|
967 |
|
968 this.focusElement = () => this.element.focus(); |
|
969 |
|
970 // Build the style name element |
|
971 this.nameNode = doc.createElementNS(HTML_NS, "div"); |
|
972 this.nameNode.setAttribute("class", "property-name theme-fg-color5"); |
|
973 // Reset its tabindex attribute otherwise, if an ellipsis is applied |
|
974 // it will be reachable via TABing |
|
975 this.nameNode.setAttribute("tabindex", ""); |
|
976 this.nameNode.textContent = this.nameNode.title = this.name; |
|
977 // Make it hand over the focus to the container |
|
978 this.onFocus = () => this.element.focus(); |
|
979 this.nameNode.addEventListener("click", this.onFocus, false); |
|
980 this.element.appendChild(this.nameNode); |
|
981 |
|
982 // Build the style value element |
|
983 this.valueNode = doc.createElementNS(HTML_NS, "div"); |
|
984 this.valueNode.setAttribute("class", "property-value theme-fg-color1"); |
|
985 // Reset its tabindex attribute otherwise, if an ellipsis is applied |
|
986 // it will be reachable via TABing |
|
987 this.valueNode.setAttribute("tabindex", ""); |
|
988 this.valueNode.setAttribute("dir", "ltr"); |
|
989 // Make it hand over the focus to the container |
|
990 this.valueNode.addEventListener("click", this.onFocus, false); |
|
991 this.element.appendChild(this.valueNode); |
|
992 |
|
993 return this.element; |
|
994 }, |
|
995 |
|
996 buildSelectorContainer: function PropertyView_buildSelectorContainer() |
|
997 { |
|
998 let doc = this.tree.styleDocument; |
|
999 let element = doc.createElementNS(HTML_NS, "div"); |
|
1000 element.setAttribute("class", this.propertyContentClassName); |
|
1001 this.matchedSelectorsContainer = doc.createElementNS(HTML_NS, "div"); |
|
1002 this.matchedSelectorsContainer.setAttribute("class", "matchedselectors"); |
|
1003 element.appendChild(this.matchedSelectorsContainer); |
|
1004 |
|
1005 return element; |
|
1006 }, |
|
1007 |
|
1008 /** |
|
1009 * Refresh the panel's CSS property value. |
|
1010 */ |
|
1011 refresh: function PropertyView_refresh() |
|
1012 { |
|
1013 this.element.className = this.propertyHeaderClassName; |
|
1014 this.element.nextElementSibling.className = this.propertyContentClassName; |
|
1015 |
|
1016 if (this.prevViewedElement != this.tree.viewedElement) { |
|
1017 this._matchedSelectorViews = null; |
|
1018 this.prevViewedElement = this.tree.viewedElement; |
|
1019 } |
|
1020 |
|
1021 if (!this.tree.viewedElement || !this.visible) { |
|
1022 this.valueNode.textContent = this.valueNode.title = ""; |
|
1023 this.matchedSelectorsContainer.parentNode.hidden = true; |
|
1024 this.matchedSelectorsContainer.textContent = ""; |
|
1025 this.matchedExpander.removeAttribute("open"); |
|
1026 return; |
|
1027 } |
|
1028 |
|
1029 this.tree.numVisibleProperties++; |
|
1030 |
|
1031 let outputParser = this.tree._outputParser; |
|
1032 let frag = outputParser.parseCssProperty(this.propertyInfo.name, |
|
1033 this.propertyInfo.value, |
|
1034 { |
|
1035 colorSwatchClass: "computedview-colorswatch", |
|
1036 urlClass: "theme-link" |
|
1037 // No need to use baseURI here as computed URIs are never relative. |
|
1038 }); |
|
1039 this.valueNode.innerHTML = ""; |
|
1040 this.valueNode.appendChild(frag); |
|
1041 |
|
1042 this.refreshMatchedSelectors(); |
|
1043 }, |
|
1044 |
|
1045 /** |
|
1046 * Refresh the panel matched rules. |
|
1047 */ |
|
1048 refreshMatchedSelectors: function PropertyView_refreshMatchedSelectors() |
|
1049 { |
|
1050 let hasMatchedSelectors = this.hasMatchedSelectors; |
|
1051 this.matchedSelectorsContainer.parentNode.hidden = !hasMatchedSelectors; |
|
1052 |
|
1053 if (hasMatchedSelectors) { |
|
1054 this.matchedExpander.classList.add("expandable"); |
|
1055 } else { |
|
1056 this.matchedExpander.classList.remove("expandable"); |
|
1057 } |
|
1058 |
|
1059 if (this.matchedExpanded && hasMatchedSelectors) { |
|
1060 return this.tree.pageStyle.getMatchedSelectors(this.tree.viewedElement, this.name).then(matched => { |
|
1061 if (!this.matchedExpanded) { |
|
1062 return; |
|
1063 } |
|
1064 |
|
1065 this._matchedSelectorResponse = matched; |
|
1066 CssHtmlTree.processTemplate(this.templateMatchedSelectors, |
|
1067 this.matchedSelectorsContainer, this); |
|
1068 this.matchedExpander.setAttribute("open", ""); |
|
1069 this.tree.styleInspector.inspector.emit("computed-view-property-expanded"); |
|
1070 }).then(null, console.error); |
|
1071 } else { |
|
1072 this.matchedSelectorsContainer.innerHTML = ""; |
|
1073 this.matchedExpander.removeAttribute("open"); |
|
1074 this.tree.styleInspector.inspector.emit("computed-view-property-collapsed"); |
|
1075 return promise.resolve(undefined); |
|
1076 } |
|
1077 }, |
|
1078 |
|
1079 get matchedSelectors() |
|
1080 { |
|
1081 return this._matchedSelectorResponse; |
|
1082 }, |
|
1083 |
|
1084 /** |
|
1085 * Provide access to the matched SelectorViews that we are currently |
|
1086 * displaying. |
|
1087 */ |
|
1088 get matchedSelectorViews() |
|
1089 { |
|
1090 if (!this._matchedSelectorViews) { |
|
1091 this._matchedSelectorViews = []; |
|
1092 this._matchedSelectorResponse.forEach( |
|
1093 function matchedSelectorViews_convert(aSelectorInfo) { |
|
1094 this._matchedSelectorViews.push(new SelectorView(this.tree, aSelectorInfo)); |
|
1095 }, this); |
|
1096 } |
|
1097 |
|
1098 return this._matchedSelectorViews; |
|
1099 }, |
|
1100 |
|
1101 /** |
|
1102 * Update all the selector source links to reflect whether we're linking to |
|
1103 * original sources (e.g. Sass files). |
|
1104 */ |
|
1105 updateSourceLinks: function PropertyView_updateSourceLinks() |
|
1106 { |
|
1107 if (!this._matchedSelectorViews) { |
|
1108 return; |
|
1109 } |
|
1110 for (let view of this._matchedSelectorViews) { |
|
1111 view.updateSourceLink(); |
|
1112 } |
|
1113 }, |
|
1114 |
|
1115 /** |
|
1116 * The action when a user expands matched selectors. |
|
1117 * |
|
1118 * @param {Event} aEvent Used to determine the class name of the targets click |
|
1119 * event. |
|
1120 */ |
|
1121 onMatchedToggle: function PropertyView_onMatchedToggle(aEvent) |
|
1122 { |
|
1123 this.matchedExpanded = !this.matchedExpanded; |
|
1124 this.refreshMatchedSelectors(); |
|
1125 aEvent.preventDefault(); |
|
1126 }, |
|
1127 |
|
1128 /** |
|
1129 * The action when a user clicks on the MDN help link for a property. |
|
1130 */ |
|
1131 mdnLinkClick: function PropertyView_mdnLinkClick(aEvent) |
|
1132 { |
|
1133 let inspector = this.tree.styleInspector.inspector; |
|
1134 |
|
1135 if (inspector.target.tab) { |
|
1136 let browserWin = inspector.target.tab.ownerDocument.defaultView; |
|
1137 browserWin.openUILinkIn(this.link, "tab"); |
|
1138 } |
|
1139 aEvent.preventDefault(); |
|
1140 }, |
|
1141 |
|
1142 /** |
|
1143 * Destroy this property view, removing event listeners |
|
1144 */ |
|
1145 destroy: function PropertyView_destroy() { |
|
1146 this.element.removeEventListener("dblclick", this.onMatchedToggle, false); |
|
1147 this.element.removeEventListener("keydown", this.onKeyDown, false); |
|
1148 this.element = null; |
|
1149 |
|
1150 this.matchedExpander.removeEventListener("click", this.onMatchedToggle, false); |
|
1151 this.matchedExpander = null; |
|
1152 |
|
1153 this.nameNode.removeEventListener("click", this.onFocus, false); |
|
1154 this.nameNode = null; |
|
1155 |
|
1156 this.valueNode.removeEventListener("click", this.onFocus, false); |
|
1157 this.valueNode = null; |
|
1158 } |
|
1159 }; |
|
1160 |
|
1161 /** |
|
1162 * A container to give us easy access to display data from a CssRule |
|
1163 * @param CssHtmlTree aTree, the owning CssHtmlTree |
|
1164 * @param aSelectorInfo |
|
1165 */ |
|
1166 function SelectorView(aTree, aSelectorInfo) |
|
1167 { |
|
1168 this.tree = aTree; |
|
1169 this.selectorInfo = aSelectorInfo; |
|
1170 this._cacheStatusNames(); |
|
1171 |
|
1172 this.updateSourceLink(); |
|
1173 } |
|
1174 |
|
1175 /** |
|
1176 * Decode for cssInfo.rule.status |
|
1177 * @see SelectorView.prototype._cacheStatusNames |
|
1178 * @see CssLogic.STATUS |
|
1179 */ |
|
1180 SelectorView.STATUS_NAMES = [ |
|
1181 // "Parent Match", "Matched", "Best Match" |
|
1182 ]; |
|
1183 |
|
1184 SelectorView.CLASS_NAMES = [ |
|
1185 "parentmatch", "matched", "bestmatch" |
|
1186 ]; |
|
1187 |
|
1188 SelectorView.prototype = { |
|
1189 /** |
|
1190 * Cache localized status names. |
|
1191 * |
|
1192 * These statuses are localized inside the styleinspector.properties string |
|
1193 * bundle. |
|
1194 * @see css-logic.js - the CssLogic.STATUS array. |
|
1195 * |
|
1196 * @return {void} |
|
1197 */ |
|
1198 _cacheStatusNames: function SelectorView_cacheStatusNames() |
|
1199 { |
|
1200 if (SelectorView.STATUS_NAMES.length) { |
|
1201 return; |
|
1202 } |
|
1203 |
|
1204 for (let status in CssLogic.STATUS) { |
|
1205 let i = CssLogic.STATUS[status]; |
|
1206 if (i > CssLogic.STATUS.UNMATCHED) { |
|
1207 let value = CssHtmlTree.l10n("rule.status." + status); |
|
1208 // Replace normal spaces with non-breaking spaces |
|
1209 SelectorView.STATUS_NAMES[i] = value.replace(/ /g, '\u00A0'); |
|
1210 } |
|
1211 } |
|
1212 }, |
|
1213 |
|
1214 /** |
|
1215 * A localized version of cssRule.status |
|
1216 */ |
|
1217 get statusText() |
|
1218 { |
|
1219 return SelectorView.STATUS_NAMES[this.selectorInfo.status]; |
|
1220 }, |
|
1221 |
|
1222 /** |
|
1223 * Get class name for selector depending on status |
|
1224 */ |
|
1225 get statusClass() |
|
1226 { |
|
1227 return SelectorView.CLASS_NAMES[this.selectorInfo.status - 1]; |
|
1228 }, |
|
1229 |
|
1230 get href() |
|
1231 { |
|
1232 if (this._href) { |
|
1233 return this._href; |
|
1234 } |
|
1235 let sheet = this.selectorInfo.rule.parentStyleSheet; |
|
1236 this._href = sheet ? sheet.href : "#"; |
|
1237 return this._href; |
|
1238 }, |
|
1239 |
|
1240 get sourceText() |
|
1241 { |
|
1242 return this.selectorInfo.sourceText; |
|
1243 }, |
|
1244 |
|
1245 |
|
1246 get value() |
|
1247 { |
|
1248 return this.selectorInfo.value; |
|
1249 }, |
|
1250 |
|
1251 get outputFragment() |
|
1252 { |
|
1253 // Sadly, because this fragment is added to the template by DOM Templater |
|
1254 // we lose any events that are attached. This means that URLs will open in a |
|
1255 // new window. At some point we should fix this by stopping using the |
|
1256 // templater. |
|
1257 let outputParser = this.tree._outputParser; |
|
1258 let frag = outputParser.parseCssProperty( |
|
1259 this.selectorInfo.name, |
|
1260 this.selectorInfo.value, { |
|
1261 colorSwatchClass: "computedview-colorswatch", |
|
1262 urlClass: "theme-link", |
|
1263 baseURI: this.selectorInfo.rule.href |
|
1264 }); |
|
1265 return frag; |
|
1266 }, |
|
1267 |
|
1268 /** |
|
1269 * Update the text of the source link to reflect whether we're showing |
|
1270 * original sources or not. |
|
1271 */ |
|
1272 updateSourceLink: function() |
|
1273 { |
|
1274 this.updateSource().then((oldSource) => { |
|
1275 if (oldSource != this.source && this.tree.propertyContainer) { |
|
1276 let selector = '[sourcelocation="' + oldSource + '"]'; |
|
1277 let link = this.tree.propertyContainer.querySelector(selector); |
|
1278 if (link) { |
|
1279 link.textContent = this.source; |
|
1280 link.setAttribute("sourcelocation", this.source); |
|
1281 } |
|
1282 } |
|
1283 }); |
|
1284 }, |
|
1285 |
|
1286 /** |
|
1287 * Update the 'source' store based on our original sources preference. |
|
1288 */ |
|
1289 updateSource: function() |
|
1290 { |
|
1291 let rule = this.selectorInfo.rule; |
|
1292 this.sheet = rule.parentStyleSheet; |
|
1293 |
|
1294 if (!rule || !this.sheet) { |
|
1295 let oldSource = this.source; |
|
1296 this.source = CssLogic.l10n("rule.sourceElement"); |
|
1297 this.href = "#"; |
|
1298 return promise.resolve(oldSource); |
|
1299 } |
|
1300 |
|
1301 let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); |
|
1302 |
|
1303 if (showOrig && rule.type != ELEMENT_STYLE) { |
|
1304 let deferred = promise.defer(); |
|
1305 |
|
1306 // set as this first so we show something while we're fetching |
|
1307 this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line; |
|
1308 |
|
1309 rule.getOriginalLocation().then(({href, line, column}) => { |
|
1310 let oldSource = this.source; |
|
1311 this.source = CssLogic.shortSource({href: href}) + ":" + line; |
|
1312 deferred.resolve(oldSource); |
|
1313 }); |
|
1314 |
|
1315 return deferred.promise; |
|
1316 } |
|
1317 |
|
1318 let oldSource = this.source; |
|
1319 this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line; |
|
1320 return promise.resolve(oldSource); |
|
1321 }, |
|
1322 |
|
1323 /** |
|
1324 * Open the style editor if the RETURN key was pressed. |
|
1325 */ |
|
1326 maybeOpenStyleEditor: function(aEvent) |
|
1327 { |
|
1328 let keyEvent = Ci.nsIDOMKeyEvent; |
|
1329 if (aEvent.keyCode == keyEvent.DOM_VK_RETURN) { |
|
1330 this.openStyleEditor(); |
|
1331 } |
|
1332 }, |
|
1333 |
|
1334 /** |
|
1335 * When a css link is clicked this method is called in order to either: |
|
1336 * 1. Open the link in view source (for chrome stylesheets). |
|
1337 * 2. Open the link in the style editor. |
|
1338 * |
|
1339 * We can only view stylesheets contained in document.styleSheets inside the |
|
1340 * style editor. |
|
1341 * |
|
1342 * @param aEvent The click event |
|
1343 */ |
|
1344 openStyleEditor: function(aEvent) |
|
1345 { |
|
1346 let inspector = this.tree.styleInspector.inspector; |
|
1347 let rule = this.selectorInfo.rule; |
|
1348 |
|
1349 // The style editor can only display stylesheets coming from content because |
|
1350 // chrome stylesheets are not listed in the editor's stylesheet selector. |
|
1351 // |
|
1352 // If the stylesheet is a content stylesheet we send it to the style |
|
1353 // editor else we display it in the view source window. |
|
1354 let sheet = rule.parentStyleSheet; |
|
1355 if (!sheet || sheet.isSystem) { |
|
1356 let contentDoc = null; |
|
1357 if (this.tree.viewedElement.isLocal_toBeDeprecated()) { |
|
1358 let rawNode = this.tree.viewedElement.rawNode(); |
|
1359 if (rawNode) { |
|
1360 contentDoc = rawNode.ownerDocument; |
|
1361 } |
|
1362 } |
|
1363 let viewSourceUtils = inspector.viewSourceUtils; |
|
1364 viewSourceUtils.viewSource(rule.href, null, contentDoc, rule.line); |
|
1365 return; |
|
1366 } |
|
1367 |
|
1368 let location = promise.resolve({ |
|
1369 href: rule.href, |
|
1370 line: rule.line |
|
1371 }); |
|
1372 if (rule.href && Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) { |
|
1373 location = rule.getOriginalLocation(); |
|
1374 } |
|
1375 |
|
1376 location.then(({href, line}) => { |
|
1377 let target = inspector.target; |
|
1378 if (ToolDefinitions.styleEditor.isTargetSupported(target)) { |
|
1379 gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) { |
|
1380 toolbox.getCurrentPanel().selectStyleSheet(href, line); |
|
1381 }); |
|
1382 } |
|
1383 }); |
|
1384 } |
|
1385 }; |
|
1386 |
|
1387 exports.CssHtmlTree = CssHtmlTree; |
|
1388 exports.PropertyView = PropertyView; |