foundation.ts 22 KB

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