foundation.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. /* eslint-disable no-param-reassign */
  2. /* eslint-disable max-len */
  3. /* eslint-disable no-nested-ternary */
  4. import BaseFoundation, { DefaultAdapter } from '../base/foundation';
  5. import touchEventPolyfill from '../utils/touchPolyfill';
  6. export interface Marks{
  7. [key: number]: string;
  8. }
  9. export type tipFormatterBasicType = string | number | boolean | null;
  10. export interface SliderProps{
  11. defaultValue?: number | number[];
  12. disabled?: boolean;
  13. included?: boolean; // Whether to juxtapose. Allow dragging
  14. marks?: Marks; // Scale
  15. max?: number;
  16. min?: number;
  17. range?: boolean; // Whether both sides
  18. step?: number;
  19. tipFormatter?: (value: tipFormatterBasicType | tipFormatterBasicType[]) => any;
  20. value?: number | number[];
  21. vertical?: boolean;
  22. onAfterChange?: (value: SliderProps['value']) => void; // triggered when mouse up and clicked
  23. onChange?: (value: SliderProps['value']) => void;
  24. tooltipVisible?: boolean;
  25. style?: Record<string, any>;
  26. className?: string;
  27. showBoundary?: boolean;
  28. railStyle?: Record<string, any>;
  29. verticalReverse?: boolean;
  30. 'aria-label'?: string;
  31. 'aria-labelledby'?: string;
  32. 'aria-valuetext'?: string;
  33. getAriaValueText?: (value: number) => string;
  34. }
  35. export interface SliderState {
  36. currentValue: number | number[];
  37. min: number;
  38. max: number;
  39. focusPos: 'min' | 'max' | '';
  40. onChange: (value: SliderProps['value']) => void;
  41. disabled: SliderProps['disabled'];
  42. chooseMovePos: 'min' | 'max' | '';
  43. isDrag: boolean;
  44. clickValue: 0;
  45. showBoundary: boolean;
  46. isInRenderTree: boolean;
  47. }
  48. export interface SliderLengths{
  49. sliderX: number;
  50. sliderY: number;
  51. sliderWidth: number;
  52. sliderHeight: number;
  53. }
  54. export interface ScrollParentVal{
  55. scrollTop: number;
  56. scrollLeft: number;
  57. }
  58. export interface OverallVars{
  59. dragging: boolean[];
  60. chooseMovePos: 'min' | 'max';
  61. }
  62. export interface SliderAdapter extends DefaultAdapter<SliderProps, SliderState>{
  63. getSliderLengths: () => SliderLengths;
  64. getParentRect: () => DOMRect | void;
  65. getScrollParentVal: () => ScrollParentVal;
  66. isEventFromHandle: (e: any) => boolean;
  67. getOverallVars: () => OverallVars;
  68. updateDisabled: (disabled: SliderState['disabled']) => void;
  69. transNewPropsToState: <K extends keyof SliderState>(stateObj: Pick<SliderState, K>, callback?: () => void) => void;
  70. notifyChange: (callbackValue: number | number[]) => void;
  71. setDragging: (value: boolean[]) => void;
  72. updateCurrentValue: (value: SliderState['currentValue']) => void;
  73. setOverallVars: (key: string, value: any) => void;
  74. getMinHandleEl: () => { current: HTMLElement };
  75. getMaxHandleEl: () => { current: HTMLElement };
  76. onHandleDown: (e: any) => any;
  77. onHandleMove: (mousePos: number, isMin: boolean, stateChangeCallback?: () => void, clickTrack?: boolean) => boolean | void;
  78. setEventDefault: (e: any) => void;
  79. setStateVal: (state: keyof SliderState, value: any) => void;
  80. onHandleEnter: (position: SliderState['focusPos']) => void;
  81. onHandleLeave: () => void;
  82. onHandleUpBefore: (e: any) => void;
  83. onHandleUpAfter: () => void;
  84. unSubscribeEventListener: () => void;
  85. checkAndUpdateIsInRenderTreeState: () => boolean;
  86. }
  87. export default class SliderFoundation extends BaseFoundation<SliderAdapter> {
  88. private _dragOffset: number;
  89. constructor(adapter: SliderAdapter) {
  90. super({ ...SliderFoundation.defaultAdapter, ...adapter });
  91. }
  92. init() {
  93. this._checkCurrentValue();
  94. this._dragOffset = 0;
  95. }
  96. _checkCurrentValue() {
  97. const { currentValue, min, max } = this.getStates();
  98. let checked;
  99. if (Array.isArray(currentValue)) {
  100. checked = [];
  101. checked[0] = this._checkValidity(currentValue[0], min, max);
  102. checked[1] = this._checkValidity(currentValue[1], min, max);
  103. } else {
  104. checked = this._checkValidity(currentValue, min, max);
  105. }
  106. this._adapter.updateCurrentValue(checked);
  107. }
  108. /**
  109. * Untie event
  110. * @memberof SliderFoundation
  111. */
  112. destroy() {
  113. // debugger
  114. this._adapter.unSubscribeEventListener();
  115. }
  116. /**
  117. * Calculate the percentage corresponding to the current value for style calculation
  118. * @{}
  119. *
  120. * @memberof SliderFoundation
  121. */
  122. getMinAndMaxPercent = (value: number | number[]) => {
  123. // debugger
  124. const { range, min, max } = this._adapter.getProps();
  125. const minPercent = range ? (value[0] - min) / (max - min) : (value as number - min) / (max - min);
  126. const maxPercent = range ? (value[1] - min) / (max - min) : 1;
  127. return { min: this._checkValidity(minPercent), max: this._checkValidity(maxPercent) };
  128. };
  129. /**
  130. * Check if value is out of range
  131. * @memberof SliderFoundation
  132. */
  133. _checkValidity = (value: number, min = 0, max = 1) => {
  134. const checked = value > max ?
  135. max :
  136. value < min ?
  137. min :
  138. value;
  139. return checked;
  140. };
  141. /**
  142. * When render handle, the display and content of the tooltip are calculated according to the conditions
  143. * @visible: props passed in by the component
  144. * @formatter: tooltip content formatting function
  145. * @memberof SliderFoundation
  146. */
  147. computeHandleVisibleVal = (visible: SliderProps['tooltipVisible'], formatter: SliderProps['tipFormatter'], range: SliderProps['range']) => {
  148. // debugger;
  149. const { focusPos, currentValue } = this._adapter.getStates();
  150. const tipVisible = { min: false, max: false };
  151. let tipChildren;
  152. if (formatter) {
  153. tipChildren = {
  154. min: range ?
  155. formatter(this.outPutValue(currentValue[0])) :
  156. formatter(this.outPutValue(currentValue)),
  157. max: range ? formatter(this.outPutValue(currentValue[1])) : null,
  158. };
  159. } else {
  160. tipChildren = {
  161. min: range ? this.outPutValue(currentValue[0]) : this.outPutValue(currentValue),
  162. max: range ? this.outPutValue(currentValue[1]) : null,
  163. };
  164. }
  165. if (visible) {
  166. tipVisible.min = true;
  167. tipVisible.max = true;
  168. } else if (typeof visible === 'undefined' && formatter) {
  169. if (focusPos === 'min') {
  170. tipVisible.min = true;
  171. } else if (focusPos === 'max') {
  172. tipVisible.max = true;
  173. }
  174. }
  175. const result = {
  176. tipVisible,
  177. tipChildren,
  178. };
  179. return result;
  180. };
  181. /**
  182. * Calculate whether the value passed in is valid
  183. *
  184. * @memberof SliderFoundation
  185. */
  186. valueFormatIsCorrect = (value: SliderProps['value']) => {
  187. if (Array.isArray(value)) {
  188. return typeof value[0] === 'number' && typeof value[0] === 'number';
  189. } else {
  190. return typeof value === 'number';
  191. }
  192. };
  193. /**
  194. * Fix the mouse position to position the parent container relative to the position
  195. *
  196. * @memberof SliderFoundation
  197. */
  198. handleMousePos = (pageX: number, pageY: number) => {
  199. const parentRect = this._adapter.getParentRect();
  200. const scrollParent = this._adapter.getScrollParentVal();
  201. const parentX = parentRect ? parentRect.left : 0;
  202. const parentY = parentRect ? parentRect.top : 0;
  203. return { x: pageX - parentX + scrollParent.scrollLeft, y: pageY - parentY + scrollParent.scrollTop };
  204. };
  205. /**
  206. * Provides the nearest scrollable parent node of the current node, which is used to calculate the scrollTop and scrollLeft attributes
  207. *
  208. * @memberof SliderFoundation
  209. */
  210. getScrollParent = (element: HTMLElement) => {
  211. // TODO: move window document out of foundation.
  212. const el = element;
  213. const regex = /(auto|scroll)/;
  214. const style = (node: Element, prop: string) => window.getComputedStyle(node, null).getPropertyValue(prop);
  215. const scroll = (node: Element) => regex.test(style(node, 'overflow') + style(node, 'overflow-y') + style(node, 'overflow-x'));
  216. const scrollParent = (node: Element): Element => (
  217. !node || (node === document.body || !(node instanceof Element)) ? document.body : scroll(node) ? node : scrollParent(node.parentNode as Element)
  218. );
  219. return scrollParent(el);
  220. };
  221. /**
  222. * Fixed the event location, beyond the maximum, minimum, left and right, etc. directly modified to the effective location
  223. *
  224. * @memberof SliderFoundation
  225. */
  226. checkMeetMinMax = (position: number) => {
  227. // Returns the length of the distance to the left
  228. const { vertical, verticalReverse, range } = this._adapter.getProps();
  229. const value = this._adapter.getState('currentValue');
  230. const currentPos = this.transValueToPos(value);
  231. const { sliderX, sliderY, sliderWidth, sliderHeight } = this._adapter.getSliderLengths();
  232. const { chooseMovePos, isDrag } = this._adapter.getStates();
  233. const len = vertical ? sliderHeight : sliderWidth;
  234. let startPos;
  235. if (vertical && verticalReverse) {
  236. startPos = sliderY + len;
  237. } else {
  238. startPos = vertical ? sliderY : sliderX;
  239. }
  240. startPos = chooseMovePos === 'max' && isDrag ? currentPos[0] : startPos;
  241. // eslint-disable-next-line one-var
  242. let endPos;
  243. if (vertical && verticalReverse) {
  244. endPos = sliderY;
  245. } else {
  246. endPos = vertical ? sliderY + sliderHeight : sliderX + sliderWidth;
  247. }
  248. endPos = chooseMovePos === 'min' && isDrag && range ? currentPos[1] : endPos;
  249. if (vertical && verticalReverse) {
  250. if (position >= startPos) {
  251. position = startPos;
  252. } else if (position <= endPos) {
  253. position = endPos;
  254. }
  255. } else {
  256. if (position <= startPos) {
  257. position = startPos;
  258. } else if (position >= endPos) {
  259. position = endPos;
  260. }
  261. }
  262. return position;
  263. };
  264. /**
  265. * Converting location information to value requires processing if step is not 1 (invalid move returns false)
  266. *
  267. * @memberof SliderFoundation
  268. */
  269. transPosToValue = (mousePos: number, isMin: boolean) => {
  270. const pos = this.checkMeetMinMax(mousePos);
  271. const { min, max, currentValue } = this._adapter.getStates();
  272. const { range, vertical, step, verticalReverse } = this._adapter.getProps();
  273. const { sliderX, sliderY, sliderWidth, sliderHeight } = this._adapter.getSliderLengths();
  274. const startPos = vertical ? sliderY : sliderX;
  275. const len = vertical ? sliderHeight : sliderWidth;
  276. let stepValue;
  277. if (vertical && verticalReverse) {
  278. isMin = !isMin;
  279. stepValue = ((startPos + len - pos) / len) * (max - min) + min;
  280. } else {
  281. stepValue = ((pos - startPos) / len) * (max - min) + min;
  282. }
  283. // debugger
  284. // eslint-disable-next-line one-var
  285. let compareValue;
  286. if (range) {
  287. compareValue = isMin ? currentValue[0] : currentValue[1];
  288. } else {
  289. compareValue = currentValue;
  290. }
  291. if (step !== 1) { // Find nearest step point
  292. stepValue = Math.round(stepValue / step) * step;
  293. }
  294. if (range && stepValue !== compareValue) {
  295. if (vertical && verticalReverse) {
  296. return (isMin ? [currentValue[0], stepValue] : [stepValue, currentValue[1]]);
  297. } else {
  298. return isMin ? [stepValue, currentValue[1]] : [currentValue[0], stepValue];
  299. }
  300. } else if (!range && stepValue !== compareValue) {
  301. return (stepValue);
  302. } else {
  303. return false;
  304. }
  305. };
  306. /**
  307. * Convert value values into location information
  308. *
  309. * @memberof SliderFoundation
  310. */
  311. transValueToPos = (value: SliderProps['value']) => {
  312. const { min, max } = this._adapter.getStates();
  313. const { vertical, range, verticalReverse } = this._adapter.getProps();
  314. const { sliderX, sliderY, sliderWidth, sliderHeight } = this._adapter.getSliderLengths();
  315. const startPos = vertical ? sliderY : sliderX;
  316. const len = vertical ? sliderHeight : sliderWidth;
  317. if (range) {
  318. if (vertical && verticalReverse) {
  319. return [startPos + len - ((value[0] - min) * len) / (max - min), startPos + len - ((value[1] - min) * len) / (max - min)];
  320. } else {
  321. return [((value[0] - min) * len) / (max - min) + startPos, ((value[1] - min) * len) / (max - min) + startPos];
  322. }
  323. } else {
  324. return ((value as number - min) * len) / (max - min) + startPos;
  325. }
  326. };
  327. /**
  328. * Determine whether the mark should be highlighted: valid interval and include = false
  329. *
  330. * @memberof SliderFoundation
  331. */
  332. isMarkActive = (mark: number) => {
  333. const { min, max, range, included } = this._adapter.getProps();
  334. const currentValue = this._adapter.getState('currentValue');
  335. if (typeof (mark / 1) === 'number' && mark >= min && mark <= max) {
  336. if (range) {
  337. return (mark > currentValue[1] || mark < currentValue[0]) && included ? 'unActive' : 'active';
  338. } else {
  339. return mark <= currentValue && included ? 'active' : 'unActive';
  340. }
  341. } else {
  342. return false;
  343. }
  344. };
  345. /**
  346. * onchange output conversion, default rounding without decimal, step less than 1 has decimal
  347. *
  348. * @memberof SliderFoundation
  349. */
  350. outPutValue = (inputValue: SliderProps['value']) => {
  351. const step = this._adapter.getProp('step');
  352. let transWay = Math.round;
  353. if (step < 1 && step >= 0.1) {
  354. transWay = value => Math.round(value * 10) / 10;
  355. } else if (step < 0.1 && step >= 0.01) {
  356. transWay = value => Math.round(value * 100) / 100;
  357. } else if (step < 0.01 && step >= 0.001) {
  358. transWay = value => Math.round(value * 1000) / 1000;
  359. }
  360. if (Array.isArray(inputValue)) {
  361. return [transWay(inputValue[0]), transWay(inputValue[1])];
  362. } else {
  363. return transWay(inputValue);
  364. }
  365. };
  366. handleDisabledChange = (disabled: SliderState['disabled']) => {
  367. this._adapter.updateDisabled(disabled);
  368. };
  369. checkAndUpdateIsInRenderTreeState = () => this._adapter.checkAndUpdateIsInRenderTreeState();
  370. /**
  371. *
  372. *
  373. * @memberof SliderFoundation
  374. */
  375. handleValueChange = (prevValue: SliderProps['value'], nextValue: SliderProps['value']) => {
  376. const { min, max } = this._adapter.getStates();
  377. let resultState = null;
  378. const disableState = {};
  379. if (this.valueFormatIsCorrect(nextValue)) {
  380. if (Array.isArray(prevValue) && Array.isArray(nextValue)) {
  381. nextValue = [
  382. nextValue[0] < min ? min : nextValue[0], // Math.round(nextValue[0]),
  383. nextValue[1] > max ? max : nextValue[1], // Math.round(nextValue[1])
  384. ];
  385. // this._adapter.notifyChange(this.outPutValue(nextValue));
  386. resultState = Object.assign(disableState, {
  387. currentValue: nextValue,
  388. });
  389. }
  390. if (typeof prevValue === 'number' && typeof nextValue === 'number') {
  391. if (nextValue > max) {
  392. nextValue = max;
  393. } else {
  394. nextValue = nextValue < min ? min : nextValue; // Math.round(nextValue);
  395. }
  396. // this._adapter.notifyChange(this.outPutValue(nextValue));
  397. resultState = Object.assign(disableState, {
  398. currentValue: nextValue,
  399. });
  400. }
  401. } else {
  402. resultState = disableState;
  403. }
  404. if (resultState) {
  405. this._adapter.transNewPropsToState(resultState);
  406. }
  407. };
  408. onHandleDown = (e: any, handler: any) => {
  409. this._adapter.onHandleDown(e);
  410. const disabled = this._adapter.getState('disabled');
  411. const { vertical } = this._adapter.getProps();
  412. const { dragging } = this._adapter.getOverallVars();
  413. if (disabled) {
  414. return false;
  415. }
  416. this._adapter.setStateVal('isDrag', true);
  417. this._adapter.setStateVal('chooseMovePos', handler);
  418. if (handler === 'min') {
  419. this._adapter.setDragging([true, dragging[1]]);
  420. } else {
  421. this._adapter.setDragging([dragging[0], true]);
  422. }
  423. const mousePos = this.handleMousePos(e.pageX, e.pageY);
  424. let pos = vertical ? mousePos.y : mousePos.x;
  425. if (!this._adapter.isEventFromHandle(e)) {
  426. this._dragOffset = 0;
  427. } else {
  428. const handlePosition = this._getHandleCenterPosition(vertical, e.target);
  429. this._dragOffset = vertical ? pos - handlePosition : pos - handlePosition;
  430. pos = handlePosition;
  431. }
  432. return true;
  433. };
  434. onHandleMove = (e: any) => {
  435. this._adapter.setEventDefault(e);
  436. const { disabled, chooseMovePos } = this._adapter.getStates();
  437. const { vertical } = this._adapter.getProps();
  438. const { dragging } = this._adapter.getOverallVars();
  439. if (disabled) {
  440. return false;
  441. }
  442. this.onHandleEnter(chooseMovePos);
  443. const mousePos = this.handleMousePos(e.pageX, e.pageY);
  444. let pagePos = vertical ? mousePos.y : mousePos.x;
  445. pagePos = pagePos - this._dragOffset;
  446. if ((chooseMovePos === 'min' && dragging[0]) || (chooseMovePos === 'max' && dragging[1])) {
  447. this._adapter.onHandleMove(pagePos, chooseMovePos === 'min');
  448. }
  449. return true;
  450. };
  451. // run when user touch left or right handle.
  452. onHandleTouchStart = (e: any, handler: 'min' | 'max') => {
  453. const handleMinDom = this._adapter.getMinHandleEl().current;
  454. const handleMaxDom = this._adapter.getMaxHandleEl().current;
  455. if (e.target === handleMinDom || e.target === handleMaxDom) {
  456. e.preventDefault();
  457. e.stopPropagation();
  458. const touch = touchEventPolyfill(e.touches[0], e);
  459. this.onHandleDown(touch, handler);
  460. }
  461. };
  462. onHandleTouchMove = (e: any) => {
  463. const handleMinDom = this._adapter.getMinHandleEl().current;
  464. const handleMaxDom = this._adapter.getMaxHandleEl().current;
  465. if (e.target === handleMinDom || e.target === handleMaxDom) {
  466. const touch = touchEventPolyfill(e.touches[0], e);
  467. this.onHandleMove(touch);
  468. }
  469. };
  470. onHandleEnter = (pos: SliderState['focusPos']) => {
  471. // debugger;
  472. // this._adapter.setEventDefault(e);
  473. const { disabled, focusPos } = this._adapter.getStates();
  474. if (!disabled) {
  475. if (!focusPos && pos !== focusPos) {
  476. this._adapter.onHandleEnter(pos);
  477. }
  478. }
  479. };
  480. onHandleLeave = () => {
  481. // this._adapter.setEventDefault(e);
  482. const disabled = this._adapter.getState('disabled');
  483. if (!disabled) {
  484. this._adapter.onHandleLeave();
  485. }
  486. };
  487. onHandleUp = (e: any) => {
  488. this._adapter.onHandleUpBefore(e);
  489. // const value = this._adapter.getProp('value');
  490. const { disabled, chooseMovePos } = this._adapter.getStates();
  491. const { dragging } = this._adapter.getOverallVars();
  492. if (disabled) {
  493. return false;
  494. }
  495. if (chooseMovePos === 'min') {
  496. this._adapter.setDragging([false, dragging[1]]);
  497. } else {
  498. this._adapter.setDragging([dragging[0], false]);
  499. }
  500. this._adapter.setStateVal('isDrag', false);
  501. // this._adapter.setStateVal('chooseMovePos', '');
  502. this._adapter.onHandleLeave();
  503. this._adapter.onHandleUpAfter();
  504. return true;
  505. };
  506. // eslint-disable-next-line @typescript-eslint/no-empty-function
  507. onFocus = (e:any, handler: 'min'| 'max') => {}
  508. handleWrapClick = (e: any) => {
  509. const { disabled, isDrag } = this._adapter.getStates();
  510. if (isDrag || disabled || this._adapter.isEventFromHandle(e)) {
  511. return;
  512. }
  513. const { vertical } = this.getProps();
  514. const mousePos = this.handleMousePos(e.pageX, e.pageY);
  515. const position = vertical ? mousePos.y : mousePos.x;
  516. const isMin = this.checkWhichHandle(position);
  517. this.setHandlePos(position, isMin, true);
  518. };
  519. /**
  520. * Move the slider to the current click position
  521. *
  522. * @memberof SliderFoundation
  523. */
  524. setHandlePos = (position: number, isMin: boolean, clickTrack = false) => {
  525. this._adapter.onHandleMove(position, isMin, () => this._adapter.onHandleUpAfter(), clickTrack);
  526. };
  527. /**
  528. * Determine which slider should be moved currently
  529. *
  530. * @memberof SliderFoundation
  531. */
  532. checkWhichHandle = (pagePos: number) => {
  533. const { vertical, verticalReverse } = this.getProps();
  534. const { currentValue } = this._adapter.getStates();
  535. const currentPos = this.transValueToPos(currentValue);
  536. let isMin = true;
  537. if (Array.isArray(currentPos)) {
  538. // Slide on both sides
  539. if (
  540. pagePos > currentPos[1] ||
  541. Math.abs(pagePos - currentPos[0]) > Math.abs(pagePos - currentPos[1])
  542. ) {
  543. isMin = false;
  544. }
  545. }
  546. if (vertical && verticalReverse) {
  547. isMin = !isMin;
  548. }
  549. return isMin;
  550. };
  551. handleWrapperEnter = () => {
  552. this._adapter.setStateVal('showBoundary', true);
  553. };
  554. handleWrapperLeave = () => {
  555. this._adapter.setStateVal('showBoundary', false);
  556. };
  557. private _getHandleCenterPosition(vertical: boolean, handle: HTMLElement) {
  558. const pos = handle.getBoundingClientRect();
  559. const { x, y } = this.handleMousePos(pos.left + (pos.width * 0.5), pos.top + (pos.height * 0.5));
  560. return vertical ? y : x;
  561. }
  562. }