Source: ui/color/ColorSlider.js

  1. import { UiElement } from "../UiElement.js";
  2. import { generateCssGradient, copyColorScale, stretchColorScale, invertColorScale } from "./Utils.js";
  3. import "./ColorSlider.css";
  4. export { ColorSlider }
  5. /**
  6. * color scale slider for changing the values of a color scale.
  7. * Used internally by ColorScalePicker
  8. *
  9. * @author rhess <robin.hess@awi.de>
  10. * @memberof vef.ui.color
  11. */
  12. class ColorSlider extends UiElement {
  13. /**
  14. * @param {HTMLElement | string} target
  15. * @param {object[]} colorScale initial color scale
  16. *
  17. * events:
  18. * slide: on dragging handles with mouse
  19. * stop: stop dragging handles with mouse
  20. * select: click/select handle with mouse
  21. * deselect: no handle is selected anymore
  22. */
  23. constructor(target, colorScale) {
  24. super(target, {
  25. 'slide': [],
  26. 'stop': [],
  27. 'select': [],
  28. 'deselect': [],
  29. 'update': []
  30. });
  31. this.handles_ = [];
  32. this.min_ = 0;
  33. this.max_ = 1;
  34. this.slider_ = document.createElement("div");
  35. this.selectedHandle_ = null;
  36. // create the element
  37. const element = this.getElement();
  38. element.classList.add("color-slider-container");
  39. this.slider_.classList.add("color-slider");
  40. element.appendChild(this.slider_);
  41. this.setColorScale(colorScale || [
  42. {
  43. value: 0,
  44. opacity: 1,
  45. color: "#000000"
  46. },
  47. {
  48. value: 1,
  49. opacity: 1,
  50. color: "#ffffff"
  51. }
  52. ]);
  53. // update slider scale when the window gets resized
  54. const resizeObserver = new ResizeObserver(() => this.update());
  55. resizeObserver.observe(this.slider_);
  56. }
  57. /**
  58. * stretch values linear from min to max
  59. *
  60. * @param {number} min
  61. * @param {number} max
  62. */
  63. stretchToScale(min, max) {
  64. stretchColorScale(this.handles_, min, max);
  65. if (this.selectedHandle_) this.selectHandle(this.selectedHandle_);
  66. this.update(true);
  67. }
  68. /**
  69. * Invert the color scale value order
  70. *
  71. * @param {number} min
  72. * @param {number} max
  73. */
  74. invertScale() {
  75. invertColorScale(this.handles_);
  76. this.handles_.sort((a, b) => a.value - b.value);
  77. if (this.selectedHandle_) this.selectHandle(this.selectedHandle_);
  78. this.update(true);
  79. }
  80. /**
  81. * get the currently selected handle object
  82. *
  83. * @returns {object}
  84. */
  85. getSelectedHandle() {
  86. return this.selectedHandle_;
  87. }
  88. /**
  89. * Get a handle objec by index
  90. *
  91. * @param {number} index
  92. * @returns {object}
  93. */
  94. getHandle(index) {
  95. return this.handles_[index];
  96. }
  97. /**
  98. * Get the amount of items/handles
  99. *
  100. * @returns {number}
  101. */
  102. getLength() {
  103. return this.handles_.length;
  104. }
  105. /**
  106. * Select a handle object and trigger the "select" event
  107. *
  108. * @param {object} handle
  109. */
  110. selectHandle(handle) {
  111. // move handle to front
  112. for (let i in this.handles_) {
  113. this.handles_[i].element.style["box-shadow"] = "unset";
  114. }
  115. this.slider_.appendChild(handle.element);
  116. handle.element.style["box-shadow"] = "0 0 0 1px" + handle.color;
  117. this.selectedHandle_ = handle;
  118. this.fire("select", this);
  119. }
  120. /**
  121. * set the ColorSlider to deselected and trigger
  122. * the "deselect" event
  123. */
  124. deselectHandle() {
  125. // move handle to front
  126. for (let i in this.handles_) {
  127. this.handles_[i].element.style["box-shadow"] = "unset";
  128. }
  129. this.selectedHandle_ = null;
  130. this.fire("deselect", this);
  131. }
  132. /**
  133. * get the current color scale as an array
  134. *
  135. * @returns [object[]]
  136. */
  137. getColorScale() {
  138. return copyColorScale(this.handles_);
  139. }
  140. /**
  141. * Set the ColorSlider Values from an existing
  142. * color scale array
  143. *
  144. * @param {object[]} colorScale
  145. */
  146. setColorScale(colorScale) {
  147. this.clear();
  148. if (colorScale.length > 0) {
  149. this.min_ = colorScale[0].value;
  150. this.max_ = colorScale[0].value;
  151. for (let i = 0; i < colorScale.length; ++i) {
  152. this.addHandle(colorScale[i]);
  153. }
  154. }
  155. }
  156. /**
  157. * Add a ne handle (color-item)
  158. * to the ColorSlider
  159. *
  160. * options = {
  161. * color: "#000000",
  162. * value: 0,
  163. * opacity: 1
  164. * }
  165. *
  166. * @param {object} options
  167. */
  168. addHandle(options) {
  169. const handle = {
  170. value: options.value || this.max_,
  171. color: options.color || "#000000",
  172. opacity: options.opacity || 1,
  173. element: document.createElement("div")
  174. };
  175. handle.element.classList.add("handle");
  176. this.slider_.appendChild(handle.element);
  177. let down, width, startPosition, valueExtent;
  178. const mousemove = (event) => {
  179. if (!down) return;
  180. let position = event.clientX - startPosition;
  181. if (event.clientX < startPosition) {
  182. position = 0;
  183. } else if (event.clientX > startPosition + width) {
  184. position = width;
  185. }
  186. handle.value = this.min_ + (valueExtent * (position / width));
  187. handle.element.style.left = position + 'px';
  188. this.updateGradient_();
  189. this.fire('slide', this);
  190. };
  191. const mousedown = (event) => {
  192. event.preventDefault();
  193. const sliderRect = this.slider_.getBoundingClientRect();
  194. const handleRect = handle.element.getBoundingClientRect();
  195. down = true;
  196. width = this.slider_.offsetWidth;
  197. startPosition = sliderRect.x;
  198. valueExtent = this.max_ - this.min_;
  199. document.addEventListener('mouseup', mouseup);
  200. document.addEventListener('mousemove', mousemove);
  201. this.selectHandle(handle);
  202. };
  203. const mouseup = () => {
  204. down = false;
  205. document.removeEventListener('mousemove', mousemove);
  206. document.removeEventListener('mouseup', mouseup);
  207. this.updateGradient_();
  208. this.fire('stop', this);
  209. };
  210. handle.element.addEventListener('mousedown', mousedown);
  211. this.handles_.push(handle);
  212. this.update();
  213. return handle;
  214. }
  215. /**
  216. * Remove a handle object from the ColorSlider
  217. *
  218. * @param {object} handle
  219. */
  220. removeHandle(handle) {
  221. if (this.selectedHandle_ == handle) this.deselectHandle();
  222. this.handles_.splice(this.handles_.indexOf(handle), 1);
  223. handle.element.remove();
  224. handle.value = null;
  225. handle.color = null;
  226. handle.opacity = null;
  227. handle.element = null
  228. this.updateGradient_();
  229. }
  230. /**
  231. * Remove al handles/color-ítems
  232. */
  233. clear() {
  234. // remove in reverse order
  235. for (let i = this.handles_.length - 1; i >= 0; --i) {
  236. this.removeHandle(this.handles_[i]);
  237. }
  238. this.updateGradient_();
  239. }
  240. /**
  241. * re-render the color gradient using css
  242. *
  243. * @private
  244. */
  245. updateGradient_() {
  246. this.handles_.sort((a, b) => a.value - b.value);
  247. const gradient = (this.handles_.length > 0) ? generateCssGradient(this.handles_, this.min_, this.max_) : "";
  248. this.slider_.style.background = gradient;
  249. }
  250. /**
  251. * Update the positions of the handles based on their values and
  252. * re-render the color gradient
  253. *
  254. * @param {boolean} autoMinMax stretch min/max to value extent
  255. */
  256. update(autoMinMax) {
  257. if (this.handles_.length == 0) return;
  258. let min = this.handles_[0].value;
  259. let max = this.min_;
  260. for (let i = 0; i < this.handles_.length; ++i) {
  261. if (this.handles_[i].value > max) max = this.handles_[i].value;
  262. if (this.handles_[i].value < min) min = this.handles_[i].value;
  263. }
  264. this.min_ = (autoMinMax || (min < this.min_)) ? min : this.min_;
  265. this.max_ = (autoMinMax || (max > this.max_)) ? max : this.max_;
  266. const rangeSize = this.slider_.offsetWidth;
  267. const valueRange = this.max_ - this.min_;
  268. const valueToPosition = (handle) => {
  269. const position = rangeSize * ((handle.value - this.min_) / valueRange);
  270. handle.element.style.left = position + "px";
  271. };
  272. for (let i = 0; i < this.handles_.length; ++i) {
  273. valueToPosition(this.handles_[i]);
  274. this.handles_[i].element.style.background = this.handles_[i].color;
  275. }
  276. this.updateGradient_();
  277. this.fire("update", this);
  278. }
  279. }