rules.js 55 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673
  1. /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. "use strict";
  6. const promise = require("promise");
  7. const Services = require("Services");
  8. const {Task} = require("devtools/shared/task");
  9. const {Tools} = require("devtools/client/definitions");
  10. const {l10n} = require("devtools/shared/inspector/css-logic");
  11. const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
  12. const {OutputParser} = require("devtools/client/shared/output-parser");
  13. const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils");
  14. const {ElementStyle} = require("devtools/client/inspector/rules/models/element-style");
  15. const {Rule} = require("devtools/client/inspector/rules/models/rule");
  16. const {RuleEditor} = require("devtools/client/inspector/rules/views/rule-editor");
  17. const {gDevTools} = require("devtools/client/framework/devtools");
  18. const {getCssProperties} = require("devtools/shared/fronts/css-properties");
  19. const HighlightersOverlay = require("devtools/client/inspector/shared/highlighters-overlay");
  20. const {
  21. VIEW_NODE_SELECTOR_TYPE,
  22. VIEW_NODE_PROPERTY_TYPE,
  23. VIEW_NODE_VALUE_TYPE,
  24. VIEW_NODE_IMAGE_URL_TYPE,
  25. VIEW_NODE_LOCATION_TYPE,
  26. } = require("devtools/client/inspector/shared/node-types");
  27. const StyleInspectorMenu = require("devtools/client/inspector/shared/style-inspector-menu");
  28. const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay");
  29. const {createChild, promiseWarn, throttle} = require("devtools/client/inspector/shared/utils");
  30. const EventEmitter = require("devtools/shared/event-emitter");
  31. const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
  32. const clipboardHelper = require("devtools/shared/platform/clipboard");
  33. const {AutocompletePopup} = require("devtools/client/shared/autocomplete-popup");
  34. const HTML_NS = "http://www.w3.org/1999/xhtml";
  35. const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
  36. const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit";
  37. const PREF_ENABLE_MDN_DOCS_TOOLTIP =
  38. "devtools.inspector.mdnDocsTooltip.enabled";
  39. const FILTER_CHANGED_TIMEOUT = 150;
  40. // This is used to parse user input when filtering.
  41. const FILTER_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*;?$/;
  42. // This is used to parse the filter search value to see if the filter
  43. // should be strict or not
  44. const FILTER_STRICT_RE = /\s*`(.*?)`\s*$/;
  45. /**
  46. * Our model looks like this:
  47. *
  48. * ElementStyle:
  49. * Responsible for keeping track of which properties are overridden.
  50. * Maintains a list of Rule objects that apply to the element.
  51. * Rule:
  52. * Manages a single style declaration or rule.
  53. * Responsible for applying changes to the properties in a rule.
  54. * Maintains a list of TextProperty objects.
  55. * TextProperty:
  56. * Manages a single property from the authoredText attribute of the
  57. * relevant declaration.
  58. * Maintains a list of computed properties that come from this
  59. * property declaration.
  60. * Changes to the TextProperty are sent to its related Rule for
  61. * application.
  62. *
  63. * View hierarchy mostly follows the model hierarchy.
  64. *
  65. * CssRuleView:
  66. * Owns an ElementStyle and creates a list of RuleEditors for its
  67. * Rules.
  68. * RuleEditor:
  69. * Owns a Rule object and creates a list of TextPropertyEditors
  70. * for its TextProperties.
  71. * Manages creation of new text properties.
  72. * TextPropertyEditor:
  73. * Owns a TextProperty object.
  74. * Manages changes to the TextProperty.
  75. * Can be expanded to display computed properties.
  76. * Can mark a property disabled or enabled.
  77. */
  78. /**
  79. * CssRuleView is a view of the style rules and declarations that
  80. * apply to a given element. After construction, the 'element'
  81. * property will be available with the user interface.
  82. *
  83. * @param {Inspector} inspector
  84. * Inspector toolbox panel
  85. * @param {Document} document
  86. * The document that will contain the rule view.
  87. * @param {Object} store
  88. * The CSS rule view can use this object to store metadata
  89. * that might outlast the rule view, particularly the current
  90. * set of disabled properties.
  91. * @param {PageStyleFront} pageStyle
  92. * The PageStyleFront for communicating with the remote server.
  93. */
  94. function CssRuleView(inspector, document, store, pageStyle) {
  95. this.inspector = inspector;
  96. this.styleDocument = document;
  97. this.styleWindow = this.styleDocument.defaultView;
  98. this.store = store || {};
  99. this.pageStyle = pageStyle;
  100. // Allow tests to override throttling behavior, as this can cause intermittents.
  101. this.throttle = throttle;
  102. this.cssProperties = getCssProperties(inspector.toolbox);
  103. this._outputParser = new OutputParser(document, this.cssProperties);
  104. this._onAddRule = this._onAddRule.bind(this);
  105. this._onContextMenu = this._onContextMenu.bind(this);
  106. this._onCopy = this._onCopy.bind(this);
  107. this._onFilterStyles = this._onFilterStyles.bind(this);
  108. this._onClearSearch = this._onClearSearch.bind(this);
  109. this._onTogglePseudoClassPanel = this._onTogglePseudoClassPanel.bind(this);
  110. this._onTogglePseudoClass = this._onTogglePseudoClass.bind(this);
  111. let doc = this.styleDocument;
  112. this.element = doc.getElementById("ruleview-container-focusable");
  113. this.addRuleButton = doc.getElementById("ruleview-add-rule-button");
  114. this.searchField = doc.getElementById("ruleview-searchbox");
  115. this.searchClearButton = doc.getElementById("ruleview-searchinput-clear");
  116. this.pseudoClassPanel = doc.getElementById("pseudo-class-panel");
  117. this.pseudoClassToggle = doc.getElementById("pseudo-class-panel-toggle");
  118. this.hoverCheckbox = doc.getElementById("pseudo-hover-toggle");
  119. this.activeCheckbox = doc.getElementById("pseudo-active-toggle");
  120. this.focusCheckbox = doc.getElementById("pseudo-focus-toggle");
  121. this.searchClearButton.hidden = true;
  122. this.shortcuts = new KeyShortcuts({ window: this.styleWindow });
  123. this._onShortcut = this._onShortcut.bind(this);
  124. this.shortcuts.on("Escape", this._onShortcut);
  125. this.shortcuts.on("Return", this._onShortcut);
  126. this.shortcuts.on("Space", this._onShortcut);
  127. this.shortcuts.on("CmdOrCtrl+F", this._onShortcut);
  128. this.element.addEventListener("copy", this._onCopy);
  129. this.element.addEventListener("contextmenu", this._onContextMenu);
  130. this.addRuleButton.addEventListener("click", this._onAddRule);
  131. this.searchField.addEventListener("input", this._onFilterStyles);
  132. this.searchField.addEventListener("contextmenu", this.inspector.onTextBoxContextMenu);
  133. this.searchClearButton.addEventListener("click", this._onClearSearch);
  134. this.pseudoClassToggle.addEventListener("click",
  135. this._onTogglePseudoClassPanel);
  136. this.hoverCheckbox.addEventListener("click", this._onTogglePseudoClass);
  137. this.activeCheckbox.addEventListener("click", this._onTogglePseudoClass);
  138. this.focusCheckbox.addEventListener("click", this._onTogglePseudoClass);
  139. this._handlePrefChange = this._handlePrefChange.bind(this);
  140. this._onSourcePrefChanged = this._onSourcePrefChanged.bind(this);
  141. this._prefObserver = new PrefObserver("devtools.");
  142. this._prefObserver.on(PREF_ORIG_SOURCES, this._onSourcePrefChanged);
  143. this._prefObserver.on(PREF_UA_STYLES, this._handlePrefChange);
  144. this._prefObserver.on(PREF_DEFAULT_COLOR_UNIT, this._handlePrefChange);
  145. this._prefObserver.on(PREF_ENABLE_MDN_DOCS_TOOLTIP, this._handlePrefChange);
  146. this.showUserAgentStyles = Services.prefs.getBoolPref(PREF_UA_STYLES);
  147. this.enableMdnDocsTooltip =
  148. Services.prefs.getBoolPref(PREF_ENABLE_MDN_DOCS_TOOLTIP);
  149. // The popup will be attached to the toolbox document.
  150. this.popup = new AutocompletePopup(inspector._toolbox.doc, {
  151. autoSelect: true,
  152. theme: "auto"
  153. });
  154. this._showEmpty();
  155. this._contextmenu = new StyleInspectorMenu(this, { isRuleView: true });
  156. // Add the tooltips and highlighters to the view
  157. this.tooltips = new TooltipsOverlay(this);
  158. this.tooltips.addToView();
  159. this.highlighters = new HighlightersOverlay(this);
  160. this.highlighters.addToView();
  161. EventEmitter.decorate(this);
  162. }
  163. CssRuleView.prototype = {
  164. // The element that we're inspecting.
  165. _viewedElement: null,
  166. // Used for cancelling timeouts in the style filter.
  167. _filterChangedTimeout: null,
  168. // Empty, unconnected element of the same type as this node, used
  169. // to figure out how shorthand properties will be parsed.
  170. _dummyElement: null,
  171. // Get the dummy elemenet.
  172. get dummyElement() {
  173. return this._dummyElement;
  174. },
  175. // Get the filter search value.
  176. get searchValue() {
  177. return this.searchField.value.toLowerCase();
  178. },
  179. /**
  180. * Get an instance of SelectorHighlighter (used to highlight nodes that match
  181. * selectors in the rule-view). A new instance is only created the first time
  182. * this function is called. The same instance will then be returned.
  183. *
  184. * @return {Promise} Resolves to the instance of the highlighter.
  185. */
  186. getSelectorHighlighter: Task.async(function* () {
  187. let utils = this.inspector.toolbox.highlighterUtils;
  188. if (!utils.supportsCustomHighlighters()) {
  189. return null;
  190. }
  191. if (this.selectorHighlighter) {
  192. return this.selectorHighlighter;
  193. }
  194. try {
  195. let h = yield utils.getHighlighterByType("SelectorHighlighter");
  196. this.selectorHighlighter = h;
  197. return h;
  198. } catch (e) {
  199. // The SelectorHighlighter type could not be created in the
  200. // current target. It could be an older server, or a XUL page.
  201. return null;
  202. }
  203. }),
  204. /**
  205. * Highlight/unhighlight all the nodes that match a given set of selectors
  206. * inside the document of the current selected node.
  207. * Only one selector can be highlighted at a time, so calling the method a
  208. * second time with a different selector will first unhighlight the previously
  209. * highlighted nodes.
  210. * Calling the method a second time with the same selector will just
  211. * unhighlight the highlighted nodes.
  212. *
  213. * @param {DOMNode} selectorIcon
  214. * The icon that was clicked to toggle the selector. The
  215. * class 'highlighted' will be added when the selector is
  216. * highlighted.
  217. * @param {String} selector
  218. * The selector used to find nodes in the page.
  219. */
  220. toggleSelectorHighlighter: function (selectorIcon, selector) {
  221. if (this.lastSelectorIcon) {
  222. this.lastSelectorIcon.classList.remove("highlighted");
  223. }
  224. selectorIcon.classList.remove("highlighted");
  225. this.unhighlightSelector().then(() => {
  226. if (selector !== this.highlighters.selectorHighlighterShown) {
  227. this.highlighters.selectorHighlighterShown = selector;
  228. selectorIcon.classList.add("highlighted");
  229. this.lastSelectorIcon = selectorIcon;
  230. this.highlightSelector(selector).then(() => {
  231. this.emit("ruleview-selectorhighlighter-toggled", true);
  232. }, e => console.error(e));
  233. } else {
  234. this.highlighters.selectorHighlighterShown = null;
  235. this.emit("ruleview-selectorhighlighter-toggled", false);
  236. }
  237. }, e => console.error(e));
  238. },
  239. highlightSelector: Task.async(function* (selector) {
  240. let node = this.inspector.selection.nodeFront;
  241. let highlighter = yield this.getSelectorHighlighter();
  242. if (!highlighter) {
  243. return;
  244. }
  245. yield highlighter.show(node, {
  246. hideInfoBar: true,
  247. hideGuides: true,
  248. selector
  249. });
  250. }),
  251. unhighlightSelector: Task.async(function* () {
  252. let highlighter = yield this.getSelectorHighlighter();
  253. if (!highlighter) {
  254. return;
  255. }
  256. yield highlighter.hide();
  257. }),
  258. /**
  259. * Get the type of a given node in the rule-view
  260. *
  261. * @param {DOMNode} node
  262. * The node which we want information about
  263. * @return {Object} The type information object contains the following props:
  264. * - type {String} One of the VIEW_NODE_XXX_TYPE const in
  265. * client/inspector/shared/node-types
  266. * - value {Object} Depends on the type of the node
  267. * returns null of the node isn't anything we care about
  268. */
  269. getNodeInfo: function (node) {
  270. if (!node) {
  271. return null;
  272. }
  273. let type, value;
  274. let classes = node.classList;
  275. let prop = getParentTextProperty(node);
  276. if (classes.contains("ruleview-propertyname") && prop) {
  277. type = VIEW_NODE_PROPERTY_TYPE;
  278. value = {
  279. property: node.textContent,
  280. value: getPropertyNameAndValue(node).value,
  281. enabled: prop.enabled,
  282. overridden: prop.overridden,
  283. pseudoElement: prop.rule.pseudoElement,
  284. sheetHref: prop.rule.domRule.href,
  285. textProperty: prop
  286. };
  287. } else if (classes.contains("ruleview-propertyvalue") && prop) {
  288. type = VIEW_NODE_VALUE_TYPE;
  289. value = {
  290. property: getPropertyNameAndValue(node).name,
  291. value: node.textContent,
  292. enabled: prop.enabled,
  293. overridden: prop.overridden,
  294. pseudoElement: prop.rule.pseudoElement,
  295. sheetHref: prop.rule.domRule.href,
  296. textProperty: prop
  297. };
  298. } else if (classes.contains("theme-link") &&
  299. !classes.contains("ruleview-rule-source") && prop) {
  300. type = VIEW_NODE_IMAGE_URL_TYPE;
  301. value = {
  302. property: getPropertyNameAndValue(node).name,
  303. value: node.parentNode.textContent,
  304. url: node.href,
  305. enabled: prop.enabled,
  306. overridden: prop.overridden,
  307. pseudoElement: prop.rule.pseudoElement,
  308. sheetHref: prop.rule.domRule.href,
  309. textProperty: prop
  310. };
  311. } else if (classes.contains("ruleview-selector-unmatched") ||
  312. classes.contains("ruleview-selector-matched") ||
  313. classes.contains("ruleview-selectorcontainer") ||
  314. classes.contains("ruleview-selector") ||
  315. classes.contains("ruleview-selector-attribute") ||
  316. classes.contains("ruleview-selector-pseudo-class") ||
  317. classes.contains("ruleview-selector-pseudo-class-lock")) {
  318. type = VIEW_NODE_SELECTOR_TYPE;
  319. value = this._getRuleEditorForNode(node).selectorText.textContent;
  320. } else if (classes.contains("ruleview-rule-source") ||
  321. classes.contains("ruleview-rule-source-label")) {
  322. type = VIEW_NODE_LOCATION_TYPE;
  323. let rule = this._getRuleEditorForNode(node).rule;
  324. value = (rule.sheet && rule.sheet.href) ? rule.sheet.href : rule.title;
  325. } else {
  326. return null;
  327. }
  328. return {type, value};
  329. },
  330. /**
  331. * Retrieve the RuleEditor instance that should be stored on
  332. * the offset parent of the node
  333. */
  334. _getRuleEditorForNode: function (node) {
  335. if (!node.offsetParent) {
  336. // some nodes don't have an offsetParent, but their parentNode does
  337. node = node.parentNode;
  338. }
  339. return node.offsetParent._ruleEditor;
  340. },
  341. /**
  342. * Context menu handler.
  343. */
  344. _onContextMenu: function (event) {
  345. this._contextmenu.show(event);
  346. },
  347. /**
  348. * Callback for copy event. Copy the selected text.
  349. *
  350. * @param {Event} event
  351. * copy event object.
  352. */
  353. _onCopy: function (event) {
  354. if (event) {
  355. this.copySelection(event.target);
  356. event.preventDefault();
  357. }
  358. },
  359. /**
  360. * Copy the current selection. The current target is necessary
  361. * if the selection is inside an input or a textarea
  362. *
  363. * @param {DOMNode} target
  364. * DOMNode target of the copy action
  365. */
  366. copySelection: function (target) {
  367. try {
  368. let text = "";
  369. let nodeName = target && target.nodeName;
  370. if (nodeName === "input" || nodeName == "textarea") {
  371. let start = Math.min(target.selectionStart, target.selectionEnd);
  372. let end = Math.max(target.selectionStart, target.selectionEnd);
  373. let count = end - start;
  374. text = target.value.substr(start, count);
  375. } else {
  376. text = this.styleWindow.getSelection().toString();
  377. // Remove any double newlines.
  378. text = text.replace(/(\r?\n)\r?\n/g, "$1");
  379. }
  380. clipboardHelper.copyString(text);
  381. } catch (e) {
  382. console.error(e);
  383. }
  384. },
  385. /**
  386. * A helper for _onAddRule that handles the case where the actor
  387. * does not support as-authored styles.
  388. */
  389. _onAddNewRuleNonAuthored: function () {
  390. let elementStyle = this._elementStyle;
  391. let element = elementStyle.element;
  392. let rules = elementStyle.rules;
  393. let pseudoClasses = element.pseudoClassLocks;
  394. this.pageStyle.addNewRule(element, pseudoClasses).then(options => {
  395. let newRule = new Rule(elementStyle, options);
  396. rules.push(newRule);
  397. let editor = new RuleEditor(this, newRule);
  398. newRule.editor = editor;
  399. // Insert the new rule editor after the inline element rule
  400. if (rules.length <= 1) {
  401. this.element.appendChild(editor.element);
  402. } else {
  403. for (let rule of rules) {
  404. if (rule.domRule.type === ELEMENT_STYLE) {
  405. let referenceElement = rule.editor.element.nextSibling;
  406. this.element.insertBefore(editor.element, referenceElement);
  407. break;
  408. }
  409. }
  410. }
  411. // Focus and make the new rule's selector editable
  412. editor.selectorText.click();
  413. elementStyle._changed();
  414. });
  415. },
  416. /**
  417. * Add a new rule to the current element.
  418. */
  419. _onAddRule: function () {
  420. let elementStyle = this._elementStyle;
  421. let element = elementStyle.element;
  422. let client = this.inspector.target.client;
  423. let pseudoClasses = element.pseudoClassLocks;
  424. if (!client.traits.addNewRule) {
  425. return;
  426. }
  427. if (!this.pageStyle.supportsAuthoredStyles) {
  428. // We're talking to an old server.
  429. this._onAddNewRuleNonAuthored();
  430. return;
  431. }
  432. // Adding a new rule with authored styles will cause the actor to
  433. // emit an event, which will in turn cause the rule view to be
  434. // updated. So, we wait for this update and for the rule creation
  435. // request to complete, and then focus the new rule's selector.
  436. let eventPromise = this.once("ruleview-refreshed");
  437. let newRulePromise = this.pageStyle.addNewRule(element, pseudoClasses);
  438. promise.all([eventPromise, newRulePromise]).then((values) => {
  439. let options = values[1];
  440. // Be sure the reference the correct |rules| here.
  441. for (let rule of this._elementStyle.rules) {
  442. if (options.rule === rule.domRule) {
  443. rule.editor.selectorText.click();
  444. elementStyle._changed();
  445. break;
  446. }
  447. }
  448. });
  449. },
  450. /**
  451. * Disables add rule button when needed
  452. */
  453. refreshAddRuleButtonState: function () {
  454. let shouldBeDisabled = !this._viewedElement ||
  455. !this.inspector.selection.isElementNode() ||
  456. this.inspector.selection.isAnonymousNode();
  457. this.addRuleButton.disabled = shouldBeDisabled;
  458. },
  459. setPageStyle: function (pageStyle) {
  460. this.pageStyle = pageStyle;
  461. },
  462. /**
  463. * Return {Boolean} true if the rule view currently has an input
  464. * editor visible.
  465. */
  466. get isEditing() {
  467. return this.tooltips.isEditing ||
  468. this.element.querySelectorAll(".styleinspector-propertyeditor")
  469. .length > 0;
  470. },
  471. _handlePrefChange: function (pref) {
  472. if (pref === PREF_UA_STYLES) {
  473. this.showUserAgentStyles = Services.prefs.getBoolPref(pref);
  474. }
  475. // Reselect the currently selected element
  476. let refreshOnPrefs = [PREF_UA_STYLES, PREF_DEFAULT_COLOR_UNIT];
  477. if (refreshOnPrefs.indexOf(pref) > -1) {
  478. this.selectElement(this._viewedElement, true);
  479. }
  480. },
  481. /**
  482. * Update source links when pref for showing original sources changes
  483. */
  484. _onSourcePrefChanged: function () {
  485. if (this._elementStyle && this._elementStyle.rules) {
  486. for (let rule of this._elementStyle.rules) {
  487. if (rule.editor) {
  488. rule.editor.updateSourceLink();
  489. }
  490. }
  491. this.inspector.emit("rule-view-sourcelinks-updated");
  492. }
  493. },
  494. /**
  495. * Set the filter style search value.
  496. * @param {String} value
  497. * The search value.
  498. */
  499. setFilterStyles: function (value = "") {
  500. this.searchField.value = value;
  501. this.searchField.focus();
  502. this._onFilterStyles();
  503. },
  504. /**
  505. * Called when the user enters a search term in the filter style search box.
  506. */
  507. _onFilterStyles: function () {
  508. if (this._filterChangedTimeout) {
  509. clearTimeout(this._filterChangedTimeout);
  510. }
  511. let filterTimeout = (this.searchValue.length > 0) ?
  512. FILTER_CHANGED_TIMEOUT : 0;
  513. this.searchClearButton.hidden = this.searchValue.length === 0;
  514. this._filterChangedTimeout = setTimeout(() => {
  515. if (this.searchField.value.length > 0) {
  516. this.searchField.setAttribute("filled", true);
  517. } else {
  518. this.searchField.removeAttribute("filled");
  519. }
  520. this.searchData = {
  521. searchPropertyMatch: FILTER_PROP_RE.exec(this.searchValue),
  522. searchPropertyName: this.searchValue,
  523. searchPropertyValue: this.searchValue,
  524. strictSearchValue: "",
  525. strictSearchPropertyName: false,
  526. strictSearchPropertyValue: false,
  527. strictSearchAllValues: false
  528. };
  529. if (this.searchData.searchPropertyMatch) {
  530. // Parse search value as a single property line and extract the
  531. // property name and value. If the parsed property name or value is
  532. // contained in backquotes (`), extract the value within the backquotes
  533. // and set the corresponding strict search for the property to true.
  534. if (FILTER_STRICT_RE.test(this.searchData.searchPropertyMatch[1])) {
  535. this.searchData.strictSearchPropertyName = true;
  536. this.searchData.searchPropertyName =
  537. FILTER_STRICT_RE.exec(this.searchData.searchPropertyMatch[1])[1];
  538. } else {
  539. this.searchData.searchPropertyName =
  540. this.searchData.searchPropertyMatch[1];
  541. }
  542. if (FILTER_STRICT_RE.test(this.searchData.searchPropertyMatch[2])) {
  543. this.searchData.strictSearchPropertyValue = true;
  544. this.searchData.searchPropertyValue =
  545. FILTER_STRICT_RE.exec(this.searchData.searchPropertyMatch[2])[1];
  546. } else {
  547. this.searchData.searchPropertyValue =
  548. this.searchData.searchPropertyMatch[2];
  549. }
  550. // Strict search for stylesheets will match the property line regex.
  551. // Extract the search value within the backquotes to be used
  552. // in the strict search for stylesheets in _highlightStyleSheet.
  553. if (FILTER_STRICT_RE.test(this.searchValue)) {
  554. this.searchData.strictSearchValue =
  555. FILTER_STRICT_RE.exec(this.searchValue)[1];
  556. }
  557. } else if (FILTER_STRICT_RE.test(this.searchValue)) {
  558. // If the search value does not correspond to a property line and
  559. // is contained in backquotes, extract the search value within the
  560. // backquotes and set the flag to perform a strict search for all
  561. // the values (selector, stylesheet, property and computed values).
  562. let searchValue = FILTER_STRICT_RE.exec(this.searchValue)[1];
  563. this.searchData.strictSearchAllValues = true;
  564. this.searchData.searchPropertyName = searchValue;
  565. this.searchData.searchPropertyValue = searchValue;
  566. this.searchData.strictSearchValue = searchValue;
  567. }
  568. this._clearHighlight(this.element);
  569. this._clearRules();
  570. this._createEditors();
  571. this.inspector.emit("ruleview-filtered");
  572. this._filterChangeTimeout = null;
  573. }, filterTimeout);
  574. },
  575. /**
  576. * Called when the user clicks on the clear button in the filter style search
  577. * box. Returns true if the search box is cleared and false otherwise.
  578. */
  579. _onClearSearch: function () {
  580. if (this.searchField.value) {
  581. this.setFilterStyles("");
  582. return true;
  583. }
  584. return false;
  585. },
  586. destroy: function () {
  587. this.isDestroyed = true;
  588. this.clear();
  589. this._dummyElement = null;
  590. this._prefObserver.off(PREF_ORIG_SOURCES, this._onSourcePrefChanged);
  591. this._prefObserver.off(PREF_UA_STYLES, this._handlePrefChange);
  592. this._prefObserver.off(PREF_DEFAULT_COLOR_UNIT, this._handlePrefChange);
  593. this._prefObserver.destroy();
  594. this._outputParser = null;
  595. // Remove context menu
  596. if (this._contextmenu) {
  597. this._contextmenu.destroy();
  598. this._contextmenu = null;
  599. }
  600. this.tooltips.destroy();
  601. this.highlighters.destroy();
  602. // Remove bound listeners
  603. this.shortcuts.destroy();
  604. this.element.removeEventListener("copy", this._onCopy);
  605. this.element.removeEventListener("contextmenu", this._onContextMenu);
  606. this.addRuleButton.removeEventListener("click", this._onAddRule);
  607. this.searchField.removeEventListener("input", this._onFilterStyles);
  608. this.searchField.removeEventListener("contextmenu",
  609. this.inspector.onTextBoxContextMenu);
  610. this.searchClearButton.removeEventListener("click", this._onClearSearch);
  611. this.pseudoClassToggle.removeEventListener("click",
  612. this._onTogglePseudoClassPanel);
  613. this.hoverCheckbox.removeEventListener("click", this._onTogglePseudoClass);
  614. this.activeCheckbox.removeEventListener("click", this._onTogglePseudoClass);
  615. this.focusCheckbox.removeEventListener("click", this._onTogglePseudoClass);
  616. this.searchField = null;
  617. this.searchClearButton = null;
  618. this.pseudoClassPanel = null;
  619. this.pseudoClassToggle = null;
  620. this.hoverCheckbox = null;
  621. this.activeCheckbox = null;
  622. this.focusCheckbox = null;
  623. this.inspector = null;
  624. this.styleDocument = null;
  625. this.styleWindow = null;
  626. if (this.element.parentNode) {
  627. this.element.parentNode.removeChild(this.element);
  628. }
  629. if (this._elementStyle) {
  630. this._elementStyle.destroy();
  631. }
  632. this.popup.destroy();
  633. },
  634. /**
  635. * Mark the view as selecting an element, disabling all interaction, and
  636. * visually clearing the view after a few milliseconds to avoid confusion
  637. * about which element's styles the rule view shows.
  638. */
  639. _startSelectingElement: function () {
  640. this.element.classList.add("non-interactive");
  641. },
  642. /**
  643. * Mark the view as no longer selecting an element, re-enabling interaction.
  644. */
  645. _stopSelectingElement: function () {
  646. this.element.classList.remove("non-interactive");
  647. },
  648. /**
  649. * Update the view with a new selected element.
  650. *
  651. * @param {NodeActor} element
  652. * The node whose style rules we'll inspect.
  653. * @param {Boolean} allowRefresh
  654. * Update the view even if the element is the same as last time.
  655. */
  656. selectElement: function (element, allowRefresh = false) {
  657. let refresh = (this._viewedElement === element);
  658. if (refresh && !allowRefresh) {
  659. return promise.resolve(undefined);
  660. }
  661. if (this.popup.isOpen) {
  662. this.popup.hidePopup();
  663. }
  664. this.clear(false);
  665. this._viewedElement = element;
  666. this.clearPseudoClassPanel();
  667. this.refreshAddRuleButtonState();
  668. if (!this._viewedElement) {
  669. this._stopSelectingElement();
  670. this._clearRules();
  671. this._showEmpty();
  672. this.refreshPseudoClassPanel();
  673. return promise.resolve(undefined);
  674. }
  675. // To figure out how shorthand properties are interpreted by the
  676. // engine, we will set properties on a dummy element and observe
  677. // how their .style attribute reflects them as computed values.
  678. let dummyElementPromise = promise.resolve(this.styleDocument).then(document => {
  679. // ::before and ::after do not have a namespaceURI
  680. let namespaceURI = this.element.namespaceURI ||
  681. document.documentElement.namespaceURI;
  682. this._dummyElement = document.createElementNS(namespaceURI,
  683. this.element.tagName);
  684. }).then(null, promiseWarn);
  685. let elementStyle = new ElementStyle(element, this, this.store,
  686. this.pageStyle, this.showUserAgentStyles);
  687. this._elementStyle = elementStyle;
  688. this._startSelectingElement();
  689. return dummyElementPromise.then(() => {
  690. if (this._elementStyle === elementStyle) {
  691. return this._populate();
  692. }
  693. return undefined;
  694. }).then(() => {
  695. if (this._elementStyle === elementStyle) {
  696. if (!refresh) {
  697. this.element.scrollTop = 0;
  698. }
  699. this._stopSelectingElement();
  700. this._elementStyle.onChanged = () => {
  701. this._changed();
  702. };
  703. }
  704. }).then(null, e => {
  705. if (this._elementStyle === elementStyle) {
  706. this._stopSelectingElement();
  707. this._clearRules();
  708. }
  709. console.error(e);
  710. });
  711. },
  712. /**
  713. * Update the rules for the currently highlighted element.
  714. */
  715. refreshPanel: function () {
  716. // Ignore refreshes during editing or when no element is selected.
  717. if (this.isEditing || !this._elementStyle) {
  718. return promise.resolve(undefined);
  719. }
  720. // Repopulate the element style once the current modifications are done.
  721. let promises = [];
  722. for (let rule of this._elementStyle.rules) {
  723. if (rule._applyingModifications) {
  724. promises.push(rule._applyingModifications);
  725. }
  726. }
  727. return promise.all(promises).then(() => {
  728. return this._populate();
  729. });
  730. },
  731. /**
  732. * Clear the pseudo class options panel by removing the checked and disabled
  733. * attributes for each checkbox.
  734. */
  735. clearPseudoClassPanel: function () {
  736. this.hoverCheckbox.checked = this.hoverCheckbox.disabled = false;
  737. this.activeCheckbox.checked = this.activeCheckbox.disabled = false;
  738. this.focusCheckbox.checked = this.focusCheckbox.disabled = false;
  739. },
  740. /**
  741. * Update the pseudo class options for the currently highlighted element.
  742. */
  743. refreshPseudoClassPanel: function () {
  744. if (!this._elementStyle || !this.inspector.selection.isElementNode()) {
  745. this.hoverCheckbox.disabled = true;
  746. this.activeCheckbox.disabled = true;
  747. this.focusCheckbox.disabled = true;
  748. return;
  749. }
  750. for (let pseudoClassLock of this._elementStyle.element.pseudoClassLocks) {
  751. switch (pseudoClassLock) {
  752. case ":hover": {
  753. this.hoverCheckbox.checked = true;
  754. break;
  755. }
  756. case ":active": {
  757. this.activeCheckbox.checked = true;
  758. break;
  759. }
  760. case ":focus": {
  761. this.focusCheckbox.checked = true;
  762. break;
  763. }
  764. }
  765. }
  766. },
  767. _populate: function () {
  768. let elementStyle = this._elementStyle;
  769. return this._elementStyle.populate().then(() => {
  770. if (this._elementStyle !== elementStyle || this.isDestroyed) {
  771. return null;
  772. }
  773. this._clearRules();
  774. let onEditorsReady = this._createEditors();
  775. this.refreshPseudoClassPanel();
  776. // Notify anyone that cares that we refreshed.
  777. return onEditorsReady.then(() => {
  778. this.emit("ruleview-refreshed");
  779. }, e => console.error(e));
  780. }).then(null, promiseWarn);
  781. },
  782. /**
  783. * Show the user that the rule view has no node selected.
  784. */
  785. _showEmpty: function () {
  786. if (this.styleDocument.getElementById("ruleview-no-results")) {
  787. return;
  788. }
  789. createChild(this.element, "div", {
  790. id: "ruleview-no-results",
  791. textContent: l10n("rule.empty")
  792. });
  793. },
  794. /**
  795. * Clear the rules.
  796. */
  797. _clearRules: function () {
  798. this.element.innerHTML = "";
  799. },
  800. /**
  801. * Clear the rule view.
  802. */
  803. clear: function (clearDom = true) {
  804. this.lastSelectorIcon = null;
  805. if (clearDom) {
  806. this._clearRules();
  807. }
  808. this._viewedElement = null;
  809. if (this._elementStyle) {
  810. this._elementStyle.destroy();
  811. this._elementStyle = null;
  812. }
  813. },
  814. /**
  815. * Called when the user has made changes to the ElementStyle.
  816. * Emits an event that clients can listen to.
  817. */
  818. _changed: function () {
  819. this.emit("ruleview-changed");
  820. },
  821. /**
  822. * Text for header that shows above rules for this element
  823. */
  824. get selectedElementLabel() {
  825. if (this._selectedElementLabel) {
  826. return this._selectedElementLabel;
  827. }
  828. this._selectedElementLabel = l10n("rule.selectedElement");
  829. return this._selectedElementLabel;
  830. },
  831. /**
  832. * Text for header that shows above rules for pseudo elements
  833. */
  834. get pseudoElementLabel() {
  835. if (this._pseudoElementLabel) {
  836. return this._pseudoElementLabel;
  837. }
  838. this._pseudoElementLabel = l10n("rule.pseudoElement");
  839. return this._pseudoElementLabel;
  840. },
  841. get showPseudoElements() {
  842. if (this._showPseudoElements === undefined) {
  843. this._showPseudoElements =
  844. Services.prefs.getBoolPref("devtools.inspector.show_pseudo_elements");
  845. }
  846. return this._showPseudoElements;
  847. },
  848. /**
  849. * Creates an expandable container in the rule view
  850. *
  851. * @param {String} label
  852. * The label for the container header
  853. * @param {Boolean} isPseudo
  854. * Whether or not the container will hold pseudo element rules
  855. * @return {DOMNode} The container element
  856. */
  857. createExpandableContainer: function (label, isPseudo = false) {
  858. let header = this.styleDocument.createElementNS(HTML_NS, "div");
  859. header.className = this._getRuleViewHeaderClassName(true);
  860. header.textContent = label;
  861. let twisty = this.styleDocument.createElementNS(HTML_NS, "span");
  862. twisty.className = "ruleview-expander theme-twisty";
  863. twisty.setAttribute("open", "true");
  864. header.insertBefore(twisty, header.firstChild);
  865. this.element.appendChild(header);
  866. let container = this.styleDocument.createElementNS(HTML_NS, "div");
  867. container.classList.add("ruleview-expandable-container");
  868. container.hidden = false;
  869. this.element.appendChild(container);
  870. header.addEventListener("dblclick", () => {
  871. this._toggleContainerVisibility(twisty, container, isPseudo,
  872. !this.showPseudoElements);
  873. }, false);
  874. twisty.addEventListener("click", () => {
  875. this._toggleContainerVisibility(twisty, container, isPseudo,
  876. !this.showPseudoElements);
  877. }, false);
  878. if (isPseudo) {
  879. this._toggleContainerVisibility(twisty, container, isPseudo,
  880. this.showPseudoElements);
  881. }
  882. return container;
  883. },
  884. /**
  885. * Toggle the visibility of an expandable container
  886. *
  887. * @param {DOMNode} twisty
  888. * Clickable toggle DOM Node
  889. * @param {DOMNode} container
  890. * Expandable container DOM Node
  891. * @param {Boolean} isPseudo
  892. * Whether or not the container will hold pseudo element rules
  893. * @param {Boolean} showPseudo
  894. * Whether or not pseudo element rules should be displayed
  895. */
  896. _toggleContainerVisibility: function (twisty, container, isPseudo,
  897. showPseudo) {
  898. let isOpen = twisty.getAttribute("open");
  899. if (isPseudo) {
  900. this._showPseudoElements = !!showPseudo;
  901. Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements",
  902. this.showPseudoElements);
  903. container.hidden = !this.showPseudoElements;
  904. isOpen = !this.showPseudoElements;
  905. } else {
  906. container.hidden = !container.hidden;
  907. }
  908. if (isOpen) {
  909. twisty.removeAttribute("open");
  910. } else {
  911. twisty.setAttribute("open", "true");
  912. }
  913. },
  914. _getRuleViewHeaderClassName: function (isPseudo) {
  915. let baseClassName = "theme-gutter ruleview-header";
  916. return isPseudo ? baseClassName + " ruleview-expandable-header" :
  917. baseClassName;
  918. },
  919. /**
  920. * Creates editor UI for each of the rules in _elementStyle.
  921. */
  922. _createEditors: function () {
  923. // Run through the current list of rules, attaching
  924. // their editors in order. Create editors if needed.
  925. let lastInheritedSource = "";
  926. let lastKeyframes = null;
  927. let seenPseudoElement = false;
  928. let seenNormalElement = false;
  929. let seenSearchTerm = false;
  930. let container = null;
  931. if (!this._elementStyle.rules) {
  932. return promise.resolve();
  933. }
  934. let editorReadyPromises = [];
  935. for (let rule of this._elementStyle.rules) {
  936. if (rule.domRule.system) {
  937. continue;
  938. }
  939. // Initialize rule editor
  940. if (!rule.editor) {
  941. rule.editor = new RuleEditor(this, rule);
  942. editorReadyPromises.push(rule.editor.once("source-link-updated"));
  943. }
  944. // Filter the rules and highlight any matches if there is a search input
  945. if (this.searchValue && this.searchData) {
  946. if (this.highlightRule(rule)) {
  947. seenSearchTerm = true;
  948. } else if (rule.domRule.type !== ELEMENT_STYLE) {
  949. continue;
  950. }
  951. }
  952. // Only print header for this element if there are pseudo elements
  953. if (seenPseudoElement && !seenNormalElement && !rule.pseudoElement) {
  954. seenNormalElement = true;
  955. let div = this.styleDocument.createElementNS(HTML_NS, "div");
  956. div.className = this._getRuleViewHeaderClassName();
  957. div.textContent = this.selectedElementLabel;
  958. this.element.appendChild(div);
  959. }
  960. let inheritedSource = rule.inheritedSource;
  961. if (inheritedSource && inheritedSource !== lastInheritedSource) {
  962. let div = this.styleDocument.createElementNS(HTML_NS, "div");
  963. div.className = this._getRuleViewHeaderClassName();
  964. div.textContent = inheritedSource;
  965. lastInheritedSource = inheritedSource;
  966. this.element.appendChild(div);
  967. }
  968. if (!seenPseudoElement && rule.pseudoElement) {
  969. seenPseudoElement = true;
  970. container = this.createExpandableContainer(this.pseudoElementLabel,
  971. true);
  972. }
  973. let keyframes = rule.keyframes;
  974. if (keyframes && keyframes !== lastKeyframes) {
  975. lastKeyframes = keyframes;
  976. container = this.createExpandableContainer(rule.keyframesName);
  977. }
  978. if (container && (rule.pseudoElement || keyframes)) {
  979. container.appendChild(rule.editor.element);
  980. } else {
  981. this.element.appendChild(rule.editor.element);
  982. }
  983. }
  984. if (this.searchValue && !seenSearchTerm) {
  985. this.searchField.classList.add("devtools-style-searchbox-no-match");
  986. } else {
  987. this.searchField.classList.remove("devtools-style-searchbox-no-match");
  988. }
  989. return promise.all(editorReadyPromises);
  990. },
  991. /**
  992. * Highlight rules that matches the filter search value and returns a
  993. * boolean indicating whether or not rules were highlighted.
  994. *
  995. * @param {Rule} rule
  996. * The rule object we're highlighting if its rule selectors or
  997. * property values match the search value.
  998. * @return {Boolean} true if the rule was highlighted, false otherwise.
  999. */
  1000. highlightRule: function (rule) {
  1001. let isRuleSelectorHighlighted = this._highlightRuleSelector(rule);
  1002. let isStyleSheetHighlighted = this._highlightStyleSheet(rule);
  1003. let isHighlighted = isRuleSelectorHighlighted || isStyleSheetHighlighted;
  1004. // Highlight search matches in the rule properties
  1005. for (let textProp of rule.textProps) {
  1006. if (!textProp.invisible && this._highlightProperty(textProp.editor)) {
  1007. isHighlighted = true;
  1008. }
  1009. }
  1010. return isHighlighted;
  1011. },
  1012. /**
  1013. * Highlights the rule selector that matches the filter search value and
  1014. * returns a boolean indicating whether or not the selector was highlighted.
  1015. *
  1016. * @param {Rule} rule
  1017. * The Rule object.
  1018. * @return {Boolean} true if the rule selector was highlighted,
  1019. * false otherwise.
  1020. */
  1021. _highlightRuleSelector: function (rule) {
  1022. let isSelectorHighlighted = false;
  1023. let selectorNodes = [...rule.editor.selectorText.childNodes];
  1024. if (rule.domRule.type === CSSRule.KEYFRAME_RULE) {
  1025. selectorNodes = [rule.editor.selectorText];
  1026. } else if (rule.domRule.type === ELEMENT_STYLE) {
  1027. selectorNodes = [];
  1028. }
  1029. // Highlight search matches in the rule selectors
  1030. for (let selectorNode of selectorNodes) {
  1031. let selector = selectorNode.textContent.toLowerCase();
  1032. if ((this.searchData.strictSearchAllValues &&
  1033. selector === this.searchData.strictSearchValue) ||
  1034. (!this.searchData.strictSearchAllValues &&
  1035. selector.includes(this.searchValue))) {
  1036. selectorNode.classList.add("ruleview-highlight");
  1037. isSelectorHighlighted = true;
  1038. }
  1039. }
  1040. return isSelectorHighlighted;
  1041. },
  1042. /**
  1043. * Highlights the stylesheet source that matches the filter search value and
  1044. * returns a boolean indicating whether or not the stylesheet source was
  1045. * highlighted.
  1046. *
  1047. * @return {Boolean} true if the stylesheet source was highlighted, false
  1048. * otherwise.
  1049. */
  1050. _highlightStyleSheet: function (rule) {
  1051. let styleSheetSource = rule.title.toLowerCase();
  1052. let isStyleSheetHighlighted = this.searchData.strictSearchValue ?
  1053. styleSheetSource === this.searchData.strictSearchValue :
  1054. styleSheetSource.includes(this.searchValue);
  1055. if (isStyleSheetHighlighted) {
  1056. rule.editor.source.classList.add("ruleview-highlight");
  1057. }
  1058. return isStyleSheetHighlighted;
  1059. },
  1060. /**
  1061. * Highlights the rule properties and computed properties that match the
  1062. * filter search value and returns a boolean indicating whether or not the
  1063. * property or computed property was highlighted.
  1064. *
  1065. * @param {TextPropertyEditor} editor
  1066. * The rule property TextPropertyEditor object.
  1067. * @return {Boolean} true if the property or computed property was
  1068. * highlighted, false otherwise.
  1069. */
  1070. _highlightProperty: function (editor) {
  1071. let isPropertyHighlighted = this._highlightRuleProperty(editor);
  1072. let isComputedHighlighted = this._highlightComputedProperty(editor);
  1073. // Expand the computed list if a computed property is highlighted and the
  1074. // property rule is not highlighted
  1075. if (!isPropertyHighlighted && isComputedHighlighted &&
  1076. !editor.computed.hasAttribute("user-open")) {
  1077. editor.expandForFilter();
  1078. }
  1079. return isPropertyHighlighted || isComputedHighlighted;
  1080. },
  1081. /**
  1082. * Called when TextPropertyEditor is updated and updates the rule property
  1083. * highlight.
  1084. *
  1085. * @param {TextPropertyEditor} editor
  1086. * The rule property TextPropertyEditor object.
  1087. */
  1088. _updatePropertyHighlight: function (editor) {
  1089. if (!this.searchValue || !this.searchData) {
  1090. return;
  1091. }
  1092. this._clearHighlight(editor.element);
  1093. if (this._highlightProperty(editor)) {
  1094. this.searchField.classList.remove("devtools-style-searchbox-no-match");
  1095. }
  1096. },
  1097. /**
  1098. * Highlights the rule property that matches the filter search value
  1099. * and returns a boolean indicating whether or not the property was
  1100. * highlighted.
  1101. *
  1102. * @param {TextPropertyEditor} editor
  1103. * The rule property TextPropertyEditor object.
  1104. * @return {Boolean} true if the rule property was highlighted,
  1105. * false otherwise.
  1106. */
  1107. _highlightRuleProperty: function (editor) {
  1108. // Get the actual property value displayed in the rule view
  1109. let propertyName = editor.prop.name.toLowerCase();
  1110. let propertyValue = editor.valueSpan.textContent.toLowerCase();
  1111. return this._highlightMatches(editor.container, propertyName,
  1112. propertyValue);
  1113. },
  1114. /**
  1115. * Highlights the computed property that matches the filter search value and
  1116. * returns a boolean indicating whether or not the computed property was
  1117. * highlighted.
  1118. *
  1119. * @param {TextPropertyEditor} editor
  1120. * The rule property TextPropertyEditor object.
  1121. * @return {Boolean} true if the computed property was highlighted, false
  1122. * otherwise.
  1123. */
  1124. _highlightComputedProperty: function (editor) {
  1125. let isComputedHighlighted = false;
  1126. // Highlight search matches in the computed list of properties
  1127. editor._populateComputed();
  1128. for (let computed of editor.prop.computed) {
  1129. if (computed.element) {
  1130. // Get the actual property value displayed in the computed list
  1131. let computedName = computed.name.toLowerCase();
  1132. let computedValue = computed.parsedValue.toLowerCase();
  1133. isComputedHighlighted = this._highlightMatches(computed.element,
  1134. computedName, computedValue) ? true : isComputedHighlighted;
  1135. }
  1136. }
  1137. return isComputedHighlighted;
  1138. },
  1139. /**
  1140. * Helper function for highlightRules that carries out highlighting the given
  1141. * element if the search terms match the property, and returns a boolean
  1142. * indicating whether or not the search terms match.
  1143. *
  1144. * @param {DOMNode} element
  1145. * The node to highlight if search terms match
  1146. * @param {String} propertyName
  1147. * The property name of a rule
  1148. * @param {String} propertyValue
  1149. * The property value of a rule
  1150. * @return {Boolean} true if the given search terms match the property, false
  1151. * otherwise.
  1152. */
  1153. _highlightMatches: function (element, propertyName, propertyValue) {
  1154. let {
  1155. searchPropertyName,
  1156. searchPropertyValue,
  1157. searchPropertyMatch,
  1158. strictSearchPropertyName,
  1159. strictSearchPropertyValue,
  1160. strictSearchAllValues,
  1161. } = this.searchData;
  1162. let matches = false;
  1163. // If the inputted search value matches a property line like
  1164. // `font-family: arial`, then check to make sure the name and value match.
  1165. // Otherwise, just compare the inputted search string directly against the
  1166. // name and value of the rule property.
  1167. let hasNameAndValue = searchPropertyMatch &&
  1168. searchPropertyName &&
  1169. searchPropertyValue;
  1170. let isMatch = (value, query, isStrict) => {
  1171. return isStrict ? value === query : query && value.includes(query);
  1172. };
  1173. if (hasNameAndValue) {
  1174. matches =
  1175. isMatch(propertyName, searchPropertyName, strictSearchPropertyName) &&
  1176. isMatch(propertyValue, searchPropertyValue, strictSearchPropertyValue);
  1177. } else {
  1178. matches =
  1179. isMatch(propertyName, searchPropertyName,
  1180. strictSearchPropertyName || strictSearchAllValues) ||
  1181. isMatch(propertyValue, searchPropertyValue,
  1182. strictSearchPropertyValue || strictSearchAllValues);
  1183. }
  1184. if (matches) {
  1185. element.classList.add("ruleview-highlight");
  1186. }
  1187. return matches;
  1188. },
  1189. /**
  1190. * Clear all search filter highlights in the panel, and close the computed
  1191. * list if toggled opened
  1192. */
  1193. _clearHighlight: function (element) {
  1194. for (let el of element.querySelectorAll(".ruleview-highlight")) {
  1195. el.classList.remove("ruleview-highlight");
  1196. }
  1197. for (let computed of element.querySelectorAll(
  1198. ".ruleview-computedlist[filter-open]")) {
  1199. computed.parentNode._textPropertyEditor.collapseForFilter();
  1200. }
  1201. },
  1202. /**
  1203. * Called when the pseudo class panel button is clicked and toggles
  1204. * the display of the pseudo class panel.
  1205. */
  1206. _onTogglePseudoClassPanel: function () {
  1207. if (this.pseudoClassPanel.hidden) {
  1208. this.pseudoClassToggle.setAttribute("checked", "true");
  1209. this.hoverCheckbox.setAttribute("tabindex", "0");
  1210. this.activeCheckbox.setAttribute("tabindex", "0");
  1211. this.focusCheckbox.setAttribute("tabindex", "0");
  1212. } else {
  1213. this.pseudoClassToggle.removeAttribute("checked");
  1214. this.hoverCheckbox.setAttribute("tabindex", "-1");
  1215. this.activeCheckbox.setAttribute("tabindex", "-1");
  1216. this.focusCheckbox.setAttribute("tabindex", "-1");
  1217. }
  1218. this.pseudoClassPanel.hidden = !this.pseudoClassPanel.hidden;
  1219. },
  1220. /**
  1221. * Called when a pseudo class checkbox is clicked and toggles
  1222. * the pseudo class for the current selected element.
  1223. */
  1224. _onTogglePseudoClass: function (event) {
  1225. let target = event.currentTarget;
  1226. this.inspector.togglePseudoClass(target.value);
  1227. },
  1228. /**
  1229. * Handle the keypress event in the rule view.
  1230. */
  1231. _onShortcut: function (name, event) {
  1232. if (!event.target.closest("#sidebar-panel-ruleview")) {
  1233. return;
  1234. }
  1235. if (name === "CmdOrCtrl+F") {
  1236. this.searchField.focus();
  1237. event.preventDefault();
  1238. } else if ((name === "Return" || name === "Space") &&
  1239. this.element.classList.contains("non-interactive")) {
  1240. event.preventDefault();
  1241. } else if (name === "Escape" &&
  1242. event.target === this.searchField &&
  1243. this._onClearSearch()) {
  1244. // Handle the search box's keypress event. If the escape key is pressed,
  1245. // clear the search box field.
  1246. event.preventDefault();
  1247. event.stopPropagation();
  1248. }
  1249. }
  1250. };
  1251. /**
  1252. * Helper functions
  1253. */
  1254. /**
  1255. * Walk up the DOM from a given node until a parent property holder is found.
  1256. * For elements inside the computed property list, the non-computed parent
  1257. * property holder will be returned
  1258. *
  1259. * @param {DOMNode} node
  1260. * The node to start from
  1261. * @return {DOMNode} The parent property holder node, or null if not found
  1262. */
  1263. function getParentTextPropertyHolder(node) {
  1264. while (true) {
  1265. if (!node || !node.classList) {
  1266. return null;
  1267. }
  1268. if (node.classList.contains("ruleview-property")) {
  1269. return node;
  1270. }
  1271. node = node.parentNode;
  1272. }
  1273. }
  1274. /**
  1275. * For any given node, find the TextProperty it is in if any
  1276. * @param {DOMNode} node
  1277. * The node to start from
  1278. * @return {TextProperty}
  1279. */
  1280. function getParentTextProperty(node) {
  1281. let parent = getParentTextPropertyHolder(node);
  1282. if (!parent) {
  1283. return null;
  1284. }
  1285. let propValue = parent.querySelector(".ruleview-propertyvalue");
  1286. if (!propValue) {
  1287. return null;
  1288. }
  1289. return propValue.textProperty;
  1290. }
  1291. /**
  1292. * Walker up the DOM from a given node until a parent property holder is found,
  1293. * and return the textContent for the name and value nodes.
  1294. * Stops at the first property found, so if node is inside the computed property
  1295. * list, the computed property will be returned
  1296. *
  1297. * @param {DOMNode} node
  1298. * The node to start from
  1299. * @return {Object} {name, value}
  1300. */
  1301. function getPropertyNameAndValue(node) {
  1302. while (true) {
  1303. if (!node || !node.classList) {
  1304. return null;
  1305. }
  1306. // Check first for ruleview-computed since it's the deepest
  1307. if (node.classList.contains("ruleview-computed") ||
  1308. node.classList.contains("ruleview-property")) {
  1309. return {
  1310. name: node.querySelector(".ruleview-propertyname").textContent,
  1311. value: node.querySelector(".ruleview-propertyvalue").textContent
  1312. };
  1313. }
  1314. node = node.parentNode;
  1315. }
  1316. }
  1317. function RuleViewTool(inspector, window) {
  1318. this.inspector = inspector;
  1319. this.document = window.document;
  1320. this.view = new CssRuleView(this.inspector, this.document);
  1321. this.clearUserProperties = this.clearUserProperties.bind(this);
  1322. this.refresh = this.refresh.bind(this);
  1323. this.onLinkClicked = this.onLinkClicked.bind(this);
  1324. this.onMutations = this.onMutations.bind(this);
  1325. this.onPanelSelected = this.onPanelSelected.bind(this);
  1326. this.onPropertyChanged = this.onPropertyChanged.bind(this);
  1327. this.onResized = this.onResized.bind(this);
  1328. this.onSelected = this.onSelected.bind(this);
  1329. this.onViewRefreshed = this.onViewRefreshed.bind(this);
  1330. this.view.on("ruleview-changed", this.onPropertyChanged);
  1331. this.view.on("ruleview-refreshed", this.onViewRefreshed);
  1332. this.view.on("ruleview-linked-clicked", this.onLinkClicked);
  1333. this.inspector.selection.on("detached-front", this.onSelected);
  1334. this.inspector.selection.on("new-node-front", this.onSelected);
  1335. this.inspector.selection.on("pseudoclass", this.refresh);
  1336. this.inspector.target.on("navigate", this.clearUserProperties);
  1337. this.inspector.sidebar.on("ruleview-selected", this.onPanelSelected);
  1338. this.inspector.pageStyle.on("stylesheet-updated", this.refresh);
  1339. this.inspector.walker.on("mutations", this.onMutations);
  1340. this.inspector.walker.on("resize", this.onResized);
  1341. this.onSelected();
  1342. }
  1343. RuleViewTool.prototype = {
  1344. isSidebarActive: function () {
  1345. if (!this.view) {
  1346. return false;
  1347. }
  1348. return this.inspector.sidebar.getCurrentTabID() == "ruleview";
  1349. },
  1350. onSelected: function (event) {
  1351. // Ignore the event if the view has been destroyed, or if it's inactive.
  1352. // But only if the current selection isn't null. If it's been set to null,
  1353. // let the update go through as this is needed to empty the view on
  1354. // navigation.
  1355. if (!this.view) {
  1356. return;
  1357. }
  1358. let isInactive = !this.isSidebarActive() &&
  1359. this.inspector.selection.nodeFront;
  1360. if (isInactive) {
  1361. return;
  1362. }
  1363. this.view.setPageStyle(this.inspector.pageStyle);
  1364. if (!this.inspector.selection.isConnected() ||
  1365. !this.inspector.selection.isElementNode()) {
  1366. this.view.selectElement(null);
  1367. return;
  1368. }
  1369. if (!event || event == "new-node-front") {
  1370. let done = this.inspector.updating("rule-view");
  1371. this.view.selectElement(this.inspector.selection.nodeFront)
  1372. .then(done, done);
  1373. }
  1374. },
  1375. refresh: function () {
  1376. if (this.isSidebarActive()) {
  1377. this.view.refreshPanel();
  1378. }
  1379. },
  1380. clearUserProperties: function () {
  1381. if (this.view && this.view.store && this.view.store.userProperties) {
  1382. this.view.store.userProperties.clear();
  1383. }
  1384. },
  1385. onPanelSelected: function () {
  1386. if (this.inspector.selection.nodeFront === this.view._viewedElement) {
  1387. this.refresh();
  1388. } else {
  1389. this.onSelected();
  1390. }
  1391. },
  1392. onLinkClicked: function (e, rule) {
  1393. let sheet = rule.parentStyleSheet;
  1394. // Chrome stylesheets are not listed in the style editor, so show
  1395. // these sheets in the view source window instead.
  1396. if (!sheet || sheet.isSystem) {
  1397. let href = rule.nodeHref || rule.href;
  1398. let toolbox = gDevTools.getToolbox(this.inspector.target);
  1399. toolbox.viewSource(href, rule.line);
  1400. return;
  1401. }
  1402. let location = promise.resolve(rule.location);
  1403. if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
  1404. location = rule.getOriginalLocation();
  1405. }
  1406. location.then(({ source, href, line, column }) => {
  1407. let target = this.inspector.target;
  1408. if (Tools.styleEditor.isTargetSupported(target)) {
  1409. gDevTools.showToolbox(target, "styleeditor").then(function (toolbox) {
  1410. let url = source || href;
  1411. toolbox.getCurrentPanel().selectStyleSheet(url, line, column);
  1412. });
  1413. }
  1414. return;
  1415. });
  1416. },
  1417. onPropertyChanged: function () {
  1418. this.inspector.markDirty();
  1419. },
  1420. onViewRefreshed: function () {
  1421. this.inspector.emit("rule-view-refreshed");
  1422. },
  1423. /**
  1424. * When markup mutations occur, if an attribute of the selected node changes,
  1425. * we need to refresh the view as that might change the node's styles.
  1426. */
  1427. onMutations: function (mutations) {
  1428. for (let {type, target} of mutations) {
  1429. if (target === this.inspector.selection.nodeFront &&
  1430. type === "attributes") {
  1431. this.refresh();
  1432. break;
  1433. }
  1434. }
  1435. },
  1436. /**
  1437. * When the window gets resized, this may cause media-queries to match, and
  1438. * therefore, different styles may apply.
  1439. */
  1440. onResized: function () {
  1441. this.refresh();
  1442. },
  1443. destroy: function () {
  1444. this.inspector.walker.off("mutations", this.onMutations);
  1445. this.inspector.walker.off("resize", this.onResized);
  1446. this.inspector.selection.off("detached-front", this.onSelected);
  1447. this.inspector.selection.off("pseudoclass", this.refresh);
  1448. this.inspector.selection.off("new-node-front", this.onSelected);
  1449. this.inspector.target.off("navigate", this.clearUserProperties);
  1450. this.inspector.sidebar.off("ruleview-selected", this.onPanelSelected);
  1451. if (this.inspector.pageStyle) {
  1452. this.inspector.pageStyle.off("stylesheet-updated", this.refresh);
  1453. }
  1454. this.view.off("ruleview-linked-clicked", this.onLinkClicked);
  1455. this.view.off("ruleview-changed", this.onPropertyChanged);
  1456. this.view.off("ruleview-refreshed", this.onViewRefreshed);
  1457. this.view.destroy();
  1458. this.view = this.document = this.inspector = null;
  1459. }
  1460. };
  1461. exports.CssRuleView = CssRuleView;
  1462. exports.RuleViewTool = RuleViewTool;