ui.slider.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. /**
  2. * DevExtreme (ui/slider/ui.slider.js)
  3. * Version: 19.1.16
  4. * Build date: Tue Oct 18 2022
  5. *
  6. * Copyright (c) 2012 - 2022 Developer Express Inc. ALL RIGHTS RESERVED
  7. * Read about DevExtreme licensing here: https://js.devexpress.com/Licensing/
  8. */
  9. "use strict";
  10. var $ = require("../../core/renderer");
  11. var eventsEngine = require("../../events/core/events_engine");
  12. var domUtils = require("../../core/utils/dom");
  13. var numberLocalization = require("../../localization/number");
  14. var devices = require("../../core/devices");
  15. var extend = require("../../core/utils/extend").extend;
  16. var applyServerDecimalSeparator = require("../../core/utils/common").applyServerDecimalSeparator;
  17. var registerComponent = require("../../core/component_registrator");
  18. var TrackBar = require("../track_bar");
  19. var eventUtils = require("../../events/utils");
  20. var pointerEvents = require("../../events/pointer");
  21. var feedbackEvents = require("../../events/core/emitter.feedback");
  22. var SliderHandle = require("./ui.slider_handle");
  23. var inkRipple = require("../widget/utils.ink_ripple");
  24. var clickEvent = require("../../events/click");
  25. var Swipeable = require("../../events/gesture/swipeable");
  26. var themes = require("../themes");
  27. var Deferred = require("../../core/utils/deferred").Deferred;
  28. var SLIDER_CLASS = "dx-slider";
  29. var SLIDER_WRAPPER_CLASS = "dx-slider-wrapper";
  30. var SLIDER_HANDLE_SELECTOR = ".dx-slider-handle";
  31. var SLIDER_BAR_CLASS = "dx-slider-bar";
  32. var SLIDER_RANGE_CLASS = "dx-slider-range";
  33. var SLIDER_RANGE_VISIBLE_CLASS = "dx-slider-range-visible";
  34. var SLIDER_LABEL_CLASS = "dx-slider-label";
  35. var SLIDER_LABEL_POSITION_CLASS_PREFIX = "dx-slider-label-position-";
  36. var SLIDER_TOOLTIP_POSITION_CLASS_PREFIX = "dx-slider-tooltip-position-";
  37. var INVALID_MESSAGE_VISIBLE_CLASS = "dx-invalid-message-visible";
  38. var SLIDER_VALIDATION_NAMESPACE = "Validation";
  39. var Slider = TrackBar.inherit({
  40. _activeStateUnit: SLIDER_HANDLE_SELECTOR,
  41. _supportedKeys: function() {
  42. var isRTL = this.option("rtlEnabled");
  43. var that = this;
  44. var roundedValue = function(offset, isLeftDirection) {
  45. offset = that._valueStep(offset);
  46. var step = that.option("step");
  47. var value = that.option("value");
  48. var division = (value - that.option("min")) % step;
  49. var result = isLeftDirection ? value - offset + (division ? step - division : 0) : value + offset - division;
  50. var min = that.option("min");
  51. var max = that.option("max");
  52. if (result < min) {
  53. result = min
  54. } else {
  55. if (result > max) {
  56. result = max
  57. }
  58. }
  59. return result
  60. };
  61. var moveHandleRight = function(offset) {
  62. that.option("value", roundedValue(offset, isRTL))
  63. };
  64. var moveHandleLeft = function(offset) {
  65. that.option("value", roundedValue(offset, !isRTL))
  66. };
  67. return extend(this.callBase(), {
  68. leftArrow: function(e) {
  69. e.preventDefault();
  70. e.stopPropagation();
  71. moveHandleLeft(this.option("step"))
  72. },
  73. rightArrow: function(e) {
  74. e.preventDefault();
  75. e.stopPropagation();
  76. moveHandleRight(this.option("step"))
  77. },
  78. pageUp: function(e) {
  79. e.preventDefault();
  80. e.stopPropagation();
  81. moveHandleRight(this.option("step") * this.option("keyStep"))
  82. },
  83. pageDown: function(e) {
  84. e.preventDefault();
  85. e.stopPropagation();
  86. moveHandleLeft(this.option("step") * this.option("keyStep"))
  87. },
  88. home: function(e) {
  89. e.preventDefault();
  90. e.stopPropagation();
  91. var min = this.option("min");
  92. this.option("value", min)
  93. },
  94. end: function(e) {
  95. e.preventDefault();
  96. e.stopPropagation();
  97. var max = this.option("max");
  98. this.option("value", max)
  99. }
  100. })
  101. },
  102. _getDefaultOptions: function() {
  103. return extend(this.callBase(), {
  104. value: 50,
  105. hoverStateEnabled: true,
  106. activeStateEnabled: true,
  107. step: 1,
  108. showRange: true,
  109. tooltip: {
  110. enabled: false,
  111. format: function(value) {
  112. return value
  113. },
  114. position: "top",
  115. showMode: "onHover"
  116. },
  117. label: {
  118. visible: false,
  119. position: "bottom",
  120. format: function(value) {
  121. return value
  122. }
  123. },
  124. keyStep: 1,
  125. useInkRipple: false,
  126. validationMessageOffset: themes.isMaterial() ? {
  127. h: 18,
  128. v: 0
  129. } : {
  130. h: 7,
  131. v: 4
  132. },
  133. focusStateEnabled: true
  134. })
  135. },
  136. _toggleValidationMessage: function(visible) {
  137. if (!this.option("isValid")) {
  138. this.$element().toggleClass(INVALID_MESSAGE_VISIBLE_CLASS, visible)
  139. }
  140. },
  141. _defaultOptionsRules: function() {
  142. return this.callBase().concat([{
  143. device: function() {
  144. return "desktop" === devices.real().deviceType && !devices.isSimulator()
  145. },
  146. options: {
  147. focusStateEnabled: true
  148. }
  149. }, {
  150. device: function() {
  151. var themeName = themes.current();
  152. return themes.isMaterial(themeName)
  153. },
  154. options: {
  155. useInkRipple: true
  156. }
  157. }])
  158. },
  159. _initMarkup: function() {
  160. this.$element().addClass(SLIDER_CLASS);
  161. this._renderSubmitElement();
  162. this.option("useInkRipple") && this._renderInkRipple();
  163. this.callBase();
  164. this._renderLabels();
  165. this._renderStartHandler();
  166. this._renderAriaMinAndMax()
  167. },
  168. _attachFocusEvents: function() {
  169. this.callBase();
  170. var namespace = this.NAME + SLIDER_VALIDATION_NAMESPACE;
  171. var focusInEvent = eventUtils.addNamespace("focusin", namespace);
  172. var focusOutEvent = eventUtils.addNamespace("focusout", namespace);
  173. var $focusTarget = this._focusTarget();
  174. eventsEngine.on($focusTarget, focusInEvent, this._toggleValidationMessage.bind(this, true));
  175. eventsEngine.on($focusTarget, focusOutEvent, this._toggleValidationMessage.bind(this, false))
  176. },
  177. _detachFocusEvents: function() {
  178. this.callBase();
  179. var $focusTarget = this._focusTarget();
  180. this._toggleValidationMessage(false);
  181. eventsEngine.off($focusTarget, this.NAME + SLIDER_VALIDATION_NAMESPACE)
  182. },
  183. _render: function() {
  184. this.callBase();
  185. this._repaintHandle()
  186. },
  187. _renderSubmitElement: function() {
  188. this._$submitElement = $("<input>").attr("type", "hidden").appendTo(this.$element())
  189. },
  190. _getSubmitElement: function() {
  191. return this._$submitElement
  192. },
  193. _renderInkRipple: function() {
  194. this._inkRipple = inkRipple.render({
  195. waveSizeCoefficient: .7,
  196. isCentered: true,
  197. wavesNumber: 2,
  198. useHoldAnimation: false
  199. })
  200. },
  201. _renderInkWave: function(element, dxEvent, doRender, waveIndex) {
  202. if (!this._inkRipple) {
  203. return
  204. }
  205. var config = {
  206. element: element,
  207. event: dxEvent,
  208. wave: waveIndex
  209. };
  210. if (doRender) {
  211. this._inkRipple.showWave(config)
  212. } else {
  213. this._inkRipple.hideWave(config)
  214. }
  215. },
  216. _visibilityChanged: function() {
  217. this.repaint()
  218. },
  219. _renderWrapper: function() {
  220. this.callBase();
  221. this._$wrapper.addClass(SLIDER_WRAPPER_CLASS);
  222. this._createComponent(this._$wrapper, Swipeable, {
  223. elastic: false,
  224. immediate: true,
  225. onStart: this._swipeStartHandler.bind(this),
  226. onUpdated: this._swipeUpdateHandler.bind(this),
  227. onEnd: this._swipeEndHandler.bind(this),
  228. itemSizeFunc: this._itemWidthFunc.bind(this)
  229. })
  230. },
  231. _renderContainer: function() {
  232. this.callBase();
  233. this._$bar.addClass(SLIDER_BAR_CLASS)
  234. },
  235. _renderRange: function() {
  236. this.callBase();
  237. this._$range.addClass(SLIDER_RANGE_CLASS);
  238. this._renderHandle();
  239. this._renderRangeVisibility()
  240. },
  241. _renderRangeVisibility: function() {
  242. this._$range.toggleClass(SLIDER_RANGE_VISIBLE_CLASS, Boolean(this.option("showRange")))
  243. },
  244. _renderHandle: function() {
  245. this._$handle = this._renderHandleImpl(this.option("value"), this._$handle)
  246. },
  247. _renderHandleImpl: function(value, $element) {
  248. var $handle = $element || $("<div>").appendTo(this._$range);
  249. var format = this.option("tooltip.format");
  250. var tooltipEnabled = this.option("tooltip.enabled");
  251. var tooltipPosition = this.option("tooltip.position");
  252. this.$element().toggleClass(SLIDER_TOOLTIP_POSITION_CLASS_PREFIX + "bottom", tooltipEnabled && "bottom" === tooltipPosition).toggleClass(SLIDER_TOOLTIP_POSITION_CLASS_PREFIX + "top", tooltipEnabled && "top" === tooltipPosition);
  253. this._createComponent($handle, SliderHandle, {
  254. value: value,
  255. tooltipEnabled: tooltipEnabled,
  256. tooltipPosition: tooltipPosition,
  257. tooltipFormat: format,
  258. tooltipShowMode: this.option("tooltip.showMode"),
  259. tooltipFitIn: this.$element()
  260. });
  261. return $handle
  262. },
  263. _renderAriaMinAndMax: function() {
  264. this.setAria({
  265. valuemin: this.option("min"),
  266. valuemax: this.option("max")
  267. }, this._$handle)
  268. },
  269. _hoverStartHandler: function(e) {
  270. SliderHandle.getInstance($(e.currentTarget)).updateTooltip()
  271. },
  272. _toggleActiveState: function($element, value) {
  273. this.callBase($element, value);
  274. if (value) {
  275. SliderHandle.getInstance($element).updateTooltip()
  276. }
  277. this._renderInkWave($element, null, !!value, 1)
  278. },
  279. _toggleFocusClass: function(isFocused, $element) {
  280. this.callBase(isFocused, $element);
  281. if (this._disposed) {
  282. return
  283. }
  284. var $focusTarget = $($element || this._focusTarget());
  285. this._renderInkWave($focusTarget, null, isFocused, 0)
  286. },
  287. _renderLabels: function() {
  288. this.$element().removeClass(SLIDER_LABEL_POSITION_CLASS_PREFIX + "bottom").removeClass(SLIDER_LABEL_POSITION_CLASS_PREFIX + "top");
  289. if (this.option("label.visible")) {
  290. var min = this.option("min");
  291. var max = this.option("max");
  292. var position = this.option("label.position");
  293. var labelFormat = this.option("label.format");
  294. if (!this._$minLabel) {
  295. this._$minLabel = $("<div>").addClass(SLIDER_LABEL_CLASS).appendTo(this._$wrapper)
  296. }
  297. this._$minLabel.html(numberLocalization.format(min, labelFormat));
  298. if (!this._$maxLabel) {
  299. this._$maxLabel = $("<div>").addClass(SLIDER_LABEL_CLASS).appendTo(this._$wrapper)
  300. }
  301. this._$maxLabel.html(numberLocalization.format(max, labelFormat));
  302. this.$element().addClass(SLIDER_LABEL_POSITION_CLASS_PREFIX + position)
  303. } else {
  304. if (this._$minLabel) {
  305. this._$minLabel.remove();
  306. delete this._$minLabel
  307. }
  308. if (this._$maxLabel) {
  309. this._$maxLabel.remove();
  310. delete this._$maxLabel
  311. }
  312. }
  313. },
  314. _renderStartHandler: function() {
  315. var pointerDownEventName = eventUtils.addNamespace(pointerEvents.down, this.NAME);
  316. var clickEventName = eventUtils.addNamespace(clickEvent.name, this.NAME);
  317. var startAction = this._createAction(this._startHandler.bind(this));
  318. var $element = this.$element();
  319. eventsEngine.off($element, pointerDownEventName);
  320. eventsEngine.on($element, pointerDownEventName, function(e) {
  321. if (eventUtils.isMouseEvent(e)) {
  322. startAction({
  323. event: e
  324. })
  325. }
  326. });
  327. eventsEngine.off($element, clickEventName);
  328. eventsEngine.on($element, clickEventName, function(e) {
  329. var $handle = this._activeHandle();
  330. if ($handle) {
  331. eventsEngine.trigger($handle, "focusin");
  332. eventsEngine.trigger($handle, "focus")
  333. }
  334. startAction({
  335. event: e
  336. })
  337. }.bind(this))
  338. },
  339. _itemWidthFunc: function() {
  340. return this._itemWidthRatio
  341. },
  342. _swipeStartHandler: function(e) {
  343. var rtlEnabled = this.option("rtlEnabled");
  344. if (eventUtils.isTouchEvent(e.event)) {
  345. this._createAction(this._startHandler.bind(this))({
  346. event: e.event
  347. })
  348. }
  349. this._feedbackDeferred = new Deferred;
  350. feedbackEvents.lock(this._feedbackDeferred);
  351. this._toggleActiveState(this._activeHandle(), this.option("activeStateEnabled"));
  352. this._startOffset = this._currentRatio;
  353. var startOffset = this._startOffset * this._swipePixelRatio();
  354. var endOffset = (1 - this._startOffset) * this._swipePixelRatio();
  355. e.event.maxLeftOffset = rtlEnabled ? endOffset : startOffset;
  356. e.event.maxRightOffset = rtlEnabled ? startOffset : endOffset;
  357. this._itemWidthRatio = this.$element().width() / this._swipePixelRatio();
  358. this._needPreventAnimation = true
  359. },
  360. _swipeEndHandler: function(e) {
  361. this._feedbackDeferred.resolve();
  362. this._toggleActiveState(this._activeHandle(), false);
  363. var offsetDirection = this.option("rtlEnabled") ? -1 : 1;
  364. delete this._needPreventAnimation;
  365. this._changeValueOnSwipe(this._startOffset + offsetDirection * e.event.targetOffset / this._swipePixelRatio());
  366. delete this._startOffset;
  367. this._renderValue()
  368. },
  369. _activeHandle: function() {
  370. return this._$handle
  371. },
  372. _swipeUpdateHandler: function(e) {
  373. this._saveValueChangeEvent(e);
  374. this._updateHandlePosition(e)
  375. },
  376. _updateHandlePosition: function(e) {
  377. var offsetDirection = this.option("rtlEnabled") ? -1 : 1;
  378. var newRatio = Math.min(this._startOffset + offsetDirection * e.event.offset / this._swipePixelRatio(), 1);
  379. this._$range.width(100 * newRatio + "%");
  380. SliderHandle.getInstance(this._activeHandle()).fitTooltipPosition;
  381. this._changeValueOnSwipe(newRatio)
  382. },
  383. _swipePixelRatio: function() {
  384. var min = this.option("min");
  385. var max = this.option("max");
  386. var step = this._valueStep(this.option("step"));
  387. return (max - min) / step
  388. },
  389. _valueStep: function(step) {
  390. if (!step || isNaN(step)) {
  391. step = 1
  392. }
  393. step = parseFloat(step.toFixed(5));
  394. if (0 === step) {
  395. step = 1e-5
  396. }
  397. return step
  398. },
  399. _changeValueOnSwipe: function(ratio) {
  400. var min = this.option("min");
  401. var max = this.option("max");
  402. var step = this._valueStep(this.option("step"));
  403. var newChange = ratio * (max - min);
  404. var newValue = min + newChange;
  405. if (step < 0) {
  406. return
  407. }
  408. if (newValue === max || newValue === min) {
  409. this._setValueOnSwipe(newValue)
  410. } else {
  411. var stepExponent = (step + "").split(".")[1];
  412. var minExponent = (min + "").split(".")[1];
  413. var exponentLength = Math.max(stepExponent && stepExponent.length || 0, minExponent && minExponent.length || 0);
  414. var stepCount = Math.round((newValue - min) / step);
  415. newValue = Number((stepCount * step + min).toFixed(exponentLength));
  416. this._setValueOnSwipe(Math.max(Math.min(newValue, max), min))
  417. }
  418. },
  419. _setValueOnSwipe: function(value) {
  420. this.option("value", value)
  421. },
  422. _startHandler: function(args) {
  423. var e = args.event;
  424. this._currentRatio = (eventUtils.eventData(e).x - this._$bar.offset().left) / this._$bar.width();
  425. if (this.option("rtlEnabled")) {
  426. this._currentRatio = 1 - this._currentRatio
  427. }
  428. this._saveValueChangeEvent(e);
  429. this._changeValueOnSwipe(this._currentRatio)
  430. },
  431. _renderValue: function() {
  432. this.callBase();
  433. var value = this.option("value");
  434. this._getSubmitElement().val(applyServerDecimalSeparator(value));
  435. SliderHandle.getInstance(this._activeHandle()).option("value", value)
  436. },
  437. _setRangeStyles: function(options) {
  438. options && this._$range.css(options)
  439. },
  440. _callHandlerMethod: function(name, args) {
  441. SliderHandle.getInstance(this._$handle)[name](args)
  442. },
  443. _repaintHandle: function() {
  444. this._callHandlerMethod("repaint")
  445. },
  446. _fitTooltip: function() {
  447. this._callHandlerMethod("fitTooltipPosition")
  448. },
  449. _optionChanged: function(args) {
  450. switch (args.name) {
  451. case "visible":
  452. this.callBase(args);
  453. this._renderHandle();
  454. this._repaintHandle();
  455. domUtils.triggerShownEvent(this.$element());
  456. break;
  457. case "min":
  458. case "max":
  459. this._renderValue();
  460. this.callBase(args);
  461. this._renderLabels();
  462. this._renderAriaMinAndMax();
  463. this._fitTooltip();
  464. break;
  465. case "step":
  466. this._renderValue();
  467. break;
  468. case "keyStep":
  469. break;
  470. case "showRange":
  471. this._renderRangeVisibility();
  472. break;
  473. case "tooltip":
  474. this._renderHandle();
  475. break;
  476. case "label":
  477. this._renderLabels();
  478. break;
  479. case "useInkRipple":
  480. this._invalidate();
  481. break;
  482. default:
  483. this.callBase(args)
  484. }
  485. },
  486. _refresh: function() {
  487. this._toggleRTLDirection(this.option("rtlEnabled"));
  488. this._renderDimensions();
  489. this._renderValue();
  490. this._renderHandle();
  491. this._repaintHandle()
  492. },
  493. _clean: function() {
  494. delete this._inkRipple;
  495. this.callBase()
  496. }
  497. });
  498. registerComponent("dxSlider", Slider);
  499. module.exports = Slider;