You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

580 lines
15 KiB

  1. <template>
  2. <div
  3. :class="[baseClassName, rangeMarginValue === 0 ? 'zero-ranage-margin' : '']"
  4. @mousewheel.prevent.stop="onMouseWheel"
  5. >
  6. <div class="bar">
  7. <div
  8. class="bar-left"
  9. :style="{ width: barMin + '%' }"
  10. @click="onBarLeftClick"
  11. ></div>
  12. <input
  13. class="input-type-range input-type-range-min"
  14. type="range"
  15. :min="minimum"
  16. :max="maximum"
  17. :step="step"
  18. :value="valueMin"
  19. @input.stop.prevent="onInputMinChange"
  20. />
  21. <div
  22. class="thumb thumb-left"
  23. @mousedown="onLeftThumbMousedown"
  24. @touchstart="onLeftThumbMousedown"
  25. >
  26. <div class="caption">
  27. <span class="min-caption">{{ minCaption || barMinVal }}</span>
  28. </div>
  29. </div>
  30. <div class="bar-inner">
  31. <div class="bar-inner-left" @click="onInnerBarLeftClick"></div>
  32. <div class="bar-inner-right" @click="onInnerBarRightClick"></div>
  33. </div>
  34. <input
  35. class="input-type-range input-type-range-max"
  36. type="range"
  37. :min="minimum"
  38. :max="maximum"
  39. :step="step"
  40. :value="valueMax"
  41. @input.stop.prevent="onInputMaxChange"
  42. />
  43. <div
  44. class="thumb thumb-right"
  45. @mousedown="onRightThumbMousedown"
  46. @touchstart="onRightThumbMousedown"
  47. >
  48. <div class="caption">
  49. <span class="max-caption">{{ maxCaption || barMaxVal }}</span>
  50. </div>
  51. </div>
  52. <div
  53. class="bar-right"
  54. :style="{ width: barMax + '%' }"
  55. @click="onBarRightClick"
  56. ></div>
  57. </div>
  58. <div class="ruler" v-if="ruler">
  59. <div v-for="n in stepCount" :key="n" class="ruler-rule"></div>
  60. </div>
  61. <div class="sub-ruler" v-if="subStepCount">
  62. <div v-for="n in subStepCount" :key="n" class="ruler-sub-rule"></div>
  63. </div>
  64. <div class="labels" v-if="label">
  65. <div class="label" v-for="label in scaleLabels" :key="label">
  66. {{ label }}
  67. </div>
  68. </div>
  69. </div>
  70. </template>
  71. <script>
  72. export default {
  73. name: "MultiRangeSlider",
  74. props: {
  75. baseClassName: {
  76. type: String,
  77. default: "multi-range-slider"
  78. },
  79. min: { type: Number },
  80. max: { type: Number },
  81. minValue: { type: Number },
  82. maxValue: { type: Number },
  83. step: { type: Number, default: 1 },
  84. preventWheel: { type: Boolean, default: false },
  85. ruler: { type: Boolean, default: true },
  86. label: { type: Boolean, default: true },
  87. labels: { type: Array },
  88. minCaption: { type: String },
  89. maxCaption: { type: String },
  90. rangeMargin: { type: Number }
  91. },
  92. data() {
  93. let _labels = this.labels || [];
  94. let _minimum = this.min === undefined ? 0 : this.min;
  95. let max = _labels.length ? _labels.length - 1 : 100;
  96. let _maximum = this.max === undefined ? max : this.max;
  97. let _minValue = this.minValue === undefined ? 25 : this.minValue;
  98. if (_labels.length && this.minValue === undefined) {
  99. _minValue = 1;
  100. }
  101. let _maxValue = this.maxValue || 75;
  102. if (_labels.length && this.maxValue === undefined) {
  103. _maxValue = _labels.length - 2;
  104. }
  105. if (_maximum <= _minimum) {
  106. throw new Error("Invalid props min or max");
  107. }
  108. if (_minValue > _maxValue) {
  109. throw new Error("Invalid props minValue or maxValue");
  110. }
  111. let _rangeMargin =
  112. this.rangeMargin === undefined ? this.step : this.rangeMargin;
  113. let m = _rangeMargin % this.step;
  114. m && (_rangeMargin = _rangeMargin + this.step - m);
  115. return {
  116. valueMin: _minValue < _minimum ? _minimum : _minValue,
  117. valueMax: _maxValue > _maximum ? _maximum : _maxValue,
  118. interVal: null,
  119. startX: null,
  120. mouseMoveCounter: null,
  121. barBox: null,
  122. barValue: 0,
  123. rangeMarginValue: _rangeMargin
  124. };
  125. },
  126. methods: {
  127. onBarLeftClick() {
  128. if (this.valueMin - this.step >= this.minimum) {
  129. this.valueMin -= this.step;
  130. } else {
  131. this.valueMin = this.minimum;
  132. }
  133. },
  134. onInnerBarLeftClick() {
  135. if (this.valueMin + this.rangeMarginValue < this.valueMax) {
  136. this.valueMin += this.step;
  137. }
  138. },
  139. onBarRightClick() {
  140. if (this.valueMax + this.step <= this.maximum) {
  141. this.valueMax += this.step;
  142. } else {
  143. this.valueMax = this.maximum;
  144. }
  145. },
  146. onInnerBarRightClick() {
  147. if (this.valueMax - this.rangeMarginValue > this.valueMin) {
  148. this.valueMax -= this.step;
  149. }
  150. },
  151. onInputMinChange(e) {
  152. let val = parseFloat(e.target.value);
  153. if (val <= this.valueMax - this.rangeMarginValue && val >= this.minimum) {
  154. this.valueMin = val;
  155. } else {
  156. e.target.value = this.valueMin;
  157. }
  158. },
  159. onInputMaxChange(e) {
  160. let val = parseFloat(e.target.value);
  161. if (val >= this.valueMin + this.rangeMarginValue && val <= this.maximum) {
  162. this.valueMax = val;
  163. } else {
  164. e.target.value = this.valueMax;
  165. }
  166. },
  167. onLeftThumbMousedown(e) {
  168. e.preventDefault();
  169. this.startX = e.clientX;
  170. if (e.type === "touchstart") {
  171. if (e.touches.length === 1) {
  172. this.startX = e.touches[0].clientX;
  173. } else {
  174. return;
  175. }
  176. }
  177. this.mouseMoveCounter = 0;
  178. this.barValue = this.valueMin;
  179. this.barBox = e.target.parentNode.getBoundingClientRect();
  180. document.addEventListener("mousemove", this.onLeftThumbMousemove);
  181. document.addEventListener("mouseup", this.onLeftThumbMouseup);
  182. document.addEventListener("touchmove", this.onLeftThumbMousemove);
  183. document.addEventListener("touchend", this.onLeftThumbMouseup);
  184. },
  185. onLeftThumbMousemove(e) {
  186. this.mouseMoveCounter++;
  187. let clientX = e.clientX;
  188. if (e.type === "touchmove") {
  189. clientX = e.touches[0].clientX;
  190. }
  191. let dx = clientX - this.startX;
  192. let per = dx / this.barBox.width;
  193. let val = this.barValue + (this.maximum - this.minimum) * per;
  194. let mod = val % this.step;
  195. val -= mod;
  196. if (val < this.minimum) {
  197. val = this.minimum;
  198. } else if (val > this.valueMax - this.rangeMarginValue) {
  199. val = this.valueMax - this.rangeMarginValue;
  200. }
  201. this.valueMin = val;
  202. },
  203. onLeftThumbMouseup() {
  204. document.removeEventListener("mousemove", this.onLeftThumbMousemove);
  205. document.removeEventListener("mouseup", this.onLeftThumbMouseup);
  206. document.removeEventListener("touchmove", this.onLeftThumbMousemove);
  207. document.removeEventListener("touchend", this.onLeftThumbMouseup);
  208. },
  209. onRightThumbMousedown(e) {
  210. e.preventDefault();
  211. this.startX = e.clientX;
  212. if (e.type === "touchstart") {
  213. if (e.touches.length === 1) {
  214. this.startX = e.touches[0].clientX;
  215. } else {
  216. return;
  217. }
  218. }
  219. this.mouseMoveCounter = 0;
  220. this.barValue = this.valueMax;
  221. this.barBox = e.target.parentNode.getBoundingClientRect();
  222. document.addEventListener("mousemove", this.onRightThumbMousemove);
  223. document.addEventListener("mouseup", this.onRightThumbMouseup);
  224. document.addEventListener("touchmove", this.onRightThumbMousemove);
  225. document.addEventListener("touchend", this.onRightThumbMouseup);
  226. },
  227. onRightThumbMousemove(e) {
  228. this.mouseMoveCounter++;
  229. let clientX = e.clientX;
  230. if (e.type === "touchmove") {
  231. clientX = e.touches[0].clientX;
  232. }
  233. let dx = clientX - this.startX;
  234. let per = dx / this.barBox.width;
  235. let val = this.barValue + (this.maximum - this.minimum) * per;
  236. let mod = val % this.step;
  237. val -= mod;
  238. if (val < this.valueMin + this.rangeMarginValue) {
  239. val = this.valueMin + this.rangeMarginValue;
  240. } else if (val > this.maximum) {
  241. val = this.maximum;
  242. }
  243. this.valueMax = val;
  244. },
  245. onRightThumbMouseup() {
  246. document.removeEventListener("mousemove", this.onRightThumbMousemove);
  247. document.removeEventListener("mouseup", this.onRightThumbMouseup);
  248. document.removeEventListener("touchmove", this.onRightThumbMousemove);
  249. document.removeEventListener("touchend", this.onRightThumbMouseup);
  250. },
  251. onMouseWheel(e) {
  252. if (this.preventWheel === true) {
  253. return;
  254. }
  255. if (!e.shiftKey && !e.ctrlKey) {
  256. return;
  257. }
  258. let val = this.step;
  259. if (e.deltaY < 0) {
  260. val = -val;
  261. }
  262. if (e.shiftKey && e.ctrlKey) {
  263. if (
  264. this.valueMin + val >= this.minimum &&
  265. this.valueMax + val <= this.maximum
  266. ) {
  267. this.valueMin = this.valueMin + val;
  268. this.valueMax = this.valueMax + val;
  269. }
  270. } else if (e.ctrlKey) {
  271. val = this.valueMax + val;
  272. if (val < this.valueMin + this.rangeMarginValue) {
  273. val = this.valueMin + this.rangeMarginValue;
  274. } else if (val > this.maximum) {
  275. val = this.maximum;
  276. }
  277. this.valueMax = val;
  278. } else if (e.shiftKey) {
  279. val = this.valueMin + val;
  280. if (val < this.minimum) {
  281. val = this.minimum;
  282. } else if (val > this.valueMax - this.rangeMarginValue) {
  283. val = this.valueMax - this.rangeMarginValue;
  284. }
  285. this.valueMin = val;
  286. }
  287. },
  288. triggerInput() {
  289. let fixed = 0;
  290. if (this.step.toString().includes(".")) {
  291. fixed = 2;
  292. }
  293. let retObj = {
  294. min: this.minimum,
  295. max: this.maximum,
  296. minValue: parseFloat(this.valueMin.toFixed(fixed)),
  297. maxValue: parseFloat(this.valueMax.toFixed(fixed))
  298. };
  299. this.$emit("input", retObj);
  300. }
  301. },
  302. computed: {
  303. minimum() {
  304. return this.min === undefined ? 0 : this.min;
  305. },
  306. maximum() {
  307. let _labels = this.labels || [];
  308. let max = _labels.length ? _labels.length - 1 : 100;
  309. return this.max === undefined ? max : this.max;
  310. },
  311. stepCount() {
  312. let _labels = this.labels || [];
  313. if (_labels.length) {
  314. return _labels.length - 1;
  315. }
  316. return Math.floor((this.maximum - this.minimum) / this.step);
  317. },
  318. subStepCount() {
  319. let _labels = this.labels || [];
  320. if (_labels.length && this.step > 1) {
  321. return (this.maximum - this.minimum) / this.step;
  322. }
  323. return 0;
  324. },
  325. barMin() {
  326. let per =
  327. ((this.valueMin - this.minimum) / (this.maximum - this.minimum)) * 100;
  328. return per;
  329. },
  330. barMax() {
  331. let per =
  332. 100 -
  333. ((this.valueMax - this.minimum) / (this.maximum - this.minimum)) * 100;
  334. return per;
  335. },
  336. barMinVal() {
  337. let fixed = 0;
  338. if (this.step.toString().includes(".")) {
  339. fixed = 2;
  340. }
  341. return (this.valueMin || 0).toFixed(fixed);
  342. },
  343. barMaxVal() {
  344. let fixed = 0;
  345. if (this.step.toString().includes(".")) {
  346. fixed = 2;
  347. }
  348. return (this.valueMax || 100).toFixed(fixed);
  349. },
  350. scaleLabels() {
  351. let _labels = this.labels || [];
  352. if (_labels.length === 0) {
  353. _labels = [];
  354. _labels.push(this.minimum);
  355. _labels.push(this.maximum);
  356. }
  357. return _labels;
  358. }
  359. },
  360. watch: {
  361. valueMin() {
  362. this.triggerInput();
  363. },
  364. valueMax() {
  365. this.triggerInput();
  366. },
  367. minValue(newValue) {
  368. this.valueMin = newValue < this.minimum ? this.minimum : newValue;
  369. },
  370. maxValue(newValue) {
  371. this.valueMax = newValue > this.maximum ? this.maximum : newValue;
  372. }
  373. },
  374. mounted() {}
  375. };
  376. </script>
  377. <!-- Add "scoped" attribute to limit CSS to this component only -->
  378. <style>
  379. .multi-range-slider * {
  380. box-sizing: border-box;
  381. padding: 0px;
  382. margin: 0px;
  383. }
  384. .multi-range-slider {
  385. display: flex;
  386. position: relative;
  387. border: solid 1px gray;
  388. border-radius: 10px;
  389. padding: 20px 10px;
  390. box-shadow: 1px 1px 4px black;
  391. flex-direction: column;
  392. -webkit-touch-callout: none; /* iOS Safari */
  393. -webkit-user-select: none; /* Safari */
  394. -khtml-user-select: none; /* Konqueror HTML */
  395. -moz-user-select: none; /* Old versions of Firefox */
  396. -ms-user-select: none; /* Internet Explorer/Edge */
  397. user-select: none; /* Non-prefixed version, currently supported by Chrome, Edge,*/
  398. }
  399. .multi-range-slider .bar {
  400. display: flex;
  401. }
  402. .multi-range-slider .bar-left {
  403. width: 25%;
  404. background-color: #f0f0f0;
  405. border-radius: 10px 0px 0px 10px;
  406. box-shadow: inset 0px 0px 5px black;
  407. padding: 4px 0px;
  408. }
  409. .multi-range-slider .bar-right {
  410. width: 25%;
  411. background-color: #f0f0f0;
  412. border-radius: 0px 10px 10px 0px;
  413. box-shadow: inset 0px 0px 5px black;
  414. }
  415. .multi-range-slider .bar-inner {
  416. background-color: lime;
  417. display: flex;
  418. flex-grow: 1;
  419. flex-shrink: 1;
  420. justify-content: space-between;
  421. position: relative;
  422. border: solid 1px black;
  423. justify-content: space-between;
  424. box-shadow: inset 0px 0px 5px black;
  425. }
  426. .multi-range-slider .bar-inner-left {
  427. width: 50%;
  428. }
  429. .multi-range-slider .bar-inner-right {
  430. width: 50%;
  431. }
  432. .multi-range-slider .thumb {
  433. background-color: red;
  434. position: relative;
  435. z-index: 1;
  436. cursor: pointer;
  437. }
  438. .multi-range-slider .thumb::before {
  439. content: "";
  440. background-color: white;
  441. position: absolute;
  442. width: 20px;
  443. height: 20px;
  444. border: solid 1px black;
  445. box-shadow: 0px 0px 3px black, inset 0px 0px 5px gray;
  446. border-radius: 50%;
  447. z-index: 1;
  448. margin: -8px;
  449. cursor: pointer;
  450. }
  451. .multi-range-slider .input-type-range:focus + .thumb::after {
  452. content: "";
  453. position: absolute;
  454. top: -4px;
  455. left: -4px;
  456. width: 11px;
  457. height: 11px;
  458. z-index: 2;
  459. border-radius: 50%;
  460. border: dotted 1px black;
  461. box-shadow: 0px 0px 5px white, inset 0px 0px 10px black;
  462. }
  463. .multi-range-slider .caption {
  464. position: absolute;
  465. bottom: 35px;
  466. width: 2px;
  467. height: 2px;
  468. left: 1px;
  469. display: flex;
  470. justify-content: center;
  471. align-items: center;
  472. overflow: visible;
  473. display: none;
  474. }
  475. .multi-range-slider .thumb .caption * {
  476. position: absolute;
  477. min-width: 30px;
  478. height: 30px;
  479. font-size: 75%;
  480. text-align: center;
  481. line-height: 30px;
  482. background-color: blue;
  483. border-radius: 15px;
  484. color: white;
  485. box-shadow: 0px 0px 5px black;
  486. padding: 0px 5px;
  487. white-space: nowrap;
  488. }
  489. .multi-range-slider .thumb:active .caption {
  490. display: flex;
  491. }
  492. .multi-range-slider .input-type-range:focus + .thumb .caption {
  493. display: flex;
  494. }
  495. .multi-range-slider .input-type-range {
  496. position: absolute;
  497. top: 0px;
  498. left: 0px;
  499. width: 100%;
  500. opacity: 0;
  501. pointer-events: none;
  502. }
  503. .multi-range-slider .ruler {
  504. margin: 10px 0px -5px 0px;
  505. display: flex;
  506. /* display: none; */
  507. overflow: hidden;
  508. }
  509. .multi-range-slider .ruler .ruler-rule {
  510. border-left: solid 1px;
  511. border-bottom: solid 1px;
  512. display: flex;
  513. flex-grow: 1;
  514. flex-shrink: 1;
  515. padding: 5px 0px;
  516. }
  517. .multi-range-slider .ruler .ruler-rule:last-child {
  518. border-right: solid 1px;
  519. }
  520. .multi-range-slider .sub-ruler {
  521. margin: -2px 0px -5px 0px;
  522. display: flex; /*
  523. display: none; */
  524. }
  525. .multi-range-slider .sub-ruler .ruler-sub-rule {
  526. border-left: solid 1px;
  527. border-bottom: solid 1px;
  528. display: flex;
  529. flex-grow: 1;
  530. flex-shrink: 1;
  531. padding: 3px 0px;
  532. }
  533. .multi-range-slider .sub-ruler .ruler-sub-rule:last-child {
  534. border-right: solid 1px;
  535. }
  536. .multi-range-slider .labels {
  537. display: flex;
  538. justify-content: space-between;
  539. padding: 0px;
  540. margin-top: 10px;
  541. margin-bottom: -20px;
  542. /* display: none; */
  543. }
  544. .multi-range-slider .label {
  545. font-size: 80%;
  546. display: flex;
  547. width: 1px;
  548. justify-content: center;
  549. }
  550. .multi-range-slider .label:first-child {
  551. justify-content: start;
  552. }
  553. .multi-range-slider .label:last-child {
  554. justify-content: end;
  555. }
  556. .multi-range-slider.zero-ranage-margin .thumb-left {
  557. right: 12px;
  558. }
  559. .multi-range-slider.zero-ranage-margin .thumb-right {
  560. left: 8px;
  561. }
  562. </style>