Bug 1523128 - Implement open source link in the new rules view. r=rcaliman
[gecko.git] / devtools / client / inspector / rules / models / rule.js
blob0781b6c89b234140c5b7c0d8dba3ad661ba2840d
1 /* -*- indent-tabs-mode: nil; js-indent-level: 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 "use strict";
9 const promise = require("promise");
10 const CssLogic = require("devtools/shared/inspector/css-logic");
11 const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
12 const TextProperty = require("devtools/client/inspector/rules/models/text-property");
13 const Services = require("Services");
15 loader.lazyRequireGetter(this, "updateSourceLink", "devtools/client/inspector/rules/actions/rules", true);
16 loader.lazyRequireGetter(this, "promiseWarn", "devtools/client/inspector/shared/utils", true);
17 loader.lazyRequireGetter(this, "parseNamedDeclarations", "devtools/shared/css/parsing-utils", true);
19 const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties";
20 const {LocalizationHelper} = require("devtools/shared/l10n");
21 const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
23 /**
24  * Rule is responsible for the following:
25  *   Manages a single style declaration or rule.
26  *   Applies changes to the properties in a rule.
27  *   Maintains a list of TextProperty objects.
28  */
29 class Rule {
30   /**
31    * @param {ElementStyle} elementStyle
32    *        The ElementStyle to which this rule belongs.
33    * @param {Object} options
34    *        The information used to construct this rule. Properties include:
35    *          rule: A StyleRuleActor
36    *          inherited: An element this rule was inherited from. If omitted,
37    *            the rule applies directly to the current element.
38    *          isSystem: Is this a user agent style?
39    *          isUnmatched: True if the rule does not match the current selected
40    *            element, otherwise, false.
41    */
42   constructor(elementStyle, options) {
43     this.elementStyle = elementStyle;
44     this.domRule = options.rule;
45     this.matchedSelectors = options.matchedSelectors || [];
46     this.pseudoElement = options.pseudoElement || "";
47     this.isSystem = options.isSystem;
48     this.isUnmatched = options.isUnmatched || false;
49     this.inherited = options.inherited || null;
50     this.keyframes = options.keyframes || null;
52     this.mediaText = this.domRule && this.domRule.mediaText ? this.domRule.mediaText : "";
53     this.cssProperties = this.elementStyle.ruleView.cssProperties;
54     this.inspector = this.elementStyle.ruleView.inspector;
55     this.store = this.elementStyle.ruleView.store;
57     // Populate the text properties with the style's current authoredText
58     // value, and add in any disabled properties from the store.
59     this.textProps = this._getTextProperties();
60     this.textProps = this.textProps.concat(this._getDisabledProperties());
62     this.getUniqueSelector = this.getUniqueSelector.bind(this);
63     this.onLocationChanged = this.onLocationChanged.bind(this);
64     this.updateSourceLocation = this.updateSourceLocation.bind(this);
65   }
67   destroy() {
68     if (this.unsubscribeSourceMap) {
69       this.unsubscribeSourceMap();
70     }
72     this.domRule.off("location-changed", this.onLocationChanged);
73   }
75   get declarations() {
76     return this.textProps;
77   }
79   get inheritance() {
80     if (!this.inherited) {
81       return null;
82     }
84     return {
85       inherited: this.inherited,
86       inheritedSource: this.inheritedSource,
87     };
88   }
90   get selector() {
91     return {
92       getUniqueSelector: this.getUniqueSelector,
93       matchedSelectors: this.matchedSelectors,
94       selectors: this.domRule.selectors,
95       selectorText: this.keyframes ? this.domRule.keyText : this.selectorText,
96     };
97   }
99   get sourceLink() {
100     return {
101       label: this.getSourceText(CssLogic.shortSource({ href: this.sourceLocation.url })),
102       title: this.getSourceText(this.sourceLocation.url),
103     };
104   }
106   get sourceMapURLService() {
107     return this.inspector.toolbox.sourceMapURLService;
108   }
110   /**
111    * Returns the original source location which includes the original URL, line and
112    * column numbers.
113    */
114   get sourceLocation() {
115     if (!this._sourceLocation) {
116       this._sourceLocation = {
117         column: this.ruleColumn,
118         line: this.ruleLine,
119         url: this.sheet ? this.sheet.href || this.sheet.nodeHref : null,
120       };
121     }
123     return this._sourceLocation;
124   }
126   get title() {
127     let title = CssLogic.shortSource(this.sheet);
128     if (this.domRule.type !== ELEMENT_STYLE && this.ruleLine > 0) {
129       title += ":" + this.ruleLine;
130     }
132     return title + (this.mediaText ? " @media " + this.mediaText : "");
133   }
135   get inheritedSource() {
136     if (this._inheritedSource) {
137       return this._inheritedSource;
138     }
139     this._inheritedSource = "";
140     if (this.inherited) {
141       let eltText = this.inherited.displayName;
142       if (this.inherited.id) {
143         eltText += "#" + this.inherited.id;
144       }
145       this._inheritedSource =
146         STYLE_INSPECTOR_L10N.getFormatStr("rule.inheritedFrom", eltText);
147     }
148     return this._inheritedSource;
149   }
151   get keyframesName() {
152     if (this._keyframesName) {
153       return this._keyframesName;
154     }
155     this._keyframesName = "";
156     if (this.keyframes) {
157       this._keyframesName =
158         STYLE_INSPECTOR_L10N.getFormatStr("rule.keyframe", this.keyframes.name);
159     }
160     return this._keyframesName;
161   }
163   get keyframesRule() {
164     if (!this.keyframes) {
165       return null;
166     }
168     return {
169       id: this.keyframes.actorID,
170       keyframesName: this.keyframesName,
171     };
172   }
174   get selectorText() {
175     return this.domRule.selectors ? this.domRule.selectors.join(", ") :
176       CssLogic.l10n("rule.sourceElement");
177   }
179   /**
180    * The rule's stylesheet.
181    */
182   get sheet() {
183     return this.domRule ? this.domRule.parentStyleSheet : null;
184   }
186   /**
187    * The rule's line within a stylesheet
188    */
189   get ruleLine() {
190     return this.domRule ? this.domRule.line : -1;
191   }
193   /**
194    * The rule's column within a stylesheet
195    */
196   get ruleColumn() {
197     return this.domRule ? this.domRule.column : null;
198   }
200   /**
201    * Returns the TextProperty with the given id or undefined if it cannot be found.
202    *
203    * @param {String} id
204    *        A TextProperty id.
205    * @return {TextProperty|undefined} with the given id in the current Rule or undefined
206    * if it cannot be found.
207    */
208   getDeclaration(id) {
209     return this.textProps.find(textProp => textProp.id === id);
210   }
212   /**
213    * Returns a formatted source text of the given stylesheet URL with its source line
214    * and @media text.
215    *
216    * @param  {String} url
217    *         The stylesheet URL.
218    */
219   getSourceText(url) {
220     if (this.isSystem) {
221       return `${STYLE_INSPECTOR_L10N.getStr("rule.userAgentStyles")} ${this.title}`;
222     }
224     let sourceText = url;
226     if (this.sourceLocation.line > 0) {
227       sourceText += ":" + this.sourceLocation.line;
228     }
230     if (this.mediaText) {
231       sourceText += " @media " + this.mediaText;
232     }
234     return sourceText;
235   }
237   /**
238    * Returns an unique selector for the CSS rule.
239    */
240   async getUniqueSelector() {
241     let selector = "";
243     if (this.domRule.selectors) {
244       // This is a style rule with a selector.
245       selector = this.domRule.selectors.join(", ");
246     } else if (this.inherited) {
247       // This is an inline style from an inherited rule. Need to resolve the unique
248       // selector from the node which rule this is inherited from.
249       selector = await this.inherited.getUniqueSelector();
250     } else {
251       // This is an inline style from the current node.
252       selector = this.inspector.selectionCssSelector;
253     }
255     return selector;
256   }
258   /**
259    * Returns true if the rule matches the creation options
260    * specified.
261    *
262    * @param {Object} options
263    *        Creation options. See the Rule constructor for documentation.
264    */
265   matches(options) {
266     return this.domRule === options.rule;
267   }
269   /**
270    * Create a new TextProperty to include in the rule.
271    *
272    * @param {String} name
273    *        The text property name (such as "background" or "border-top").
274    * @param {String} value
275    *        The property's value (not including priority).
276    * @param {String} priority
277    *        The property's priority (either "important" or an empty string).
278    * @param {Boolean} enabled
279    *        True if the property should be enabled.
280    * @param {TextProperty} siblingProp
281    *        Optional, property next to which the new property will be added.
282    */
283   createProperty(name, value, priority, enabled, siblingProp) {
284     const prop = new TextProperty(this, name, value, priority, enabled);
286     let ind;
287     if (siblingProp) {
288       ind = this.textProps.indexOf(siblingProp) + 1;
289       this.textProps.splice(ind, 0, prop);
290     } else {
291       ind = this.textProps.length;
292       this.textProps.push(prop);
293     }
295     this.applyProperties((modifications) => {
296       modifications.createProperty(ind, name, value, priority, enabled);
297       // Now that the rule has been updated, the server might have given us data
298       // that changes the state of the property. Update it now.
299       prop.updateEditor();
300     });
302     return prop;
303   }
305   /**
306    * Helper function for applyProperties that is called when the actor
307    * does not support as-authored styles.  Store disabled properties
308    * in the element style's store.
309    */
310   _applyPropertiesNoAuthored(modifications) {
311     this.elementStyle.markOverriddenAll();
313     const disabledProps = [];
315     for (const prop of this.textProps) {
316       if (prop.invisible) {
317         continue;
318       }
319       if (!prop.enabled) {
320         disabledProps.push({
321           name: prop.name,
322           value: prop.value,
323           priority: prop.priority,
324         });
325         continue;
326       }
327       if (prop.value.trim() === "") {
328         continue;
329       }
331       modifications.setProperty(-1, prop.name, prop.value, prop.priority);
333       prop.updateComputed();
334     }
336     // Store disabled properties in the disabled store.
337     const disabled = this.elementStyle.store.disabled;
338     if (disabledProps.length > 0) {
339       disabled.set(this.domRule, disabledProps);
340     } else {
341       disabled.delete(this.domRule);
342     }
344     return modifications.apply().then(() => {
345       const cssProps = {};
346       // Note that even though StyleRuleActors normally provide parsed
347       // declarations already, _applyPropertiesNoAuthored is only used when
348       // connected to older backend that do not provide them. So parse here.
349       for (const cssProp of parseNamedDeclarations(this.cssProperties.isKnown,
350                                                  this.domRule.authoredText)) {
351         cssProps[cssProp.name] = cssProp;
352       }
354       for (const textProp of this.textProps) {
355         if (!textProp.enabled) {
356           continue;
357         }
358         let cssProp = cssProps[textProp.name];
360         if (!cssProp) {
361           cssProp = {
362             name: textProp.name,
363             value: "",
364             priority: "",
365           };
366         }
368         textProp.priority = cssProp.priority;
369       }
370     });
371   }
373   /**
374    * A helper for applyProperties that applies properties in the "as
375    * authored" case; that is, when the StyleRuleActor supports
376    * setRuleText.
377    */
378   _applyPropertiesAuthored(modifications) {
379     return modifications.apply().then(() => {
380       // The rewriting may have required some other property values to
381       // change, e.g., to insert some needed terminators.  Update the
382       // relevant properties here.
383       for (const index in modifications.changedDeclarations) {
384         const newValue = modifications.changedDeclarations[index];
385         this.textProps[index].updateValue(newValue);
386       }
387       // Recompute and redisplay the computed properties.
388       for (const prop of this.textProps) {
389         if (!prop.invisible && prop.enabled) {
390           prop.updateComputed();
391           prop.updateEditor();
392         }
393       }
394     });
395   }
397   /**
398    * Reapply all the properties in this rule, and update their
399    * computed styles.  Will re-mark overridden properties.  Sets the
400    * |_applyingModifications| property to a promise which will resolve
401    * when the edit has completed.
402    *
403    * @param {Function} modifier a function that takes a RuleModificationList
404    *        (or RuleRewriter) as an argument and that modifies it
405    *        to apply the desired edit
406    * @return {Promise} a promise which will resolve when the edit
407    *        is complete
408    */
409   applyProperties(modifier) {
410     // If there is already a pending modification, we have to wait
411     // until it settles before applying the next modification.
412     const resultPromise =
413         promise.resolve(this._applyingModifications).then(() => {
414           const modifications = this.domRule.startModifyingProperties(
415             this.cssProperties);
416           modifier(modifications);
417           if (this.domRule.canSetRuleText) {
418             return this._applyPropertiesAuthored(modifications);
419           }
420           return this._applyPropertiesNoAuthored(modifications);
421         }).then(() => {
422           this.elementStyle.markOverriddenAll();
424           if (resultPromise === this._applyingModifications) {
425             this._applyingModifications = null;
426             this.elementStyle._changed();
427           }
428         }).catch(promiseWarn);
430     this._applyingModifications = resultPromise;
431     return resultPromise;
432   }
434   /**
435    * Renames a property.
436    *
437    * @param {TextProperty} property
438    *        The property to rename.
439    * @param {String} name
440    *        The new property name (such as "background" or "border-top").
441    * @return {Promise}
442    */
443   setPropertyName(property, name) {
444     if (name === property.name) {
445       return Promise.resolve();
446     }
448     const oldName = property.name;
449     property.name = name;
450     const index = this.textProps.indexOf(property);
451     return this.applyProperties(modifications => {
452       modifications.renameProperty(index, oldName, name);
453     });
454   }
456   /**
457    * Sets the value and priority of a property, then reapply all properties.
458    *
459    * @param {TextProperty} property
460    *        The property to manipulate.
461    * @param {String} value
462    *        The property's value (not including priority).
463    * @param {String} priority
464    *        The property's priority (either "important" or an empty string).
465    * @return {Promise}
466    */
467   setPropertyValue(property, value, priority) {
468     if (value === property.value && priority === property.priority) {
469       return Promise.resolve();
470     }
472     property.value = value;
473     property.priority = priority;
475     const index = this.textProps.indexOf(property);
476     return this.applyProperties(modifications => {
477       modifications.setProperty(index, property.name, value, priority);
478     });
479   }
481   /**
482    * Just sets the value and priority of a property, in order to preview its
483    * effect on the content document.
484    *
485    * @param {TextProperty} property
486    *        The property which value will be previewed
487    * @param {String} value
488    *        The value to be used for the preview
489    * @param {String} priority
490    *        The property's priority (either "important" or an empty string).
491    **@return {Promise}
492    */
493   previewPropertyValue(property, value, priority) {
494     const modifications = this.domRule.startModifyingProperties(this.cssProperties);
495     modifications.setProperty(this.textProps.indexOf(property),
496                               property.name, value, priority);
497     return modifications.apply().then(() => {
498       // Ensure dispatching a ruleview-changed event
499       // also for previews
500       this.elementStyle._changed();
501     });
502   }
504   /**
505    * Disables or enables given TextProperty.
506    *
507    * @param {TextProperty} property
508    *        The property to enable/disable
509    * @param {Boolean} value
510    */
511   setPropertyEnabled(property, value) {
512     if (property.enabled === !!value) {
513       return;
514     }
515     property.enabled = !!value;
516     const index = this.textProps.indexOf(property);
517     this.applyProperties((modifications) => {
518       modifications.setPropertyEnabled(index, property.name, property.enabled);
519     });
520   }
522   /**
523    * Remove a given TextProperty from the rule and update the rule
524    * accordingly.
525    *
526    * @param {TextProperty} property
527    *        The property to be removed
528    */
529   removeProperty(property) {
530     const index = this.textProps.indexOf(property);
531     this.textProps.splice(index, 1);
532     // Need to re-apply properties in case removing this TextProperty
533     // exposes another one.
534     this.applyProperties((modifications) => {
535       modifications.removeProperty(index, property.name);
536     });
537   }
539   /**
540    * Get the list of TextProperties from the style. Needs
541    * to parse the style's authoredText.
542    */
543   _getTextProperties() {
544     const textProps = [];
545     const store = this.elementStyle.store;
547     // Starting with FF49, StyleRuleActors provide parsed declarations.
548     let props = this.domRule.declarations;
549     if (!props.length) {
550       // If the authored text has an invalid property, it will show up
551       // as nameless.  Skip these as we don't currently have a good
552       // way to display them.
553       props = parseNamedDeclarations(this.cssProperties.isKnown,
554                                      this.domRule.authoredText, true);
555     }
557     for (const prop of props) {
558       const name = prop.name;
559       // In an inherited rule, we only show inherited properties.
560       // However, we must keep all properties in order for rule
561       // rewriting to work properly.  So, compute the "invisible"
562       // property here.
563       const invisible = this.inherited && !this.cssProperties.isInherited(name);
564       const value = store.userProperties.getProperty(this.domRule, name,
565                                                    prop.value);
566       const textProp = new TextProperty(this, name, value, prop.priority,
567                                       !("commentOffsets" in prop),
568                                       invisible);
569       textProps.push(textProp);
570     }
572     return textProps;
573   }
575   /**
576    * Return the list of disabled properties from the store for this rule.
577    */
578   _getDisabledProperties() {
579     const store = this.elementStyle.store;
581     // Include properties from the disabled property store, if any.
582     const disabledProps = store.disabled.get(this.domRule);
583     if (!disabledProps) {
584       return [];
585     }
587     const textProps = [];
589     for (const prop of disabledProps) {
590       const value = store.userProperties.getProperty(this.domRule, prop.name,
591                                                    prop.value);
592       const textProp = new TextProperty(this, prop.name, value, prop.priority);
593       textProp.enabled = false;
594       textProps.push(textProp);
595     }
597     return textProps;
598   }
600   /**
601    * Reread the current state of the rules and rebuild text
602    * properties as needed.
603    */
604   refresh(options) {
605     this.matchedSelectors = options.matchedSelectors || [];
606     const newTextProps = this._getTextProperties();
608     // The element style rule behaves differently on refresh. We basically need to update
609     // it to reflect the new text properties exactly. The order might have changed, some
610     // properties might have been removed, etc. And we don't need to mark anything as
611     // disabled here. The element style rule should always reflect the content of the
612     // style attribute.
613     if (this.domRule.type === ELEMENT_STYLE) {
614       this.textProps = newTextProps;
616       if (this.editor) {
617         this.editor.populate(true);
618       }
620       return;
621     }
623     // Update current properties for each property present on the style.
624     // This will mark any touched properties with _visited so we
625     // can detect properties that weren't touched (because they were
626     // removed from the style).
627     // Also keep track of properties that didn't exist in the current set
628     // of properties.
629     const brandNewProps = [];
630     for (const newProp of newTextProps) {
631       if (!this._updateTextProperty(newProp)) {
632         brandNewProps.push(newProp);
633       }
634     }
636     // Refresh editors and disabled state for all the properties that
637     // were updated.
638     for (const prop of this.textProps) {
639       // Properties that weren't touched during the update
640       // process must no longer exist on the node.  Mark them disabled.
641       if (!prop._visited) {
642         prop.enabled = false;
643         prop.updateEditor();
644       } else {
645         delete prop._visited;
646       }
647     }
649     // Add brand new properties.
650     this.textProps = this.textProps.concat(brandNewProps);
652     // Refresh the editor if one already exists.
653     if (this.editor) {
654       this.editor.populate();
655     }
656   }
658   /**
659    * Update the current TextProperties that match a given property
660    * from the authoredText.  Will choose one existing TextProperty to update
661    * with the new property's value, and will disable all others.
662    *
663    * When choosing the best match to reuse, properties will be chosen
664    * by assigning a rank and choosing the highest-ranked property:
665    *   Name, value, and priority match, enabled. (6)
666    *   Name, value, and priority match, disabled. (5)
667    *   Name and value match, enabled. (4)
668    *   Name and value match, disabled. (3)
669    *   Name matches, enabled. (2)
670    *   Name matches, disabled. (1)
671    *
672    * If no existing properties match the property, nothing happens.
673    *
674    * @param {TextProperty} newProp
675    *        The current version of the property, as parsed from the
676    *        authoredText in Rule._getTextProperties().
677    * @return {Boolean} true if a property was updated, false if no properties
678    *         were updated.
679    */
680   _updateTextProperty(newProp) {
681     const match = { rank: 0, prop: null };
683     for (const prop of this.textProps) {
684       if (prop.name !== newProp.name) {
685         continue;
686       }
688       // Mark this property visited.
689       prop._visited = true;
691       // Start at rank 1 for matching name.
692       let rank = 1;
694       // Value and Priority matches add 2 to the rank.
695       // Being enabled adds 1.  This ranks better matches higher,
696       // with priority breaking ties.
697       if (prop.value === newProp.value) {
698         rank += 2;
699         if (prop.priority === newProp.priority) {
700           rank += 2;
701         }
702       }
704       if (prop.enabled) {
705         rank += 1;
706       }
708       if (rank > match.rank) {
709         if (match.prop) {
710           // We outrank a previous match, disable it.
711           match.prop.enabled = false;
712           match.prop.updateEditor();
713         }
714         match.rank = rank;
715         match.prop = prop;
716       } else if (rank) {
717         // A previous match outranks us, disable ourself.
718         prop.enabled = false;
719         prop.updateEditor();
720       }
721     }
723     // If we found a match, update its value with the new text property
724     // value.
725     if (match.prop) {
726       match.prop.set(newProp);
727       return true;
728     }
730     return false;
731   }
733   /**
734    * Jump between editable properties in the UI. If the focus direction is
735    * forward, begin editing the next property name if available or focus the
736    * new property editor otherwise. If the focus direction is backward,
737    * begin editing the previous property value or focus the selector editor if
738    * this is the first element in the property list.
739    *
740    * @param {TextProperty} textProperty
741    *        The text property that will be left to focus on a sibling.
742    * @param {Number} direction
743    *        The move focus direction number.
744    */
745   editClosestTextProperty(textProperty, direction) {
746     let index = this.textProps.indexOf(textProperty);
748     if (direction === Services.focus.MOVEFOCUS_FORWARD) {
749       for (++index; index < this.textProps.length; ++index) {
750         if (!this.textProps[index].invisible) {
751           break;
752         }
753       }
754       if (index === this.textProps.length) {
755         textProperty.rule.editor.closeBrace.click();
756       } else {
757         this.textProps[index].editor.nameSpan.click();
758       }
759     } else if (direction === Services.focus.MOVEFOCUS_BACKWARD) {
760       for (--index; index >= 0; --index) {
761         if (!this.textProps[index].invisible) {
762           break;
763         }
764       }
765       if (index < 0) {
766         textProperty.editor.ruleEditor.selectorText.click();
767       } else {
768         this.textProps[index].editor.valueSpan.click();
769       }
770     }
771   }
773   /**
774    * Return a string representation of the rule.
775    */
776   stringifyRule() {
777     const selectorText = this.selectorText;
778     let cssText = "";
779     const terminator = Services.appinfo.OS === "WINNT" ? "\r\n" : "\n";
781     for (const textProp of this.textProps) {
782       if (!textProp.invisible) {
783         cssText += "\t" + textProp.stringifyProperty() + terminator;
784       }
785     }
787     return selectorText + " {" + terminator + cssText + "}";
788   }
790   /**
791    * See whether this rule has any non-invisible properties.
792    * @return {Boolean} true if there is any visible property, or false
793    *         if all properties are invisible
794    */
795   hasAnyVisibleProperties() {
796     for (const prop of this.textProps) {
797       if (!prop.invisible) {
798         return true;
799       }
800     }
801     return false;
802   }
804   /**
805    * Handler for "location-changed" events fired from the StyleRuleActor. This could
806    * occur by adding a new declaration to the rule. Updates the source location of the
807    * rule. This will overwrite the source map location.
808    */
809   onLocationChanged() {
810     const url = this.sheet ? this.sheet.href || this.sheet.nodeHref : null;
811     this.updateSourceLocation(url, this.ruleLine, this.ruleColumn);
812   }
814   /**
815    * Subscribes the rule to the source map service to map the the original source
816    * location.
817    */
818   subscribeToLocationChange() {
819     const { url, line, column } = this.sourceLocation;
821     if (url && !this.isSystem && this.domRule.type !== ELEMENT_STYLE) {
822       // Subscribe returns an unsubscribe function that can be called on destroy.
823       this.unsubscribeSourceMap = this.sourceMapURLService.subscribe(url, line, column,
824         (enabled, sourceUrl, sourceLine, sourceColumn) => {
825           if (enabled) {
826             // Only update the source location if source map is in use.
827             this.updateSourceLocation(sourceUrl, sourceLine, sourceColumn);
828           }
829         });
830     }
832     this.domRule.on("location-changed", this.onLocationChanged);
833   }
835   /**
836    * Handler for any location changes called from the SourceMapURLService and can also be
837    * called from onLocationChanged(). Updates the source location for the rule.
838    *
839    * @param  {String} url
840    *         The original URL.
841    * @param  {Number} line
842    *         The original line number.
843    * @param  {number} column
844    *         The original column number.
845    */
846   updateSourceLocation(url, line, column) {
847     this._sourceLocation = {
848       column,
849       line,
850       url,
851     };
852     this.store.dispatch(updateSourceLink(this.domRule.actorID, this.sourceLink));
853   }
856 module.exports = Rule;