domDrag.ts 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. export interface ElementInfo{
  2. el: HTMLElement | null;
  3. left: number;
  4. top: number;
  5. parentLeft: number;
  6. parentTop: number;
  7. width: number;
  8. height: number;
  9. // 鼠标点击位置与元素位置的差值
  10. diffX: number;
  11. diffY: number;
  12. }
  13. export interface MouseInfo{
  14. x: number;
  15. y: number;
  16. }
  17. export interface MouseListener{
  18. (mouse: MouseInfo, el: ElementInfo): void;
  19. }
  20. // 元素拖动
  21. function getElementLeft(el: HTMLElement): number{
  22. let left = el.offsetLeft || 0;
  23. if(el.parentElement){
  24. left += getElementLeft(el.parentElement);
  25. }
  26. return left;
  27. }
  28. function getElementTop(el: HTMLElement): number{
  29. let top = el.offsetTop || 0;
  30. if(el.parentElement){
  31. top += getElementTop(el.parentElement);
  32. }
  33. return top;
  34. }
  35. function getElementDistanceToViewportEdge(element: HTMLElement): { top: number, right: number, bottom: number, left: number } {
  36. const rect = element.getBoundingClientRect();
  37. const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
  38. const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
  39. // 兼容性处理
  40. const scrollX = window.scrollX || window.pageXOffset || document.documentElement.scrollLeft;
  41. const scrollY = window.scrollY || window.pageYOffset || document.documentElement.scrollTop;
  42. const top = rect.top - scrollY;
  43. const right = viewportWidth - rect.right + scrollX;
  44. const bottom = viewportHeight - rect.bottom + scrollY;
  45. const left = rect.left - scrollX;
  46. return { top, right, bottom, left };
  47. }
  48. // 获取元素滚动条位置
  49. function getScrollLeft(el: HTMLElement): number{
  50. return el.scrollLeft || 0;
  51. }
  52. function getScrollTop(el: HTMLElement): number{
  53. return el.scrollTop || 0;
  54. }
  55. /**
  56. * 鼠标位置基于滚动条偏移
  57. * @param mouseInfo 直接修改此对象
  58. * @param el
  59. */
  60. function comMouseInfo(mouseInfo: MouseInfo, el?: HTMLElement | null): MouseInfo{
  61. // 父元素有滚动条的情况下,需要减去滚动条的偏移量
  62. if (el) {
  63. const scrollX = getScrollLeft(el);
  64. const scrollY = getScrollTop(el);
  65. // console.log(`scrollX: ${scrollX} scrollY: ${scrollY}`);
  66. mouseInfo.x += scrollX;
  67. mouseInfo.y += scrollY;
  68. }
  69. return mouseInfo;
  70. }
  71. export enum CollisionDirection {
  72. None = 0,
  73. Top = 1,
  74. Down = 2,
  75. Left = 3,
  76. Right = 4
  77. }
  78. export interface CollisionResult {
  79. colliding: boolean;
  80. direction: CollisionDirection;
  81. }
  82. export interface Rect {
  83. x: number;
  84. y: number;
  85. width: number;
  86. height: number;
  87. }
  88. export function detectCollisionDirection(rect1: Rect, rect2: Rect): CollisionResult {
  89. const horizontalOverlap = Math.min(rect1.x + rect1.width, rect2.x + rect2.width) - Math.max(rect1.x, rect2.x);
  90. const verticalOverlap = Math.min(rect1.y + rect1.height, rect2.y + rect2.height) - Math.max(rect1.y, rect2.y);
  91. if (horizontalOverlap > 0 && verticalOverlap > 0) {
  92. if (horizontalOverlap > verticalOverlap) {
  93. return {
  94. colliding: true,
  95. direction: rect1.y < rect2.y ? CollisionDirection.Top : CollisionDirection.Down
  96. };
  97. } else {
  98. return {
  99. colliding: true,
  100. direction: rect1.x < rect2.x ? CollisionDirection.Left : CollisionDirection.Right
  101. };
  102. }
  103. }
  104. return { colliding: false, direction: CollisionDirection.None };
  105. }
  106. export class Drag{
  107. constructor(el: HTMLElement) {
  108. // 绑定事件
  109. this.el = el;
  110. this.bindEvent();
  111. }
  112. el: HTMLElement;
  113. isMove: boolean = false;
  114. // 延迟时间, 同一时间内的合并为同一
  115. parent: ElementInfo = {
  116. el: null,
  117. left: 0,
  118. top: 0,
  119. parentTop: 0,
  120. parentLeft: 0,
  121. width: 0,
  122. height: 0,
  123. diffX: 0,
  124. diffY: 0
  125. };
  126. thisInfo: ElementInfo = {
  127. el: null,
  128. left: 0,
  129. top: 0,
  130. parentTop: 0,
  131. parentLeft: 0,
  132. width: 0,
  133. height: 0,
  134. diffX: 0,
  135. diffY: 0
  136. };
  137. startX: number = 0;
  138. startY: number = 0;
  139. public static Event = {
  140. moveStart: "moveStart",
  141. move: "move",
  142. moveEnd: "mouseEnd"
  143. }
  144. //
  145. private bindEvent() {
  146. const el = this.el;
  147. el.addEventListener('mousedown', this.downEvent)
  148. // todo 触摸支持
  149. let parent = el.parentElement;
  150. if (!parent) {
  151. parent = document.body;
  152. }
  153. const parentDistance = getElementDistanceToViewportEdge(parent);
  154. this.parent = {
  155. el: parent,
  156. left: parentDistance.left,
  157. top: parentDistance.top,
  158. parentTop: 0,
  159. parentLeft: 0,
  160. width: parent.offsetWidth,
  161. height: parent.offsetHeight,
  162. diffX: 0,
  163. diffY: 0
  164. }
  165. this.thisInfo = {
  166. el: el,
  167. left: getElementLeft(el),
  168. top: getElementTop(el),
  169. parentTop: this.parent.top,
  170. parentLeft: this.parent.left,
  171. width: el.offsetWidth,
  172. height: el.offsetHeight,
  173. diffX: 0,
  174. diffY: 0
  175. }
  176. }
  177. downEvent = (e: MouseEvent) => {
  178. if(this.isMove){
  179. return
  180. }
  181. // 获取鼠标位置
  182. const x = e.clientX;
  183. const y = e.clientY;
  184. this.isMove = true;
  185. // 计算元素偏移差值
  186. const diffX = e.offsetX;
  187. const diffY = e.offsetY;
  188. let mouseInfo: MouseInfo = {
  189. x: x,
  190. y: y
  191. }
  192. this.startX = x;
  193. this.startY = y;
  194. this.thisInfo = {
  195. ...this.thisInfo,
  196. diffX: diffX,
  197. diffY: diffY
  198. }
  199. if (this.parent.el) {
  200. const parentDistance = getElementDistanceToViewportEdge(this.parent.el)
  201. this.thisInfo.parentTop = parentDistance.top
  202. this.thisInfo.parentLeft = parentDistance.left
  203. comMouseInfo(mouseInfo, this.parent.el);
  204. }
  205. this.moveStart && this.moveStart(mouseInfo, this.thisInfo);
  206. setTimeout(()=>{
  207. document.addEventListener('pointermove', this.mouseMove);
  208. }, 100)
  209. document.addEventListener('mouseup', this.mouseUp);
  210. }
  211. mouseUp = (e: MouseEvent) => {
  212. this.isMove = false;
  213. // 获取鼠标位置
  214. const x = e.clientX;
  215. const y = e.clientY;
  216. let mouseInfo: MouseInfo = {
  217. x: x,
  218. y: y
  219. }
  220. // 解除事件绑定
  221. document.removeEventListener('pointermove', this.mouseMove);
  222. document.removeEventListener('mouseup', this.mouseUp);
  223. this.moveEnd && this.moveEnd(mouseInfo , this.thisInfo);
  224. }
  225. mouseMove = (e: MouseEvent) => {
  226. if (!this.isMove){
  227. // 已经被取消
  228. document.removeEventListener('pointermove', this.mouseMove);
  229. return
  230. }
  231. // 获取鼠标位置
  232. const x = e.pageX;
  233. const y = e.pageY;
  234. let mouseInfo: MouseInfo = {
  235. x: x,
  236. y: y
  237. }
  238. comMouseInfo(mouseInfo, this.parent.el);
  239. this.move && this.move(mouseInfo, this.thisInfo);
  240. }
  241. moveStart: MouseListener | null = null;
  242. move: MouseListener | null = null;
  243. moveEnd: MouseListener | null = null;
  244. public on(eventName: string, callback: MouseListener) {
  245. switch(eventName) {
  246. case Drag.Event.moveStart:
  247. this.moveStart = callback;
  248. break;
  249. case Drag.Event.move:
  250. this.move = callback;
  251. break;
  252. case Drag.Event.moveEnd:
  253. this.moveEnd = callback;
  254. break;
  255. default:
  256. throw new Error('Invalid event name');
  257. }
  258. }
  259. public off(eventName: string) {
  260. switch(eventName) {
  261. case Drag.Event.moveStart:
  262. this.moveStart = null;
  263. break;
  264. case Drag.Event.move:
  265. this.move = null;
  266. break;
  267. case Drag.Event.moveEnd:
  268. this.moveEnd = null;
  269. break;
  270. default:
  271. throw new Error('Invalid event name');
  272. }
  273. }
  274. public destroy() {
  275. // 移除事件
  276. this.el.removeEventListener('mousedown', this.downEvent);
  277. document.removeEventListener('mouseup', this.mouseUp);
  278. document.removeEventListener('mousemove', this.mouseMove);
  279. this.off(Drag.Event.moveStart);
  280. this.off(Drag.Event.move);
  281. this.off(Drag.Event.moveEnd);
  282. }
  283. }