Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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/. */
7 const {Cc, Ci, Cu} = require("chrome");
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", {});
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");
23 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
24 "resource://gre/modules/PluralForm.jsm");
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";
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;
59 this.canceled = false;
60 }
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 },
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 },
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 },
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 };
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 = [];
139 this._outputParser = new OutputParser();
141 let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
142 getService(Ci.nsIXULChromeRegistry);
143 this.getRTLAttr = chromeReg.isLocaleRTL("global") ? "rtl" : "ltr";
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);
153 this.styleDocument.addEventListener("copy", this._onCopy);
154 this.styleDocument.addEventListener("mousedown", this.focusWindow);
155 this.styleDocument.addEventListener("contextmenu", this._onContextMenu);
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");
162 // Listen for click events
163 this.propertyContainer.addEventListener("click", this._onClick, false);
165 // No results text.
166 this.noResults = this.styleDocument.getElementById("noResults");
168 // Refresh panel when color unit changed.
169 this._handlePrefChange = this._handlePrefChange.bind(this);
170 gDevTools.on("pref-changed", this._handlePrefChange);
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);
177 CssHtmlTree.processTemplate(this.templateRoot, this.root, this);
179 // The element that we're inspecting, and the document that it comes from.
180 this.viewedElement = null;
182 // Properties preview tooltip
183 this.tooltip = new Tooltip(this.styleInspector.inspector.panelDoc);
184 this.tooltip.startTogglingOnHover(this.propertyContainer,
185 this._onTooltipTargetHover.bind(this));
187 this._buildContextMenu();
188 this.createStyleViews();
189 }
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 };
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 }
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);
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 };
237 XPCOMUtils.defineLazyGetter(CssHtmlTree, "_strings", function() Services.strings
238 .createBundle("chrome://global/locale/devtools/styleinspector.properties"));
240 XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
241 return Cc["@mozilla.org/widget/clipboardhelper;1"].
242 getService(Ci.nsIClipboardHelper);
243 });
245 CssHtmlTree.prototype = {
246 // Cache the list of properties that match the selected element.
247 _matchedProperties: null,
249 // Used for cancelling timeouts in the style filter.
250 _filterChangedTimeout: null,
252 // The search filter
253 searchField: null,
255 // Reference to the "Include browser styles" checkbox.
256 includeBrowserStylesCheckbox: null,
258 // Holds the ID of the panelRefresh timeout.
259 _panelRefreshTimeout: null,
261 // Toggle for zebra striping
262 _darkStripe: true,
264 // Number of visible properties
265 numVisibleProperties: 0,
267 setPageStyle: function(pageStyle) {
268 this.pageStyle = pageStyle;
269 },
271 get includeBrowserStyles()
272 {
273 return this.includeBrowserStylesCheckbox.checked;
274 },
276 _handlePrefChange: function(event, data) {
277 if (this._computed && (data.pref == "devtools.defaultColorUnit" ||
278 data.pref == PREF_ORIG_SOURCES)) {
279 this.refreshPanel();
280 }
281 },
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;
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 }
305 this.tooltip.hide();
307 if (aElement === this.viewedElement) {
308 return promise.resolve(undefined);
309 }
311 this.viewedElement = aElement;
312 this.refreshSourceFilter();
314 return this.refreshPanel();
315 },
317 _createPropertyViews: function()
318 {
319 if (this._createViewsPromise) {
320 return this._createViewsPromise;
321 }
323 let deferred = promise.defer();
324 this._createViewsPromise = deferred.promise;
326 this.refreshSourceFilter();
327 this.numVisibleProperties = 0;
328 let fragment = this.styleDocument.createDocumentFragment();
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());
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 });
353 this._createViewsProcess.schedule();
354 return deferred.promise;
355 },
357 /**
358 * Refresh the panel content.
359 */
360 refreshPanel: function CssHtmlTree_refreshPanel()
361 {
362 if (!this.viewedElement) {
363 return promise.resolve();
364 }
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;
382 if (this._refreshProcess) {
383 this._refreshProcess.cancel();
384 }
386 this.noResults.hidden = true;
388 // Reset visible property count
389 this.numVisibleProperties = 0;
391 // Reset zebra striping.
392 this._darkStripe = true;
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 },
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;
420 if (this._filterChangedTimeout) {
421 win.clearTimeout(this._filterChangedTimeout);
422 }
424 this._filterChangedTimeout = win.setTimeout(function() {
425 this.refreshPanel();
426 this._filterChangeTimeout = null;
427 }.bind(this), FILTER_CHANGED_TIMEOUT);
428 },
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 },
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 },
456 _updateSourceLinks: function CssHtmlTree__updateSourceLinks()
457 {
458 for (let propView of this.propertyViews) {
459 propView.updateSourceLinks();
460 }
461 },
463 /**
464 * The CSS as displayed by the UI.
465 */
466 createStyleViews: function CssHtmlTree_createStyleViews()
467 {
468 if (CssHtmlTree.propertyNames) {
469 return;
470 }
472 CssHtmlTree.propertyNames = [];
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 }
487 CssHtmlTree.propertyNames.sort();
488 CssHtmlTree.propertyNames.push.apply(CssHtmlTree.propertyNames,
489 mozProps.sort());
491 this._createPropertyViews();
492 },
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 },
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 },
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;
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 }
536 if (target.classList.contains("property-value")) {
537 let propValue = target;
538 let propName = target.parentNode.querySelector(".property-name");
540 // Test for css transform
541 if (propName.textContent === "transform") {
542 return this.tooltip.setCssTransformContent(propValue.textContent,
543 this.pageStyle, this.viewedElement);
544 }
546 // Test for font family
547 if (propName.textContent === "font-family") {
548 this.tooltip.setFontFamilyContent(propValue.textContent);
549 return true;
550 }
551 }
553 // If the target isn't one that should receive a tooltip, signal it by rejecting
554 // a promise
555 return promise.reject();
556 },
558 /**
559 * Create a context menu.
560 */
561 _buildContextMenu: function()
562 {
563 let doc = this.styleDocument.defaultView.parent.document;
565 this._contextmenu = this.styleDocument.createElementNS(XUL_NS, "menupopup");
566 this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate);
567 this._contextmenu.id = "computed-view-context-menu";
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 });
576 // Copy
577 this.menuitemCopy = createMenuItem(this._contextmenu, {
578 label: "computedView.contextmenu.copy",
579 accesskey: "computedView.contextmenu.copy.accessKey",
580 command: this._onCopy
581 });
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 });
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 },
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;
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));
615 let accessKey = label + ".accessKey";
616 this.menuitemSources.setAttribute("accesskey",
617 CssHtmlTree.l10n(accessKey));
618 },
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 },
632 /**
633 * Select all text.
634 */
635 _onSelectAll: function()
636 {
637 try {
638 let win = this.styleDocument.defaultView;
639 let selection = win.getSelection();
641 selection.selectAllChildren(this.styleDocument.documentElement);
642 } catch(e) {
643 console.error(e);
644 }
645 },
647 _onClick: function(event) {
648 let target = event.target;
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 },
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();
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 = "";
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 }
694 clipboardHelper.copyString(result, this.styleDocument);
696 if (event) {
697 event.preventDefault();
698 }
699 } catch(e) {
700 console.error(e);
701 }
702 },
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 },
713 /**
714 * Destructor for CssHtmlTree.
715 */
716 destroy: function CssHtmlTree_destroy()
717 {
718 delete this.viewedElement;
719 delete this._outputParser;
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);
727 this._prefObserver.off(PREF_ORIG_SOURCES, this._updateSourceLinks);
728 this._prefObserver.destroy();
730 // Cancel tree construction
731 if (this._createViewsProcess) {
732 this._createViewsProcess.cancel();
733 }
734 if (this._refreshProcess) {
735 this._refreshProcess.cancel();
736 }
738 this.propertyContainer.removeEventListener("click", this._onClick, false);
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;
746 // Destroy the Copy menuitem.
747 this.menuitemSelectAll.removeEventListener("command", this._onSelectAll);
748 this.menuitemSelectAll = null;
750 // Destroy the context menu.
751 this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate);
752 this._contextmenu.parentNode.removeChild(this._contextmenu);
753 this._contextmenu = null;
754 }
756 this.tooltip.stopTogglingOnHover(this.propertyContainer);
757 this.tooltip.destroy();
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);
764 // Nodes used in templating
765 delete this.root;
766 delete this.propertyContainer;
767 delete this.panel;
769 // The document in which we display the results (csshtmltree.xul).
770 delete this.styleDocument;
772 for (let propView of this.propertyViews) {
773 propView.destroy();
774 }
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 };
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 };
797 function createMenuItem(aMenu, aAttributes)
798 {
799 let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem");
801 item.setAttribute("label", CssHtmlTree.l10n(aAttributes.label));
802 item.setAttribute("accesskey", CssHtmlTree.l10n(aAttributes.accesskey));
803 item.addEventListener("command", aAttributes.command);
805 aMenu.appendChild(item);
807 return item;
808 }
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;
824 this.link = "https://developer.mozilla.org/CSS/" + aName;
826 this.templateMatchedSelectors = aTree.styleDocument.getElementById("templateMatchedSelectors");
827 this._propertyInfo = new PropertyInfo(aTree, aName);
828 }
830 PropertyView.prototype = {
831 // The parent element which contains the open attribute
832 element: null,
834 // Property header node
835 propertyHeader: null,
837 // Destination for property names
838 nameNode: null,
840 // Destination for property values
841 valueNode: null,
843 // Are matched rules expanded?
844 matchedExpanded: false,
846 // Matched selector container
847 matchedSelectorsContainer: null,
849 // Matched selector expando
850 matchedExpander: null,
852 // Cache for matched selector views
853 _matchedSelectorViews: null,
855 // The previously selected element used for the selector view caches
856 prevViewedElement: null,
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 },
869 /**
870 * An easy way to access the CssPropertyInfo behind this PropertyView.
871 */
872 get propertyInfo()
873 {
874 return this._propertyInfo;
875 },
877 /**
878 * Does the property have any matched selectors?
879 */
880 get hasMatchedSelectors()
881 {
882 return this.tree.matchedProperties.has(this.name);
883 },
885 /**
886 * Should this property be visible?
887 */
888 get visible()
889 {
890 if (!this.tree.viewedElement) {
891 return false;
892 }
894 if (!this.tree.includeBrowserStyles && !this.hasMatchedSelectors) {
895 return false;
896 }
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 }
904 return true;
905 },
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 },
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 },
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;
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);
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);
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);
968 this.focusElement = () => this.element.focus();
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);
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);
993 return this.element;
994 },
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);
1005 return element;
1006 },
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;
1016 if (this.prevViewedElement != this.tree.viewedElement) {
1017 this._matchedSelectorViews = null;
1018 this.prevViewedElement = this.tree.viewedElement;
1019 }
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 }
1029 this.tree.numVisibleProperties++;
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);
1042 this.refreshMatchedSelectors();
1043 },
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;
1053 if (hasMatchedSelectors) {
1054 this.matchedExpander.classList.add("expandable");
1055 } else {
1056 this.matchedExpander.classList.remove("expandable");
1057 }
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 }
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 },
1079 get matchedSelectors()
1080 {
1081 return this._matchedSelectorResponse;
1082 },
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 }
1098 return this._matchedSelectorViews;
1099 },
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 },
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 },
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;
1135 if (inspector.target.tab) {
1136 let browserWin = inspector.target.tab.ownerDocument.defaultView;
1137 browserWin.openUILinkIn(this.link, "tab");
1138 }
1139 aEvent.preventDefault();
1140 },
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;
1150 this.matchedExpander.removeEventListener("click", this.onMatchedToggle, false);
1151 this.matchedExpander = null;
1153 this.nameNode.removeEventListener("click", this.onFocus, false);
1154 this.nameNode = null;
1156 this.valueNode.removeEventListener("click", this.onFocus, false);
1157 this.valueNode = null;
1158 }
1159 };
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();
1172 this.updateSourceLink();
1173 }
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 ];
1184 SelectorView.CLASS_NAMES = [
1185 "parentmatch", "matched", "bestmatch"
1186 ];
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 }
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 },
1214 /**
1215 * A localized version of cssRule.status
1216 */
1217 get statusText()
1218 {
1219 return SelectorView.STATUS_NAMES[this.selectorInfo.status];
1220 },
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 },
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 },
1240 get sourceText()
1241 {
1242 return this.selectorInfo.sourceText;
1243 },
1246 get value()
1247 {
1248 return this.selectorInfo.value;
1249 },
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 },
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 },
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;
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 }
1301 let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
1303 if (showOrig && rule.type != ELEMENT_STYLE) {
1304 let deferred = promise.defer();
1306 // set as this first so we show something while we're fetching
1307 this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line;
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 });
1315 return deferred.promise;
1316 }
1318 let oldSource = this.source;
1319 this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line;
1320 return promise.resolve(oldSource);
1321 },
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 },
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;
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 }
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 }
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 };
1387 exports.CssHtmlTree = CssHtmlTree;
1388 exports.PropertyView = PropertyView;