foundation.ts 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import BaseFoundation, { DefaultAdapter } from '../base/foundation';
  2. export function clampValueInRange(value: number, min: number, max: number) {
  3. return Math.min(Math.max(value, min), max);
  4. }
  5. export interface DragMoveAdapter<P = Record<string, any>, S = Record<string, any>> extends DefaultAdapter<P, S> {
  6. getDragElement: () => HTMLElement;
  7. getConstrainer: () => HTMLElement | null;
  8. getHandler: () => HTMLElement;
  9. notifyMouseDown?: (e: MouseEvent) => void;
  10. notifyMouseMove?: (e: MouseEvent) => void;
  11. notifyMouseUp?: (e: MouseEvent) => void;
  12. notifyTouchStart?: (e: TouchEvent) => void;
  13. notifyTouchMove?: (e: TouchEvent) => void;
  14. notifyTouchEnd?: (e: TouchEvent) => void;
  15. notifyTouchCancel?: (e: TouchEvent) => void
  16. }
  17. export default class DragMoveFoundation<P = Record<string, any>, S = Record<string, any>> extends BaseFoundation<DragMoveAdapter<P, S>, P, S> {
  18. element: HTMLElement;
  19. xMax: number;
  20. xMin: number;
  21. yMax: number;
  22. yMin: number;
  23. startOffsetX: number;
  24. startOffsetY: number;
  25. get constrainer() {
  26. return this._adapter.getConstrainer();
  27. }
  28. get handler() {
  29. return this._adapter.getHandler();
  30. }
  31. constructor(adapter: DragMoveAdapter<P, S>) {
  32. super({ ...adapter });
  33. }
  34. init() {
  35. const element = this._adapter.getDragElement();
  36. if (!element) {
  37. throw new Error('drag element must be a valid element');
  38. }
  39. this.element = element;
  40. this.element.style.position = 'absolute';
  41. this.handler.style.cursor = 'move';
  42. this._registerStartEvent();
  43. }
  44. _registerStartEvent = () => {
  45. this.handler.addEventListener('mousedown', this.onMouseDown);
  46. this.handler.addEventListener('touchstart', this.onTouchStart);
  47. }
  48. _unRegisterStartEvent = () => {
  49. this.handler.removeEventListener('mousedown', this.onMouseDown);
  50. this.handler.removeEventListener('touchstart', this.onTouchStart);
  51. }
  52. destroy() {
  53. this._unRegisterStartEvent();
  54. this._unRegisterEvent();
  55. }
  56. _registerDocMouseEvent = () => {
  57. document.addEventListener('mousemove', this._onMouseMove);
  58. document.addEventListener('mouseup', this._onMouseUp);
  59. }
  60. _unRegisterDocMouseEvent = () => {
  61. document.removeEventListener('mousemove', this._onMouseMove);
  62. document.removeEventListener('mouseup', this._onMouseUp);
  63. }
  64. _registerDocTouchEvent = () => {
  65. document.addEventListener('touchend', this._onTouchEnd);
  66. document.addEventListener('touchmove', this._onTouchMove);
  67. document.addEventListener('touchcancel', this._onTouchCancel);
  68. }
  69. _unRegisterDocTouchEvent = () => {
  70. document.removeEventListener('touchend', this._onTouchEnd);
  71. document.removeEventListener('touchmove', this._onTouchMove);
  72. document.removeEventListener('touchcancel', this._onTouchCancel);
  73. }
  74. _unRegisterEvent() {
  75. this._unRegisterDocMouseEvent();
  76. this._unRegisterDocTouchEvent();
  77. }
  78. _calcMoveRange() {
  79. // Calculate the range within which an element can move
  80. if (this.constrainer) {
  81. let node = this.element.offsetParent as HTMLElement;
  82. let startX = 0;
  83. let startY = 0;
  84. while (node !== this.constrainer && node !== null) {
  85. startX -= node.offsetLeft;
  86. startY -= node.offsetTop;
  87. node = node.offsetParent as any;
  88. }
  89. this.xMin = startX;
  90. this.xMax = startX + this.constrainer.offsetWidth - this.element.offsetWidth;
  91. this.yMin = startY;
  92. this.yMax = startY + this.constrainer.offsetHeight - this.element.offsetHeight;
  93. }
  94. }
  95. _allowMove(e: MouseEvent | TouchEvent) {
  96. const { allowMove, allowInputDrag } = this.getProps();
  97. // When the clicked object is an input or textarea, clicking should be allowed but dragging should not be allowed.
  98. if (!allowInputDrag) {
  99. let target = (e.target as HTMLElement).tagName.toLowerCase();
  100. if (target === 'input' || target === 'textarea') {
  101. return;
  102. }
  103. }
  104. if (allowMove) {
  105. return allowMove(e, this.element);
  106. }
  107. return true;
  108. }
  109. _calcOffset = (e: Touch | MouseEvent) => {
  110. this.startOffsetX = e.clientX - this.element.offsetLeft;
  111. this.startOffsetY = e.clientY - this.element.offsetTop;
  112. }
  113. _preventDefault = (e: MouseEvent | TouchEvent) => {
  114. // prevent default behavior, avoid other element(like img, text) be selected
  115. e.preventDefault();
  116. }
  117. onMouseDown = (e: MouseEvent) => {
  118. this._calcMoveRange();
  119. this._adapter.notifyMouseDown(e);
  120. if (!this._allowMove(e)) {
  121. return;
  122. }
  123. this._registerDocMouseEvent();
  124. // store origin offset
  125. this._calcOffset(e);
  126. this._preventDefault(e);
  127. }
  128. onTouchStart = (e: TouchEvent) => {
  129. this._calcMoveRange();
  130. this._adapter.notifyTouchStart(e);
  131. if (!this._allowMove(e)) {
  132. return;
  133. }
  134. this._registerDocTouchEvent();
  135. const touch = e.targetTouches[0];
  136. this._calcOffset(touch);
  137. this._preventDefault(e);
  138. }
  139. _changePos = (e: Touch | MouseEvent) => {
  140. const { customMove } = this.getProps();
  141. let newLeft = e.clientX - this.startOffsetX;
  142. let newTop = e.clientY - this.startOffsetY;
  143. if (this.constrainer) {
  144. newLeft = clampValueInRange(newLeft, this.xMin, this.xMax);
  145. newTop = clampValueInRange(newTop, this.yMin, this.yMax);
  146. }
  147. requestAnimationFrame(() => {
  148. if (customMove) {
  149. customMove(this.element, newTop, newLeft);
  150. return;
  151. }
  152. this.element.style.top = newTop + 'px';
  153. this.element.style.left = newLeft + 'px';
  154. });
  155. }
  156. _onMouseMove = (e: MouseEvent) => {
  157. this._adapter.notifyMouseMove(e);
  158. this._changePos(e);
  159. }
  160. _onTouchMove = (e: TouchEvent) => {
  161. this._adapter.notifyTouchMove(e);
  162. const touch = e.targetTouches[0];
  163. this._changePos(touch);
  164. }
  165. _onMouseUp = (e: MouseEvent) => {
  166. this._adapter.notifyMouseUp(e);
  167. this._unRegisterDocMouseEvent();
  168. }
  169. _onTouchEnd = (e: TouchEvent) => {
  170. this._adapter.notifyTouchEnd(e);
  171. this._unRegisterDocTouchEvent();
  172. }
  173. _onTouchCancel = (e: TouchEvent) => {
  174. this._adapter.notifyTouchCancel(e);
  175. this._unRegisterDocTouchEvent();
  176. }
  177. }