import { UiElement } from "../UiElement.js";
import { generateCssGradient, copyColorScale, stretchColorScale, invertColorScale } from "./Utils.js";
import "./ColorSlider.css";
export { ColorSlider }
/**
* color scale slider for changing the values of a color scale.
* Used internally by ColorScalePicker
*
* @author rhess <robin.hess@awi.de>
* @memberof vef.ui.color
*/
class ColorSlider extends UiElement {
/**
* @param {HTMLElement | string} target
* @param {object[]} colorScale initial color scale
*
* events:
* slide: on dragging handles with mouse
* stop: stop dragging handles with mouse
* select: click/select handle with mouse
* deselect: no handle is selected anymore
*/
constructor(target, colorScale) {
super(target, {
'slide': [],
'stop': [],
'select': [],
'deselect': [],
'update': []
});
this.handles_ = [];
this.min_ = 0;
this.max_ = 1;
this.slider_ = document.createElement("div");
this.selectedHandle_ = null;
// create the element
const element = this.getElement();
element.classList.add("color-slider-container");
this.slider_.classList.add("color-slider");
element.appendChild(this.slider_);
this.setColorScale(colorScale || [
{
value: 0,
opacity: 1,
color: "#000000"
},
{
value: 1,
opacity: 1,
color: "#ffffff"
}
]);
// update slider scale when the window gets resized
const resizeObserver = new ResizeObserver(() => this.update());
resizeObserver.observe(this.slider_);
}
/**
* stretch values linear from min to max
*
* @param {number} min
* @param {number} max
*/
stretchToScale(min, max) {
stretchColorScale(this.handles_, min, max);
if (this.selectedHandle_) this.selectHandle(this.selectedHandle_);
this.update(true);
}
/**
* Invert the color scale value order
*
* @param {number} min
* @param {number} max
*/
invertScale() {
invertColorScale(this.handles_);
this.handles_.sort((a, b) => a.value - b.value);
if (this.selectedHandle_) this.selectHandle(this.selectedHandle_);
this.update(true);
}
/**
* get the currently selected handle object
*
* @returns {object}
*/
getSelectedHandle() {
return this.selectedHandle_;
}
/**
* Get a handle objec by index
*
* @param {number} index
* @returns {object}
*/
getHandle(index) {
return this.handles_[index];
}
/**
* Get the amount of items/handles
*
* @returns {number}
*/
getLength() {
return this.handles_.length;
}
/**
* Select a handle object and trigger the "select" event
*
* @param {object} handle
*/
selectHandle(handle) {
// move handle to front
for (let i in this.handles_) {
this.handles_[i].element.style["box-shadow"] = "unset";
}
this.slider_.appendChild(handle.element);
handle.element.style["box-shadow"] = "0 0 0 1px" + handle.color;
this.selectedHandle_ = handle;
this.fire("select", this);
}
/**
* set the ColorSlider to deselected and trigger
* the "deselect" event
*/
deselectHandle() {
// move handle to front
for (let i in this.handles_) {
this.handles_[i].element.style["box-shadow"] = "unset";
}
this.selectedHandle_ = null;
this.fire("deselect", this);
}
/**
* get the current color scale as an array
*
* @returns [object[]]
*/
getColorScale() {
return copyColorScale(this.handles_);
}
/**
* Set the ColorSlider Values from an existing
* color scale array
*
* @param {object[]} colorScale
*/
setColorScale(colorScale) {
this.clear();
if (colorScale.length > 0) {
this.min_ = colorScale[0].value;
this.max_ = colorScale[0].value;
for (let i = 0; i < colorScale.length; ++i) {
this.addHandle(colorScale[i]);
}
}
}
/**
* Add a ne handle (color-item)
* to the ColorSlider
*
* options = {
* color: "#000000",
* value: 0,
* opacity: 1
* }
*
* @param {object} options
*/
addHandle(options) {
const handle = {
value: options.value || this.max_,
color: options.color || "#000000",
opacity: options.opacity || 1,
element: document.createElement("div")
};
handle.element.classList.add("handle");
this.slider_.appendChild(handle.element);
let down, width, startPosition, valueExtent;
const mousemove = (event) => {
if (!down) return;
let position = event.clientX - startPosition;
if (event.clientX < startPosition) {
position = 0;
} else if (event.clientX > startPosition + width) {
position = width;
}
handle.value = this.min_ + (valueExtent * (position / width));
handle.element.style.left = position + 'px';
this.updateGradient_();
this.fire('slide', this);
};
const mousedown = (event) => {
event.preventDefault();
const sliderRect = this.slider_.getBoundingClientRect();
const handleRect = handle.element.getBoundingClientRect();
down = true;
width = this.slider_.offsetWidth;
startPosition = sliderRect.x;
valueExtent = this.max_ - this.min_;
document.addEventListener('mouseup', mouseup);
document.addEventListener('mousemove', mousemove);
this.selectHandle(handle);
};
const mouseup = () => {
down = false;
document.removeEventListener('mousemove', mousemove);
document.removeEventListener('mouseup', mouseup);
this.updateGradient_();
this.fire('stop', this);
};
handle.element.addEventListener('mousedown', mousedown);
this.handles_.push(handle);
this.update();
return handle;
}
/**
* Remove a handle object from the ColorSlider
*
* @param {object} handle
*/
removeHandle(handle) {
if (this.selectedHandle_ == handle) this.deselectHandle();
this.handles_.splice(this.handles_.indexOf(handle), 1);
handle.element.remove();
handle.value = null;
handle.color = null;
handle.opacity = null;
handle.element = null
this.updateGradient_();
}
/**
* Remove al handles/color-ítems
*/
clear() {
// remove in reverse order
for (let i = this.handles_.length - 1; i >= 0; --i) {
this.removeHandle(this.handles_[i]);
}
this.updateGradient_();
}
/**
* re-render the color gradient using css
*
* @private
*/
updateGradient_() {
this.handles_.sort((a, b) => a.value - b.value);
const gradient = (this.handles_.length > 0) ? generateCssGradient(this.handles_, this.min_, this.max_) : "";
this.slider_.style.background = gradient;
}
/**
* Update the positions of the handles based on their values and
* re-render the color gradient
*
* @param {boolean} autoMinMax stretch min/max to value extent
*/
update(autoMinMax) {
if (this.handles_.length == 0) return;
let min = this.handles_[0].value;
let max = this.min_;
for (let i = 0; i < this.handles_.length; ++i) {
if (this.handles_[i].value > max) max = this.handles_[i].value;
if (this.handles_[i].value < min) min = this.handles_[i].value;
}
this.min_ = (autoMinMax || (min < this.min_)) ? min : this.min_;
this.max_ = (autoMinMax || (max > this.max_)) ? max : this.max_;
const rangeSize = this.slider_.offsetWidth;
const valueRange = this.max_ - this.min_;
const valueToPosition = (handle) => {
const position = rangeSize * ((handle.value - this.min_) / valueRange);
handle.element.style.left = position + "px";
};
for (let i = 0; i < this.handles_.length; ++i) {
valueToPosition(this.handles_[i]);
this.handles_[i].element.style.background = this.handles_[i].color;
}
this.updateGradient_();
this.fire("update", this);
}
}