783 lines
22 KiB
JavaScript
783 lines
22 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
"use strict";
|
|
|
|
const EventEmitter = require("resource://devtools/shared/event-emitter.js");
|
|
const {
|
|
MultiLocalizationHelper,
|
|
} = require("resource://devtools/shared/l10n.js");
|
|
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"colorUtils",
|
|
"resource://devtools/shared/css/color.js",
|
|
true
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
"labColors",
|
|
"resource://devtools/shared/css/color-db.js",
|
|
true
|
|
);
|
|
loader.lazyRequireGetter(
|
|
this,
|
|
["getTextProperties", "getContrastRatioAgainstBackground"],
|
|
"resource://devtools/shared/accessibility.js",
|
|
true
|
|
);
|
|
|
|
const L10N = new MultiLocalizationHelper(
|
|
"devtools/client/locales/accessibility.properties",
|
|
"devtools/client/locales/inspector.properties"
|
|
);
|
|
const ARROW_KEYS = ["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft"];
|
|
const [ArrowUp, ArrowRight, ArrowDown, ArrowLeft] = ARROW_KEYS;
|
|
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
|
const SLIDER = {
|
|
hue: {
|
|
MIN: "0",
|
|
MAX: "128",
|
|
STEP: "1",
|
|
},
|
|
alpha: {
|
|
MIN: "0",
|
|
MAX: "1",
|
|
STEP: "0.01",
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Spectrum creates a color picker widget in any container you give it.
|
|
*
|
|
* Simple usage example:
|
|
*
|
|
* const {Spectrum} = require("devtools/client/shared/widgets/Spectrum");
|
|
* let s = new Spectrum(containerElement, [255, 126, 255, 1]);
|
|
* s.on("changed", (rgba, color) => {
|
|
* console.log("rgba(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + ", " +
|
|
* rgba[3] + ")");
|
|
* });
|
|
* s.show();
|
|
* s.destroy();
|
|
*
|
|
* Note that the color picker is hidden by default and you need to call show to
|
|
* make it appear. This 2 stages initialization helps in cases you are creating
|
|
* the color picker in a parent element that hasn't been appended anywhere yet
|
|
* or that is hidden. Calling show() when the parent element is appended and
|
|
* visible will allow spectrum to correctly initialize its various parts.
|
|
*
|
|
* Fires the following events:
|
|
* - changed : When the user changes the current color
|
|
*/
|
|
class Spectrum {
|
|
constructor(parentEl, rgb) {
|
|
EventEmitter.decorate(this);
|
|
|
|
this.document = parentEl.ownerDocument;
|
|
this.element = parentEl.ownerDocument.createElementNS(XHTML_NS, "div");
|
|
this.parentEl = parentEl;
|
|
|
|
this.element.className = "spectrum-container";
|
|
// eslint-disable-next-line no-unsanitized/property
|
|
this.element.innerHTML = `
|
|
<section class="spectrum-color-picker">
|
|
<div class="spectrum-color spectrum-box"
|
|
tabindex="0"
|
|
role="slider"
|
|
title="${L10N.getStr("colorPickerTooltip.spectrumDraggerTitle")}"
|
|
aria-describedby="spectrum-dragger">
|
|
<div class="spectrum-sat">
|
|
<div class="spectrum-val">
|
|
<div class="spectrum-dragger" id="spectrum-dragger"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<section class="spectrum-controls">
|
|
<div class="spectrum-color-preview"></div>
|
|
<div class="spectrum-slider-container">
|
|
<div class="spectrum-hue spectrum-box"></div>
|
|
<div class="spectrum-alpha spectrum-checker spectrum-box"></div>
|
|
</div>
|
|
</section>
|
|
<section class="spectrum-color-contrast accessibility-color-contrast">
|
|
<div class="contrast-ratio-header-and-single-ratio">
|
|
<span class="contrast-ratio-label" role="presentation"></span>
|
|
<span class="contrast-value-and-swatch contrast-ratio-single" role="presentation">
|
|
<span class="accessibility-contrast-value"></span>
|
|
</span>
|
|
</div>
|
|
<div class="contrast-ratio-range">
|
|
<span class="contrast-value-and-swatch contrast-ratio-min" role="presentation">
|
|
<span class="accessibility-contrast-value"></span>
|
|
</span>
|
|
<span class="accessibility-color-contrast-separator"></span>
|
|
<span class="contrast-value-and-swatch contrast-ratio-max" role="presentation">
|
|
<span class="accessibility-contrast-value"></span>
|
|
</span>
|
|
</div>
|
|
</section>
|
|
`;
|
|
|
|
this.onElementClick = this.onElementClick.bind(this);
|
|
this.element.addEventListener("click", this.onElementClick);
|
|
|
|
this.parentEl.appendChild(this.element);
|
|
|
|
// Color spectrum dragger.
|
|
this.dragger = this.element.querySelector(".spectrum-color");
|
|
this.dragHelper = this.element.querySelector(".spectrum-dragger");
|
|
draggable(this.dragger, this.dragHelper, this.onDraggerMove.bind(this));
|
|
|
|
// Here we define the components for the "controls" section of the color picker.
|
|
this.controls = this.element.querySelector(".spectrum-controls");
|
|
this.colorPreview = this.element.querySelector(".spectrum-color-preview");
|
|
|
|
// Create the eyedropper.
|
|
const eyedropper = this.document.createElementNS(XHTML_NS, "button");
|
|
eyedropper.id = "eyedropper-button";
|
|
eyedropper.className = "devtools-button";
|
|
eyedropper.style.pointerEvents = "auto";
|
|
eyedropper.setAttribute(
|
|
"aria-label",
|
|
L10N.getStr("colorPickerTooltip.eyedropperTitle")
|
|
);
|
|
this.controls.insertBefore(eyedropper, this.colorPreview);
|
|
|
|
// Hue slider and alpha slider
|
|
this.hueSlider = this.createSlider("hue", this.onHueSliderMove.bind(this));
|
|
this.hueSlider.setAttribute("aria-describedby", this.dragHelper.id);
|
|
this.alphaSlider = this.createSlider(
|
|
"alpha",
|
|
this.onAlphaSliderMove.bind(this)
|
|
);
|
|
|
|
// Color contrast
|
|
this.spectrumContrast = this.element.querySelector(
|
|
".spectrum-color-contrast"
|
|
);
|
|
this.contrastLabel = this.element.querySelector(".contrast-ratio-label");
|
|
[this.contrastValue, this.contrastValueMin, this.contrastValueMax] =
|
|
this.element.querySelectorAll(".accessibility-contrast-value");
|
|
|
|
// Create the learn more info button
|
|
const learnMore = this.document.createElementNS(XHTML_NS, "button");
|
|
learnMore.id = "learn-more-button";
|
|
learnMore.className = "learn-more";
|
|
learnMore.title = L10N.getStr("accessibility.learnMore");
|
|
this.element
|
|
.querySelector(".contrast-ratio-header-and-single-ratio")
|
|
.appendChild(learnMore);
|
|
|
|
if (rgb) {
|
|
this.rgb = rgb;
|
|
this.updateUI();
|
|
}
|
|
}
|
|
|
|
set textProps(style) {
|
|
this._textProps = style
|
|
? {
|
|
fontSize: style["font-size"].value,
|
|
fontWeight: style["font-weight"].value,
|
|
opacity: style.opacity.value,
|
|
}
|
|
: null;
|
|
}
|
|
|
|
set rgb(color) {
|
|
this.hsv = rgbToHsv(color[0], color[1], color[2], color[3]);
|
|
}
|
|
|
|
set backgroundColorData(colorData) {
|
|
this._backgroundColorData = colorData;
|
|
}
|
|
|
|
get backgroundColorData() {
|
|
return this._backgroundColorData;
|
|
}
|
|
|
|
get textProps() {
|
|
return this._textProps;
|
|
}
|
|
|
|
get rgb() {
|
|
const rgb = hsvToRgb(this.hsv[0], this.hsv[1], this.hsv[2], this.hsv[3]);
|
|
return [
|
|
Math.round(rgb[0]),
|
|
Math.round(rgb[1]),
|
|
Math.round(rgb[2]),
|
|
Math.round(rgb[3] * 100) / 100,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Map current rgb to the closest color available in the database by
|
|
* calculating the delta-E between each available color and the current rgb
|
|
*
|
|
* @return {String}
|
|
* Color name or closest color name
|
|
*/
|
|
get colorName() {
|
|
const labColorEntries = Object.entries(labColors);
|
|
|
|
const deltaEs = labColorEntries.map(color =>
|
|
colorUtils.calculateDeltaE(color[1], colorUtils.rgbToLab(this.rgb))
|
|
);
|
|
|
|
// Get the color name for the one that has the lowest delta-E
|
|
const minDeltaE = Math.min(...deltaEs);
|
|
const colorName = labColorEntries[deltaEs.indexOf(minDeltaE)][0];
|
|
return minDeltaE === 0
|
|
? colorName
|
|
: L10N.getFormatStr("colorPickerTooltip.colorNameTitle", colorName);
|
|
}
|
|
|
|
get rgbNoSatVal() {
|
|
const rgb = hsvToRgb(this.hsv[0], 1, 1);
|
|
return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2]), rgb[3]];
|
|
}
|
|
|
|
get rgbCssString() {
|
|
const rgb = this.rgb;
|
|
return (
|
|
"rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " + rgb[3] + ")"
|
|
);
|
|
}
|
|
|
|
show() {
|
|
this.dragWidth = this.dragger.offsetWidth;
|
|
this.dragHeight = this.dragger.offsetHeight;
|
|
this.dragHelperHeight = this.dragHelper.offsetHeight;
|
|
|
|
this.updateUI();
|
|
}
|
|
|
|
onElementClick(e) {
|
|
e.stopPropagation();
|
|
}
|
|
|
|
onHueSliderMove() {
|
|
this.hsv[0] = this.hueSlider.value / this.hueSlider.max;
|
|
this.updateUI();
|
|
this.onChange();
|
|
}
|
|
|
|
onDraggerMove(dragX, dragY) {
|
|
this.hsv[1] = dragX / this.dragWidth;
|
|
this.hsv[2] = (this.dragHeight - dragY) / this.dragHeight;
|
|
this.updateUI();
|
|
this.onChange();
|
|
}
|
|
|
|
onAlphaSliderMove() {
|
|
this.hsv[3] = this.alphaSlider.value / this.alphaSlider.max;
|
|
this.updateUI();
|
|
this.onChange();
|
|
}
|
|
|
|
onChange() {
|
|
this.emit("changed", this.rgb, this.rgbCssString);
|
|
}
|
|
|
|
/**
|
|
* Creates and initializes a slider element, attaches it to its parent container
|
|
* based on the slider type and returns it
|
|
*
|
|
* @param {String} sliderType
|
|
* The type of the slider (i.e. alpha or hue)
|
|
* @param {Function} onSliderMove
|
|
* The function to tie the slider to on input
|
|
* @return {DOMNode}
|
|
* Newly created slider
|
|
*/
|
|
createSlider(sliderType, onSliderMove) {
|
|
const container = this.element.querySelector(`.spectrum-${sliderType}`);
|
|
|
|
const slider = this.document.createElementNS(XHTML_NS, "input");
|
|
slider.className = `spectrum-${sliderType}-input`;
|
|
slider.type = "range";
|
|
slider.min = SLIDER[sliderType].MIN;
|
|
slider.max = SLIDER[sliderType].MAX;
|
|
slider.step = SLIDER[sliderType].STEP;
|
|
slider.title = L10N.getStr(`colorPickerTooltip.${sliderType}SliderTitle`);
|
|
slider.addEventListener("input", onSliderMove);
|
|
|
|
container.appendChild(slider);
|
|
return slider;
|
|
}
|
|
|
|
/**
|
|
* Updates the contrast label with appropriate content (i.e. large text indicator
|
|
* if the contrast is calculated for large text, or a base label otherwise)
|
|
*
|
|
* @param {Boolean} isLargeText
|
|
* True if contrast is calculated for large text.
|
|
*/
|
|
updateContrastLabel(isLargeText) {
|
|
if (!isLargeText) {
|
|
this.contrastLabel.textContent = L10N.getStr(
|
|
"accessibility.contrast.ratio.label"
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Clear previously appended children before appending any new children
|
|
while (this.contrastLabel.firstChild) {
|
|
this.contrastLabel.firstChild.remove();
|
|
}
|
|
|
|
const largeTextStr = L10N.getStr("accessibility.contrast.large.text");
|
|
const contrastLabelStr = L10N.getFormatStr(
|
|
"colorPickerTooltip.contrast.large.title",
|
|
largeTextStr
|
|
);
|
|
|
|
// Build an array of children nodes for the contrast label element
|
|
const contents = contrastLabelStr
|
|
.split(new RegExp(largeTextStr), 2)
|
|
.map(content => this.document.createTextNode(content));
|
|
const largeTextIndicator = this.document.createElementNS(XHTML_NS, "span");
|
|
largeTextIndicator.className = "accessibility-color-contrast-large-text";
|
|
largeTextIndicator.textContent = largeTextStr;
|
|
largeTextIndicator.title = L10N.getStr(
|
|
"accessibility.contrast.large.title"
|
|
);
|
|
contents.splice(1, 0, largeTextIndicator);
|
|
|
|
// Append children to contrast label
|
|
for (const content of contents) {
|
|
this.contrastLabel.appendChild(content);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates a contrast value element with the given score, value and swatches.
|
|
*
|
|
* @param {DOMNode} el
|
|
* Contrast value element to update.
|
|
* @param {String} score
|
|
* Contrast ratio score.
|
|
* @param {Number} value
|
|
* Contrast ratio value.
|
|
* @param {Array} backgroundColor
|
|
* RGBA color array for the background color to show in the swatch.
|
|
*/
|
|
updateContrastValueEl(el, score, value, backgroundColor) {
|
|
el.classList.toggle(score, true);
|
|
el.textContent = value.toFixed(2);
|
|
el.title = L10N.getFormatStr(
|
|
`accessibility.contrast.annotation.${score}`,
|
|
L10N.getFormatStr(
|
|
"colorPickerTooltip.contrastAgainstBgTitle",
|
|
`rgba(${backgroundColor})`
|
|
)
|
|
);
|
|
el.parentElement.style.setProperty(
|
|
"--accessibility-contrast-color",
|
|
this.rgbCssString
|
|
);
|
|
el.parentElement.style.setProperty(
|
|
"--accessibility-contrast-bg",
|
|
`rgba(${backgroundColor})`
|
|
);
|
|
}
|
|
|
|
updateAlphaSlider() {
|
|
// Set alpha slider background
|
|
const rgb = this.rgb;
|
|
|
|
const rgbNoAlpha = "rgb(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ")";
|
|
const rgbAlpha0 = "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + ", 0)";
|
|
const alphaGradient =
|
|
"linear-gradient(to right, " + rgbAlpha0 + ", " + rgbNoAlpha + ")";
|
|
this.alphaSlider.style.background = alphaGradient;
|
|
}
|
|
|
|
updateColorPreview() {
|
|
// Overlay the rgba color over a checkered image background.
|
|
this.colorPreview.style.setProperty("--overlay-color", this.rgbCssString);
|
|
|
|
// We should be able to distinguish the color preview on high luminance rgba values.
|
|
// Give the color preview a light grey border if the luminance of the current rgba
|
|
// tuple is great.
|
|
const colorLuminance = colorUtils.calculateLuminance(this.rgb);
|
|
this.colorPreview.classList.toggle("high-luminance", colorLuminance > 0.85);
|
|
|
|
// Set title on color preview for better UX
|
|
this.colorPreview.title = this.colorName;
|
|
}
|
|
|
|
updateDragger() {
|
|
// Set dragger background color
|
|
const flatColor =
|
|
"rgb(" +
|
|
this.rgbNoSatVal[0] +
|
|
", " +
|
|
this.rgbNoSatVal[1] +
|
|
", " +
|
|
this.rgbNoSatVal[2] +
|
|
")";
|
|
this.dragger.style.backgroundColor = flatColor;
|
|
|
|
// Set dragger aria attributes
|
|
this.dragger.setAttribute("aria-valuetext", this.rgbCssString);
|
|
}
|
|
|
|
updateHueSlider() {
|
|
// Set hue slider aria attributes
|
|
this.hueSlider.setAttribute("aria-valuetext", this.rgbCssString);
|
|
}
|
|
|
|
updateHelperLocations() {
|
|
const h = this.hsv[0];
|
|
const s = this.hsv[1];
|
|
const v = this.hsv[2];
|
|
|
|
// Placing the color dragger
|
|
let dragX = s * this.dragWidth;
|
|
let dragY = this.dragHeight - v * this.dragHeight;
|
|
const helperDim = this.dragHelperHeight / 2;
|
|
|
|
dragX = Math.max(
|
|
-helperDim,
|
|
Math.min(this.dragWidth - helperDim, dragX - helperDim)
|
|
);
|
|
dragY = Math.max(
|
|
-helperDim,
|
|
Math.min(this.dragHeight - helperDim, dragY - helperDim)
|
|
);
|
|
|
|
this.dragHelper.style.top = dragY + "px";
|
|
this.dragHelper.style.left = dragX + "px";
|
|
|
|
// Placing the hue slider
|
|
this.hueSlider.value = h * this.hueSlider.max;
|
|
|
|
// Placing the alpha slider
|
|
this.alphaSlider.value = this.hsv[3] * this.alphaSlider.max;
|
|
}
|
|
|
|
/* Calculates the contrast ratio for the currently selected
|
|
* color against a single or range of background colors and displays contrast ratio section
|
|
* components depending on the contrast ratio calculated.
|
|
*
|
|
* Contrast ratio components include:
|
|
* - contrastLargeTextIndicator: Hidden by default, shown when text has large font
|
|
* size if there is no error in calculation.
|
|
* - contrastValue(s): Set to calculated value(s), score(s) and text color on
|
|
* background swatches. Set to error text
|
|
* if there is an error in calculation.
|
|
*/
|
|
updateContrast() {
|
|
// Remove additional classes on spectrum contrast, leaving behind only base classes
|
|
this.spectrumContrast.classList.toggle("visible", false);
|
|
this.spectrumContrast.classList.toggle("range", false);
|
|
this.spectrumContrast.classList.toggle("error", false);
|
|
// Assign only base class to all contrastValues, removing any score class
|
|
this.contrastValue.className =
|
|
this.contrastValueMin.className =
|
|
this.contrastValueMax.className =
|
|
"accessibility-contrast-value";
|
|
|
|
if (!this.contrastEnabled) {
|
|
return;
|
|
}
|
|
|
|
const isRange = this.backgroundColorData.min !== undefined;
|
|
this.spectrumContrast.classList.toggle("visible", true);
|
|
this.spectrumContrast.classList.toggle("range", isRange);
|
|
|
|
const colorContrast = getContrastRatio(
|
|
{
|
|
...this.textProps,
|
|
color: this.rgbCssString,
|
|
},
|
|
this.backgroundColorData
|
|
);
|
|
|
|
const {
|
|
value,
|
|
min,
|
|
max,
|
|
score,
|
|
scoreMin,
|
|
scoreMax,
|
|
backgroundColor,
|
|
backgroundColorMin,
|
|
backgroundColorMax,
|
|
isLargeText,
|
|
error,
|
|
} = colorContrast;
|
|
|
|
if (error) {
|
|
this.updateContrastLabel(false);
|
|
this.spectrumContrast.classList.toggle("error", true);
|
|
|
|
// If current background color is a range, show the error text in the contrast range
|
|
// span. Otherwise, show it in the single contrast span.
|
|
const contrastValEl = isRange
|
|
? this.contrastValueMin
|
|
: this.contrastValue;
|
|
contrastValEl.textContent = L10N.getStr("accessibility.contrast.error");
|
|
contrastValEl.title = L10N.getStr(
|
|
"accessibility.contrast.annotation.transparent.error"
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
this.updateContrastLabel(isLargeText);
|
|
if (!isRange) {
|
|
this.updateContrastValueEl(
|
|
this.contrastValue,
|
|
score,
|
|
value,
|
|
backgroundColor
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
this.updateContrastValueEl(
|
|
this.contrastValueMin,
|
|
scoreMin,
|
|
min,
|
|
backgroundColorMin
|
|
);
|
|
this.updateContrastValueEl(
|
|
this.contrastValueMax,
|
|
scoreMax,
|
|
max,
|
|
backgroundColorMax
|
|
);
|
|
}
|
|
|
|
updateUI() {
|
|
this.updateHelperLocations();
|
|
|
|
this.updateColorPreview();
|
|
this.updateDragger();
|
|
this.updateHueSlider();
|
|
this.updateAlphaSlider();
|
|
this.updateContrast();
|
|
}
|
|
|
|
destroy() {
|
|
this.element.removeEventListener("click", this.onElementClick);
|
|
this.hueSlider.removeEventListener("input", this.onHueSliderMove);
|
|
this.alphaSlider.removeEventListener("input", this.onAlphaSliderMove);
|
|
|
|
this.parentEl.removeChild(this.element);
|
|
|
|
this.dragger = this.dragHelper = null;
|
|
this.alphaSlider = null;
|
|
this.hueSlider = null;
|
|
this.colorPreview = null;
|
|
this.element = null;
|
|
this.parentEl = null;
|
|
this.spectrumContrast = null;
|
|
this.contrastValue = this.contrastValueMin = this.contrastValueMax = null;
|
|
this.contrastLabel = null;
|
|
}
|
|
}
|
|
|
|
function hsvToRgb(h, s, v, a) {
|
|
let r, g, b;
|
|
|
|
const i = Math.floor(h * 6);
|
|
const f = h * 6 - i;
|
|
const p = v * (1 - s);
|
|
const q = v * (1 - f * s);
|
|
const t = v * (1 - (1 - f) * s);
|
|
|
|
switch (i % 6) {
|
|
case 0:
|
|
r = v;
|
|
g = t;
|
|
b = p;
|
|
break;
|
|
case 1:
|
|
r = q;
|
|
g = v;
|
|
b = p;
|
|
break;
|
|
case 2:
|
|
r = p;
|
|
g = v;
|
|
b = t;
|
|
break;
|
|
case 3:
|
|
r = p;
|
|
g = q;
|
|
b = v;
|
|
break;
|
|
case 4:
|
|
r = t;
|
|
g = p;
|
|
b = v;
|
|
break;
|
|
case 5:
|
|
r = v;
|
|
g = p;
|
|
b = q;
|
|
break;
|
|
}
|
|
|
|
return [r * 255, g * 255, b * 255, a];
|
|
}
|
|
|
|
function rgbToHsv(r, g, b, a) {
|
|
r = r / 255;
|
|
g = g / 255;
|
|
b = b / 255;
|
|
|
|
const max = Math.max(r, g, b);
|
|
const min = Math.min(r, g, b);
|
|
|
|
const v = max;
|
|
const d = max - min;
|
|
const s = max == 0 ? 0 : d / max;
|
|
|
|
let h;
|
|
if (max == min) {
|
|
// achromatic
|
|
h = 0;
|
|
} else {
|
|
switch (max) {
|
|
case r:
|
|
h = (g - b) / d + (g < b ? 6 : 0);
|
|
break;
|
|
case g:
|
|
h = (b - r) / d + 2;
|
|
break;
|
|
case b:
|
|
h = (r - g) / d + 4;
|
|
break;
|
|
}
|
|
h /= 6;
|
|
}
|
|
return [h, s, v, a];
|
|
}
|
|
|
|
function draggable(element, dragHelper, onmove) {
|
|
onmove = onmove || function () {};
|
|
|
|
const doc = element.ownerDocument;
|
|
let dragging = false;
|
|
let offset = {};
|
|
let maxHeight = 0;
|
|
let maxWidth = 0;
|
|
|
|
function setDraggerDimensionsAndOffset() {
|
|
maxHeight = element.offsetHeight;
|
|
maxWidth = element.offsetWidth;
|
|
offset = element.getBoundingClientRect();
|
|
}
|
|
|
|
function prevent(e) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
}
|
|
|
|
function move(e) {
|
|
if (dragging) {
|
|
if (e.buttons === 0) {
|
|
// The button is no longer pressed but we did not get a mouseup event.
|
|
stop();
|
|
return;
|
|
}
|
|
const pageX = e.pageX;
|
|
const pageY = e.pageY;
|
|
|
|
const dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth));
|
|
const dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight));
|
|
|
|
onmove.apply(element, [dragX, dragY]);
|
|
}
|
|
}
|
|
|
|
function start(e) {
|
|
const rightClick = e.which === 3;
|
|
|
|
if (!rightClick && !dragging) {
|
|
dragging = true;
|
|
setDraggerDimensionsAndOffset();
|
|
|
|
move(e);
|
|
|
|
doc.addEventListener("selectstart", prevent);
|
|
doc.addEventListener("dragstart", prevent);
|
|
doc.addEventListener("mousemove", move);
|
|
doc.addEventListener("mouseup", stop);
|
|
|
|
prevent(e);
|
|
}
|
|
}
|
|
|
|
function stop() {
|
|
if (dragging) {
|
|
doc.removeEventListener("selectstart", prevent);
|
|
doc.removeEventListener("dragstart", prevent);
|
|
doc.removeEventListener("mousemove", move);
|
|
doc.removeEventListener("mouseup", stop);
|
|
}
|
|
dragging = false;
|
|
}
|
|
|
|
function onKeydown(e) {
|
|
const { key } = e;
|
|
|
|
if (!ARROW_KEYS.includes(key)) {
|
|
return;
|
|
}
|
|
|
|
setDraggerDimensionsAndOffset();
|
|
const { offsetHeight, offsetTop, offsetLeft } = dragHelper;
|
|
let dragX = offsetLeft + offsetHeight / 2;
|
|
let dragY = offsetTop + offsetHeight / 2;
|
|
|
|
if (key === ArrowLeft && dragX > 0) {
|
|
dragX -= 1;
|
|
} else if (key === ArrowRight && dragX < maxWidth) {
|
|
dragX += 1;
|
|
} else if (key === ArrowUp && dragY > 0) {
|
|
dragY -= 1;
|
|
} else if (key === ArrowDown && dragY < maxHeight) {
|
|
dragY += 1;
|
|
}
|
|
|
|
onmove.apply(element, [dragX, dragY]);
|
|
}
|
|
|
|
element.addEventListener("mousedown", start);
|
|
element.addEventListener("keydown", onKeydown);
|
|
}
|
|
|
|
/**
|
|
* Calculates the contrast ratio for a DOM node's computed style against
|
|
* a given background.
|
|
*
|
|
* @param {Object} computedStyle
|
|
* The computed style for which we want to calculate the contrast ratio.
|
|
* @param {Object} backgroundColor
|
|
* Object with one or more of the following properties: value, min, max
|
|
* @return {Object}
|
|
* An object that may contain one or more of the following fields: error,
|
|
* isLargeText, value, score for contrast.
|
|
*/
|
|
function getContrastRatio(computedStyle, backgroundColor) {
|
|
const props = getTextProperties(computedStyle);
|
|
|
|
if (!props) {
|
|
return {
|
|
error: true,
|
|
};
|
|
}
|
|
|
|
return getContrastRatioAgainstBackground(backgroundColor, props);
|
|
}
|
|
|
|
module.exports = Spectrum;
|