Bug 1577282 - Part 1: Move the requires before the constants and format the markup...
[gecko.git] / devtools / client / shared / widgets / Spectrum.js
blobc728bf959d950898263e941e190650b2b66f7ba3
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 const EventEmitter = require("devtools/shared/event-emitter");
8 const { MultiLocalizationHelper } = require("devtools/shared/l10n");
10 loader.lazyRequireGetter(this, "colorUtils", "devtools/shared/css/color", true);
11 loader.lazyRequireGetter(
12   this,
13   "labColors",
14   "devtools/shared/css/color-db",
15   true
17 loader.lazyRequireGetter(
18   this,
19   "getTextProperties",
20   "devtools/shared/accessibility",
21   true
23 loader.lazyRequireGetter(
24   this,
25   "getContrastRatioAgainstBackground",
26   "devtools/shared/accessibility",
27   true
30 const L10N = new MultiLocalizationHelper(
31   "devtools/shared/locales/en-US/accessibility.properties",
32   "devtools/client/locales/en-US/accessibility.properties",
33   "devtools/client/locales/en-US/inspector.properties"
35 const ARROW_KEYS = ["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft"];
36 const [ArrowUp, ArrowRight, ArrowDown, ArrowLeft] = ARROW_KEYS;
37 const XHTML_NS = "http://www.w3.org/1999/xhtml";
38 const SLIDER = {
39   hue: {
40     MIN: "0",
41     MAX: "128",
42     STEP: "1",
43   },
44   alpha: {
45     MIN: "0",
46     MAX: "1",
47     STEP: "0.01",
48   },
51 /**
52  * Spectrum creates a color picker widget in any container you give it.
53  *
54  * Simple usage example:
55  *
56  * const {Spectrum} = require("devtools/client/shared/widgets/Spectrum");
57  * let s = new Spectrum(containerElement, [255, 126, 255, 1]);
58  * s.on("changed", (rgba, color) => {
59  *   console.log("rgba(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + ", " +
60  *     rgba[3] + ")");
61  * });
62  * s.show();
63  * s.destroy();
64  *
65  * Note that the color picker is hidden by default and you need to call show to
66  * make it appear. This 2 stages initialization helps in cases you are creating
67  * the color picker in a parent element that hasn't been appended anywhere yet
68  * or that is hidden. Calling show() when the parent element is appended and
69  * visible will allow spectrum to correctly initialize its various parts.
70  *
71  * Fires the following events:
72  * - changed : When the user changes the current color
73  */
74 function Spectrum(parentEl, rgb) {
75   EventEmitter.decorate(this);
77   this.document = parentEl.ownerDocument;
78   this.element = parentEl.ownerDocument.createElementNS(XHTML_NS, "div");
79   this.parentEl = parentEl;
81   this.element.className = "spectrum-container";
82   // eslint-disable-next-line no-unsanitized/property
83   this.element.innerHTML = `
84     <section class="spectrum-color-picker">
85       <div class="spectrum-color spectrum-box"
86            tabindex="0"
87            role="slider"
88            title="${L10N.getStr("colorPickerTooltip.spectrumDraggerTitle")}"
89            aria-describedby="spectrum-dragger">
90         <div class="spectrum-sat">
91           <div class="spectrum-val">
92             <div class="spectrum-dragger" id="spectrum-dragger"></div>
93           </div>
94         </div>
95       </div>
96     </section>
97     <section class="spectrum-controls">
98       <div class="spectrum-color-preview"></div>
99       <div class="spectrum-slider-container">
100         <div class="spectrum-hue spectrum-box"></div>
101         <div class="spectrum-alpha spectrum-checker spectrum-box"></div>
102       </div>
103     </section>
104     <section class="spectrum-color-contrast accessibility-color-contrast">
105       <div class="contrast-ratio-header-and-single-ratio">
106         <span class="contrast-ratio-label" role="presentation"></span>
107         <span class="contrast-value-and-swatch contrast-ratio-single" role="presentation">
108           <span class="accessibility-contrast-value"></span>
109         </span>
110       </div>
111       <div class="contrast-ratio-range">
112         <span class="contrast-value-and-swatch contrast-ratio-min" role="presentation">
113           <span class="accessibility-contrast-value"></span>
114         </span>
115         <span class="accessibility-color-contrast-separator"></span>
116         <span class="contrast-value-and-swatch contrast-ratio-max" role="presentation">
117           <span class="accessibility-contrast-value"></span>
118         </span>
119       </div>
120     </section>
121   `;
123   this.onElementClick = this.onElementClick.bind(this);
124   this.element.addEventListener("click", this.onElementClick);
126   this.parentEl.appendChild(this.element);
128   // Color spectrum dragger.
129   this.dragger = this.element.querySelector(".spectrum-color");
130   this.dragHelper = this.element.querySelector(".spectrum-dragger");
131   Spectrum.draggable(
132     this.dragger,
133     this.dragHelper,
134     this.onDraggerMove.bind(this)
135   );
137   // Here we define the components for the "controls" section of the color picker.
138   this.controls = this.element.querySelector(".spectrum-controls");
139   this.colorPreview = this.element.querySelector(".spectrum-color-preview");
141   // Create the eyedropper.
142   const eyedropper = this.document.createElementNS(XHTML_NS, "button");
143   eyedropper.id = "eyedropper-button";
144   eyedropper.className = "devtools-button";
145   eyedropper.style.pointerEvents = "auto";
146   eyedropper.setAttribute(
147     "aria-label",
148     L10N.getStr("colorPickerTooltip.eyedropperTitle")
149   );
150   this.controls.insertBefore(eyedropper, this.colorPreview);
152   // Hue slider and alpha slider
153   this.hueSlider = this.createSlider("hue", this.onHueSliderMove.bind(this));
154   this.hueSlider.setAttribute("aria-describedby", this.dragHelper.id);
155   this.alphaSlider = this.createSlider(
156     "alpha",
157     this.onAlphaSliderMove.bind(this)
158   );
160   // Color contrast
161   this.spectrumContrast = this.element.querySelector(
162     ".spectrum-color-contrast"
163   );
164   this.contrastLabel = this.element.querySelector(".contrast-ratio-label");
165   [
166     this.contrastValue,
167     this.contrastValueMin,
168     this.contrastValueMax,
169   ] = this.element.querySelectorAll(".accessibility-contrast-value");
171   // Create the learn more info button
172   const learnMore = this.document.createElementNS(XHTML_NS, "button");
173   learnMore.id = "learn-more-button";
174   learnMore.className = "learn-more";
175   learnMore.title = L10N.getStr("accessibility.learnMore");
176   this.element
177     .querySelector(".contrast-ratio-header-and-single-ratio")
178     .appendChild(learnMore);
180   if (rgb) {
181     this.rgb = rgb;
182     this.updateUI();
183   }
186 module.exports.Spectrum = Spectrum;
188 Spectrum.hsvToRgb = function(h, s, v, a) {
189   let r, g, b;
191   const i = Math.floor(h * 6);
192   const f = h * 6 - i;
193   const p = v * (1 - s);
194   const q = v * (1 - f * s);
195   const t = v * (1 - (1 - f) * s);
197   switch (i % 6) {
198     case 0:
199       r = v;
200       g = t;
201       b = p;
202       break;
203     case 1:
204       r = q;
205       g = v;
206       b = p;
207       break;
208     case 2:
209       r = p;
210       g = v;
211       b = t;
212       break;
213     case 3:
214       r = p;
215       g = q;
216       b = v;
217       break;
218     case 4:
219       r = t;
220       g = p;
221       b = v;
222       break;
223     case 5:
224       r = v;
225       g = p;
226       b = q;
227       break;
228   }
230   return [r * 255, g * 255, b * 255, a];
233 Spectrum.rgbToHsv = function(r, g, b, a) {
234   r = r / 255;
235   g = g / 255;
236   b = b / 255;
238   const max = Math.max(r, g, b);
239   const min = Math.min(r, g, b);
241   const v = max;
242   const d = max - min;
243   const s = max == 0 ? 0 : d / max;
245   let h;
246   if (max == min) {
247     // achromatic
248     h = 0;
249   } else {
250     switch (max) {
251       case r:
252         h = (g - b) / d + (g < b ? 6 : 0);
253         break;
254       case g:
255         h = (b - r) / d + 2;
256         break;
257       case b:
258         h = (r - g) / d + 4;
259         break;
260     }
261     h /= 6;
262   }
263   return [h, s, v, a];
266 Spectrum.draggable = function(element, dragHelper, onmove) {
267   onmove = onmove || function() {};
269   const doc = element.ownerDocument;
270   let dragging = false;
271   let offset = {};
272   let maxHeight = 0;
273   let maxWidth = 0;
275   function setDraggerDimensionsAndOffset() {
276     maxHeight = element.offsetHeight;
277     maxWidth = element.offsetWidth;
278     offset = element.getBoundingClientRect();
279   }
281   function prevent(e) {
282     e.stopPropagation();
283     e.preventDefault();
284   }
286   function move(e) {
287     if (dragging) {
288       if (e.buttons === 0) {
289         // The button is no longer pressed but we did not get a mouseup event.
290         stop();
291         return;
292       }
293       const pageX = e.pageX;
294       const pageY = e.pageY;
296       const dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth));
297       const dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight));
299       onmove.apply(element, [dragX, dragY]);
300     }
301   }
303   function start(e) {
304     const rightClick = e.which === 3;
306     if (!rightClick && !dragging) {
307       dragging = true;
308       setDraggerDimensionsAndOffset();
310       move(e);
312       doc.addEventListener("selectstart", prevent);
313       doc.addEventListener("dragstart", prevent);
314       doc.addEventListener("mousemove", move);
315       doc.addEventListener("mouseup", stop);
317       prevent(e);
318     }
319   }
321   function stop() {
322     if (dragging) {
323       doc.removeEventListener("selectstart", prevent);
324       doc.removeEventListener("dragstart", prevent);
325       doc.removeEventListener("mousemove", move);
326       doc.removeEventListener("mouseup", stop);
327     }
328     dragging = false;
329   }
331   function onKeydown(e) {
332     const { key } = e;
334     if (!ARROW_KEYS.includes(key)) {
335       return;
336     }
338     setDraggerDimensionsAndOffset();
339     const { offsetHeight, offsetTop, offsetLeft } = dragHelper;
340     let dragX = offsetLeft + offsetHeight / 2;
341     let dragY = offsetTop + offsetHeight / 2;
343     if (key === ArrowLeft && dragX > 0) {
344       dragX -= 1;
345     } else if (key === ArrowRight && dragX < maxWidth) {
346       dragX += 1;
347     } else if (key === ArrowUp && dragY > 0) {
348       dragY -= 1;
349     } else if (key === ArrowDown && dragY < maxHeight) {
350       dragY += 1;
351     }
353     onmove.apply(element, [dragX, dragY]);
354   }
356   element.addEventListener("mousedown", start);
357   element.addEventListener("keydown", onKeydown);
361  * Calculates the contrast ratio for a DOM node's computed style against
362  * a given background.
364  * @param  {Object} computedStyle
365  *         The computed style for which we want to calculate the contrast ratio.
366  * @param  {Object} backgroundColor
367  *         Object with one or more of the following properties: value, min, max
368  * @return {Object}
369  *         An object that may contain one or more of the following fields: error,
370  *         isLargeText, value, score for contrast.
371  */
372 function getContrastRatio(computedStyle, backgroundColor) {
373   const props = getTextProperties(computedStyle);
375   if (!props) {
376     return {
377       error: true,
378     };
379   }
381   return getContrastRatioAgainstBackground(backgroundColor, props);
384 Spectrum.prototype = {
385   set textProps(style) {
386     this._textProps = style
387       ? {
388           fontSize: style["font-size"].value,
389           fontWeight: style["font-weight"].value,
390           opacity: style.opacity.value,
391         }
392       : null;
393   },
395   set rgb(color) {
396     this.hsv = Spectrum.rgbToHsv(color[0], color[1], color[2], color[3]);
397   },
399   set backgroundColorData(colorData) {
400     this._backgroundColorData = colorData;
401   },
403   get backgroundColorData() {
404     return this._backgroundColorData;
405   },
407   get textProps() {
408     return this._textProps;
409   },
411   get rgb() {
412     const rgb = Spectrum.hsvToRgb(
413       this.hsv[0],
414       this.hsv[1],
415       this.hsv[2],
416       this.hsv[3]
417     );
418     return [
419       Math.round(rgb[0]),
420       Math.round(rgb[1]),
421       Math.round(rgb[2]),
422       Math.round(rgb[3] * 100) / 100,
423     ];
424   },
426   /**
427    * Map current rgb to the closest color available in the database by
428    * calculating the delta-E between each available color and the current rgb
429    *
430    * @return {String}
431    *         Color name or closest color name
432    */
433   get colorName() {
434     const labColorEntries = Object.entries(labColors);
436     const deltaEs = labColorEntries.map(color =>
437       colorUtils.calculateDeltaE(color[1], colorUtils.rgbToLab(this.rgb))
438     );
440     // Get the color name for the one that has the lowest delta-E
441     const minDeltaE = Math.min(...deltaEs);
442     const colorName = labColorEntries[deltaEs.indexOf(minDeltaE)][0];
443     return minDeltaE === 0
444       ? colorName
445       : L10N.getFormatStr("colorPickerTooltip.colorNameTitle", colorName);
446   },
448   get rgbNoSatVal() {
449     const rgb = Spectrum.hsvToRgb(this.hsv[0], 1, 1);
450     return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]), rgb[3]];
451   },
453   get rgbCssString() {
454     const rgb = this.rgb;
455     return (
456       "rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " + rgb[3] + ")"
457     );
458   },
460   show: function() {
461     this.dragWidth = this.dragger.offsetWidth;
462     this.dragHeight = this.dragger.offsetHeight;
463     this.dragHelperHeight = this.dragHelper.offsetHeight;
465     this.updateUI();
466   },
468   onElementClick: function(e) {
469     e.stopPropagation();
470   },
472   onHueSliderMove: function() {
473     this.hsv[0] = this.hueSlider.value / this.hueSlider.max;
474     this.updateUI();
475     this.onChange();
476   },
478   onDraggerMove: function(dragX, dragY) {
479     this.hsv[1] = dragX / this.dragWidth;
480     this.hsv[2] = (this.dragHeight - dragY) / this.dragHeight;
481     this.updateUI();
482     this.onChange();
483   },
485   onAlphaSliderMove: function() {
486     this.hsv[3] = this.alphaSlider.value / this.alphaSlider.max;
487     this.updateUI();
488     this.onChange();
489   },
491   onChange: function() {
492     this.emit("changed", this.rgb, this.rgbCssString);
493   },
495   /**
496    * Creates and initializes a slider element, attaches it to its parent container
497    * based on the slider type and returns it
498    *
499    * @param  {String} sliderType
500    *         The type of the slider (i.e. alpha or hue)
501    * @param  {Function} onSliderMove
502    *         The function to tie the slider to on input
503    * @return {DOMNode}
504    *         Newly created slider
505    */
506   createSlider: function(sliderType, onSliderMove) {
507     const container = this.element.querySelector(`.spectrum-${sliderType}`);
509     const slider = this.document.createElementNS(XHTML_NS, "input");
510     slider.className = `spectrum-${sliderType}-input`;
511     slider.type = "range";
512     slider.min = SLIDER[sliderType].MIN;
513     slider.max = SLIDER[sliderType].MAX;
514     slider.step = SLIDER[sliderType].STEP;
515     slider.title = L10N.getStr(`colorPickerTooltip.${sliderType}SliderTitle`);
516     slider.addEventListener("input", onSliderMove);
518     container.appendChild(slider);
519     return slider;
520   },
522   /**
523    * Updates the contrast label with appropriate content (i.e. large text indicator
524    * if the contrast is calculated for large text, or a base label otherwise)
525    *
526    * @param  {Boolean} isLargeText
527    *         True if contrast is calculated for large text.
528    */
529   updateContrastLabel: function(isLargeText) {
530     if (!isLargeText) {
531       this.contrastLabel.textContent = L10N.getStr(
532         "accessibility.contrast.ratio.label"
533       );
534       return;
535     }
537     // Clear previously appended children before appending any new children
538     while (this.contrastLabel.firstChild) {
539       this.contrastLabel.firstChild.remove();
540     }
542     const largeTextStr = L10N.getStr("accessibility.contrast.large.text");
543     const contrastLabelStr = L10N.getFormatStr(
544       "colorPickerTooltip.contrast.large.title",
545       largeTextStr
546     );
548     // Build an array of children nodes for the contrast label element
549     const contents = contrastLabelStr
550       .split(new RegExp(largeTextStr), 2)
551       .map(content => this.document.createTextNode(content));
552     const largeTextIndicator = this.document.createElementNS(XHTML_NS, "span");
553     largeTextIndicator.className = "accessibility-color-contrast-large-text";
554     largeTextIndicator.textContent = largeTextStr;
555     largeTextIndicator.title = L10N.getStr(
556       "accessibility.contrast.large.title"
557     );
558     contents.splice(1, 0, largeTextIndicator);
560     // Append children to contrast label
561     for (const content of contents) {
562       this.contrastLabel.appendChild(content);
563     }
564   },
566   /**
567    * Updates a contrast value element with the given score, value and swatches.
568    *
569    * @param  {DOMNode} el
570    *         Contrast value element to update.
571    * @param  {String} score
572    *         Contrast ratio score.
573    * @param  {Number} value
574    *         Contrast ratio value.
575    * @param  {Array} backgroundColor
576    *         RGBA color array for the background color to show in the swatch.
577    */
578   updateContrastValueEl: function(el, score, value, backgroundColor) {
579     el.classList.toggle(score, true);
580     el.textContent = value.toFixed(2);
581     el.title = L10N.getFormatStr(
582       `accessibility.contrast.annotation.${score}`,
583       L10N.getFormatStr(
584         "colorPickerTooltip.contrastAgainstBgTitle",
585         `rgba(${backgroundColor})`
586       )
587     );
588     el.parentElement.style.setProperty(
589       "--accessibility-contrast-color",
590       this.rgbCssString
591     );
592     el.parentElement.style.setProperty(
593       "--accessibility-contrast-bg",
594       `rgba(${backgroundColor})`
595     );
596   },
598   updateAlphaSlider: function() {
599     // Set alpha slider background
600     const rgb = this.rgb;
602     const rgbNoAlpha = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")";
603     const rgbAlpha0 = "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ", 0)";
604     const alphaGradient =
605       "linear-gradient(to right, " + rgbAlpha0 + ", " + rgbNoAlpha + ")";
606     this.alphaSlider.style.background = alphaGradient;
607   },
609   updateColorPreview: function() {
610     // Overlay the rgba color over a checkered image background.
611     this.colorPreview.style.setProperty("--overlay-color", this.rgbCssString);
613     // We should be able to distinguish the color preview on high luminance rgba values.
614     // Give the color preview a light grey border if the luminance of the current rgba
615     // tuple is great.
616     const colorLuminance = colorUtils.calculateLuminance(this.rgb);
617     this.colorPreview.classList.toggle("high-luminance", colorLuminance > 0.85);
619     // Set title on color preview for better UX
620     this.colorPreview.title = this.colorName;
621   },
623   updateDragger: function() {
624     // Set dragger background color
625     const flatColor =
626       "rgb(" +
627       this.rgbNoSatVal[0] +
628       ", " +
629       this.rgbNoSatVal[1] +
630       ", " +
631       this.rgbNoSatVal[2] +
632       ")";
633     this.dragger.style.backgroundColor = flatColor;
635     // Set dragger aria attributes
636     this.dragger.setAttribute("aria-valuetext", this.rgbCssString);
637   },
639   updateHueSlider: function() {
640     // Set hue slider aria attributes
641     this.hueSlider.setAttribute("aria-valuetext", this.rgbCssString);
642   },
644   updateHelperLocations: function() {
645     const h = this.hsv[0];
646     const s = this.hsv[1];
647     const v = this.hsv[2];
649     // Placing the color dragger
650     let dragX = s * this.dragWidth;
651     let dragY = this.dragHeight - v * this.dragHeight;
652     const helperDim = this.dragHelperHeight / 2;
654     dragX = Math.max(
655       -helperDim,
656       Math.min(this.dragWidth - helperDim, dragX - helperDim)
657     );
658     dragY = Math.max(
659       -helperDim,
660       Math.min(this.dragHeight - helperDim, dragY - helperDim)
661     );
663     this.dragHelper.style.top = dragY + "px";
664     this.dragHelper.style.left = dragX + "px";
666     // Placing the hue slider
667     this.hueSlider.value = h * this.hueSlider.max;
669     // Placing the alpha slider
670     this.alphaSlider.value = this.hsv[3] * this.alphaSlider.max;
671   },
673   /* Calculates the contrast ratio for the currently selected
674    * color against a single or range of background colors and displays contrast ratio section
675    * components depending on the contrast ratio calculated.
676    *
677    * Contrast ratio components include:
678    *    - contrastLargeTextIndicator: Hidden by default, shown when text has large font
679    *                                  size if there is no error in calculation.
680    *    - contrastValue(s):           Set to calculated value(s), score(s) and text color on
681    *                                  background swatches. Set to error text
682    *                                  if there is an error in calculation.
683    */
684   updateContrast: function() {
685     // Remove additional classes on spectrum contrast, leaving behind only base classes
686     this.spectrumContrast.classList.toggle("visible", false);
687     this.spectrumContrast.classList.toggle("range", false);
688     this.spectrumContrast.classList.toggle("error", false);
689     // Assign only base class to all contrastValues, removing any score class
690     this.contrastValue.className = this.contrastValueMin.className = this.contrastValueMax.className =
691       "accessibility-contrast-value";
693     if (!this.contrastEnabled) {
694       return;
695     }
697     const isRange = this.backgroundColorData.min !== undefined;
698     this.spectrumContrast.classList.toggle("visible", true);
699     this.spectrumContrast.classList.toggle("range", isRange);
701     const colorContrast = getContrastRatio(
702       {
703         ...this.textProps,
704         color: this.rgbCssString,
705       },
706       this.backgroundColorData
707     );
709     const {
710       value,
711       min,
712       max,
713       score,
714       scoreMin,
715       scoreMax,
716       backgroundColor,
717       backgroundColorMin,
718       backgroundColorMax,
719       isLargeText,
720       error,
721     } = colorContrast;
723     if (error) {
724       this.updateContrastLabel(false);
725       this.spectrumContrast.classList.toggle("error", true);
727       // If current background color is a range, show the error text in the contrast range
728       // span. Otherwise, show it in the single contrast span.
729       const contrastValEl = isRange
730         ? this.contrastValueMin
731         : this.contrastValue;
732       contrastValEl.textContent = L10N.getStr("accessibility.contrast.error");
733       contrastValEl.title = L10N.getStr(
734         "accessibility.contrast.annotation.transparent.error"
735       );
737       return;
738     }
740     this.updateContrastLabel(isLargeText);
741     if (!isRange) {
742       this.updateContrastValueEl(
743         this.contrastValue,
744         score,
745         value,
746         backgroundColor
747       );
749       return;
750     }
752     this.updateContrastValueEl(
753       this.contrastValueMin,
754       scoreMin,
755       min,
756       backgroundColorMin
757     );
758     this.updateContrastValueEl(
759       this.contrastValueMax,
760       scoreMax,
761       max,
762       backgroundColorMax
763     );
764   },
766   updateUI: function() {
767     this.updateHelperLocations();
769     this.updateColorPreview();
770     this.updateDragger();
771     this.updateHueSlider();
772     this.updateAlphaSlider();
773     this.updateContrast();
774   },
776   destroy: function() {
777     this.element.removeEventListener("click", this.onElementClick);
778     this.hueSlider.removeEventListener("input", this.onHueSliderMove);
779     this.alphaSlider.removeEventListener("input", this.onAlphaSliderMove);
781     this.parentEl.removeChild(this.element);
783     this.dragger = this.dragHelper = null;
784     this.alphaSlider = null;
785     this.hueSlider = null;
786     this.colorPreview = null;
787     this.element = null;
788     this.parentEl = null;
789     this.spectrumContrast = null;
790     this.contrastValue = this.contrastValueMin = this.contrastValueMax = null;
791     this.contrastLabel = null;
792   },