VTimePickerClock.mjs 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. import { createVNode as _createVNode } from "vue";
  2. // Styles
  3. import "./VTimePickerClock.css";
  4. // Composables
  5. import { useBackgroundColor, useTextColor } from "../../composables/color.mjs"; // Utilities
  6. import { computed, ref, toRef, watch } from 'vue';
  7. import { genericComponent, propsFactory, useRender } from "../../util/index.mjs"; // Types
  8. export const makeVTimePickerClockProps = propsFactory({
  9. allowedValues: Function,
  10. ampm: Boolean,
  11. color: String,
  12. disabled: Boolean,
  13. displayedValue: null,
  14. double: Boolean,
  15. format: {
  16. type: Function,
  17. default: val => val
  18. },
  19. max: {
  20. type: Number,
  21. required: true
  22. },
  23. min: {
  24. type: Number,
  25. required: true
  26. },
  27. scrollable: Boolean,
  28. readonly: Boolean,
  29. rotate: {
  30. type: Number,
  31. default: 0
  32. },
  33. step: {
  34. type: Number,
  35. default: 1
  36. },
  37. modelValue: {
  38. type: Number
  39. }
  40. }, 'VTimePickerClock');
  41. export const VTimePickerClock = genericComponent()({
  42. name: 'VTimePickerClock',
  43. props: makeVTimePickerClockProps(),
  44. emits: {
  45. change: val => true,
  46. input: val => true
  47. },
  48. setup(props, _ref) {
  49. let {
  50. emit
  51. } = _ref;
  52. const clockRef = ref(null);
  53. const innerClockRef = ref(null);
  54. const inputValue = ref(undefined);
  55. const isDragging = ref(false);
  56. const valueOnMouseDown = ref(null);
  57. const valueOnMouseUp = ref(null);
  58. const {
  59. textColorClasses,
  60. textColorStyles
  61. } = useTextColor(toRef(props, 'color'));
  62. const {
  63. backgroundColorClasses,
  64. backgroundColorStyles
  65. } = useBackgroundColor(toRef(props, 'color'));
  66. const count = computed(() => props.max - props.min + 1);
  67. const roundCount = computed(() => props.double ? count.value / 2 : count.value);
  68. const degreesPerUnit = computed(() => 360 / roundCount.value);
  69. const degrees = computed(() => degreesPerUnit.value * Math.PI / 180);
  70. const displayedValue = computed(() => props.modelValue == null ? props.min : props.modelValue);
  71. const innerRadiusScale = computed(() => 0.62);
  72. const genChildren = computed(() => {
  73. const children = [];
  74. for (let value = props.min; value <= props.max; value = value + props.step) {
  75. children.push(value);
  76. }
  77. return children;
  78. });
  79. watch(() => props.modelValue, val => {
  80. inputValue.value = val;
  81. });
  82. function update(value) {
  83. if (inputValue.value !== value) {
  84. inputValue.value = value;
  85. }
  86. emit('input', value);
  87. }
  88. function isAllowed(value) {
  89. return !props.allowedValues || props.allowedValues(value);
  90. }
  91. function wheel(e) {
  92. if (!props.scrollable || props.disabled) return;
  93. e.preventDefault();
  94. const delta = Math.sign(-e.deltaY || 1);
  95. let value = displayedValue.value;
  96. do {
  97. value = value + delta;
  98. value = (value - props.min + count.value) % count.value + props.min;
  99. } while (!isAllowed(value) && value !== displayedValue.value);
  100. if (value !== props.displayedValue) {
  101. update(value);
  102. }
  103. }
  104. function isInner(value) {
  105. return props.double && value - props.min >= roundCount.value;
  106. }
  107. function handScale(value) {
  108. return isInner(value) ? innerRadiusScale.value : 1;
  109. }
  110. function getPosition(value) {
  111. const rotateRadians = props.rotate * Math.PI / 180;
  112. return {
  113. x: Math.sin((value - props.min) * degrees.value + rotateRadians) * handScale(value),
  114. y: -Math.cos((value - props.min) * degrees.value + rotateRadians) * handScale(value)
  115. };
  116. }
  117. function angleToValue(angle, insideClick) {
  118. const value = (Math.round(angle / degreesPerUnit.value) + (insideClick ? roundCount.value : 0)) % count.value + props.min;
  119. // Necessary to fix edge case when selecting left part of the value(s) at 12 o'clock
  120. if (angle < 360 - degreesPerUnit.value / 2) return value;
  121. return insideClick ? props.max - roundCount.value + 1 : props.min;
  122. }
  123. function getTransform(i) {
  124. const {
  125. x,
  126. y
  127. } = getPosition(i);
  128. return {
  129. left: `${50 + x * 50}%`,
  130. top: `${50 + y * 50}%`
  131. };
  132. }
  133. function euclidean(p0, p1) {
  134. const dx = p1.x - p0.x;
  135. const dy = p1.y - p0.y;
  136. return Math.sqrt(dx * dx + dy * dy);
  137. }
  138. function angle(center, p1) {
  139. const value = 2 * Math.atan2(p1.y - center.y - euclidean(center, p1), p1.x - center.x);
  140. return Math.abs(value * 180 / Math.PI);
  141. }
  142. function setMouseDownValue(value) {
  143. if (valueOnMouseDown.value === null) {
  144. valueOnMouseDown.value = value;
  145. }
  146. valueOnMouseUp.value = value;
  147. update(value);
  148. }
  149. function onDragMove(e) {
  150. e.preventDefault();
  151. if (!isDragging.value && e.type !== 'click' || !clockRef.value) return;
  152. const {
  153. width,
  154. top,
  155. left
  156. } = clockRef.value?.getBoundingClientRect();
  157. const {
  158. width: innerWidth
  159. } = innerClockRef.value?.getBoundingClientRect() ?? {
  160. width: 0
  161. };
  162. const {
  163. clientX,
  164. clientY
  165. } = 'touches' in e ? e.touches[0] : e;
  166. const center = {
  167. x: width / 2,
  168. y: -width / 2
  169. };
  170. const coords = {
  171. x: clientX - left,
  172. y: top - clientY
  173. };
  174. const handAngle = Math.round(angle(center, coords) - props.rotate + 360) % 360;
  175. const insideClick = props.double && euclidean(center, coords) < (innerWidth + innerWidth * innerRadiusScale.value) / 4;
  176. const checksCount = Math.ceil(15 / degreesPerUnit.value);
  177. let value;
  178. for (let i = 0; i < checksCount; i++) {
  179. value = angleToValue(handAngle + i * degreesPerUnit.value, insideClick);
  180. if (isAllowed(value)) return setMouseDownValue(value);
  181. value = angleToValue(handAngle - i * degreesPerUnit.value, insideClick);
  182. if (isAllowed(value)) return setMouseDownValue(value);
  183. }
  184. }
  185. function onMouseDown(e) {
  186. if (props.disabled) return;
  187. e.preventDefault();
  188. window.addEventListener('mousemove', onDragMove);
  189. window.addEventListener('touchmove', onDragMove);
  190. window.addEventListener('mouseup', onMouseUp);
  191. window.addEventListener('touchend', onMouseUp);
  192. valueOnMouseDown.value = null;
  193. valueOnMouseUp.value = null;
  194. isDragging.value = true;
  195. onDragMove(e);
  196. }
  197. function onMouseUp(e) {
  198. e.stopPropagation();
  199. window.removeEventListener('mousemove', onDragMove);
  200. window.removeEventListener('touchmove', onDragMove);
  201. window.removeEventListener('mouseup', onMouseUp);
  202. window.removeEventListener('touchend', onMouseUp);
  203. isDragging.value = false;
  204. if (valueOnMouseUp.value !== null && isAllowed(valueOnMouseUp.value)) {
  205. emit('change', valueOnMouseUp.value);
  206. }
  207. }
  208. useRender(() => {
  209. return _createVNode("div", {
  210. "class": [{
  211. 'v-time-picker-clock': true,
  212. 'v-time-picker-clock--indeterminate': props.modelValue == null,
  213. 'v-time-picker-clock--readonly': props.readonly
  214. }],
  215. "onMousedown": onMouseDown,
  216. "onTouchstart": onMouseDown,
  217. "onWheel": wheel,
  218. "ref": clockRef
  219. }, [_createVNode("div", {
  220. "class": "v-time-picker-clock__inner",
  221. "ref": innerClockRef
  222. }, [_createVNode("div", {
  223. "class": [{
  224. 'v-time-picker-clock__hand': true,
  225. 'v-time-picker-clock__hand--inner': isInner(props.modelValue)
  226. }, textColorClasses.value],
  227. "style": [{
  228. transform: `rotate(${props.rotate + degreesPerUnit.value * (displayedValue.value - props.min)}deg) scaleY(${handScale(displayedValue.value)})`
  229. }, textColorStyles.value]
  230. }, null), genChildren.value.map(value => {
  231. const isActive = value === displayedValue.value;
  232. return _createVNode("div", {
  233. "class": [{
  234. 'v-time-picker-clock__item': true,
  235. 'v-time-picker-clock__item--active': isActive,
  236. 'v-time-picker-clock__item--disabled': props.disabled || !isAllowed(value)
  237. }, isActive && backgroundColorClasses.value],
  238. "style": [getTransform(value), isActive && backgroundColorStyles.value]
  239. }, [_createVNode("span", null, [props.format(value)])]);
  240. })])]);
  241. });
  242. }
  243. });
  244. //# sourceMappingURL=VTimePickerClock.mjs.map