guide-drag.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. // 弹窗拖动
  2. import { ref, Ref, watch, nextTick, computed, reactive } from 'vue';
  3. type posType = {
  4. top: number;
  5. left: number;
  6. };
  7. type directionType =
  8. | 'TOP'
  9. | 'RIGHT'
  10. | 'BOTTOM'
  11. | 'LEFT'
  12. | 'TOP_RIGHT'
  13. | 'BOTTOM_RIGHT'
  14. | 'BOTTOM_LEFT'
  15. | 'TOP_LEFT';
  16. type baseSizeType = {
  17. /**
  18. * 允许拖动方向 上/上右/右/下右/下/下左/左/上左
  19. */
  20. resizeDirection: boolean[];
  21. layoutTopHeight: number;
  22. windowHeight: number;
  23. windowWidth: number;
  24. // 窗口模式的尺寸
  25. winWidth: number;
  26. winHeight: number;
  27. winMinWidth: number;
  28. minWidth: number;
  29. minHeight: number;
  30. maxHeight: number;
  31. maxWidth: number;
  32. transformX: number;
  33. transformY: number;
  34. defaultWidth: number;
  35. defaultHeight: number;
  36. width: number;
  37. height: number;
  38. };
  39. type initSizeType = {
  40. /** 默认宽 */
  41. width?: number;
  42. /** 默认高 */
  43. height?: number;
  44. /** 最小宽 */
  45. minWidth?: number;
  46. /** 最小高 */
  47. minHeight?: number;
  48. /** 允许拖动方向 上/上右/右/下右/下/下左/左/上左 */
  49. resizeDirection?: boolean[];
  50. /** 初始定位 */
  51. defaultPosition?: string;
  52. };
  53. /***
  54. * 初始化默认弹窗位置
  55. */
  56. const initPos = {
  57. right: 14,
  58. top: 60
  59. };
  60. const getSizeToUnit = (num: number, unit = 'px') => {
  61. return num > 0 ? num + unit : num + '';
  62. };
  63. /**
  64. * @params classList 可拖动地方的class值,也为唯一值
  65. * @params boxClass 容器class值必须为唯一值
  66. * @params dragShow 弹窗是否显示
  67. * @params initSize 默认尺寸
  68. */
  69. export default function useDrag(
  70. classList: string[],
  71. boxClass: string,
  72. dragShow: Ref<boolean>,
  73. initSize?: initSizeType
  74. ) {
  75. const windowInfo = reactive({
  76. // 小窗口 侧边大窗口
  77. currentType: 'SMALL' as 'SMALL' | 'LARGE',
  78. // 弹窗,还是还原
  79. windowType: 'SMALL' as 'SMALL' | 'LARGE',
  80. // showScreen: false, // 是否全屏显示
  81. showType: 'MENU' as 'MENU' | 'CONTENT' // 当前显示哪一部分 - 如果是全屏显示则无效
  82. });
  83. const pos = ref<posType>({
  84. top: -1, // -1 为初始值 代表没有缓存 默认居中
  85. left: -1
  86. });
  87. watch(
  88. () => windowInfo.windowType,
  89. () => {
  90. if (windowInfo.windowType === 'LARGE') {
  91. baseSize.resizeDirection = [
  92. true,
  93. true,
  94. true,
  95. true,
  96. true,
  97. true,
  98. true,
  99. true
  100. ];
  101. } else if (windowInfo.windowType === 'SMALL') {
  102. baseSize.resizeDirection = [
  103. true,
  104. false,
  105. false,
  106. false,
  107. true,
  108. false,
  109. false,
  110. false
  111. ];
  112. }
  113. const dragDirectionPoints = document.querySelectorAll(
  114. `.${boxClass} .dragDirectionPoint`
  115. );
  116. dragDirectionPoints.forEach((element: any, index) => {
  117. if (baseSize.resizeDirection[index]) {
  118. element.style.pointerEvents = 'all';
  119. } else {
  120. element.style.pointerEvents = 'none';
  121. }
  122. });
  123. }
  124. );
  125. const styleDrag = computed(() => {
  126. return {
  127. ...dragStyles,
  128. width: getSizeToUnit(baseSize.width),
  129. height: getSizeToUnit(baseSize.height),
  130. transform: `translate(${baseSize.transformX}px, ${baseSize.transformY}px)`
  131. };
  132. });
  133. const baseSize = reactive<baseSizeType>({
  134. resizeDirection: initSize?.resizeDirection || [
  135. true,
  136. false,
  137. false,
  138. false,
  139. true,
  140. false,
  141. false,
  142. false
  143. ],
  144. layoutTopHeight: 0,
  145. windowHeight: window.innerHeight,
  146. windowWidth: window.innerWidth,
  147. // 窗口模式的尺寸
  148. winWidth: 1010,
  149. winHeight: 650,
  150. winMinWidth: initSize?.minWidth || 800,
  151. minWidth: initSize?.minWidth || 400,
  152. minHeight: initSize?.minHeight || 340,
  153. maxHeight: window.innerHeight,
  154. maxWidth: window.innerWidth > 1024 ? 1024 : window.innerWidth,
  155. transformX: window.innerWidth - 400 - initPos.right,
  156. transformY: (window.innerHeight - 640) / 2,
  157. defaultWidth: initSize?.width || 400,
  158. defaultHeight: initSize?.height || 640,
  159. height: 0,
  160. width: 0
  161. });
  162. const dragStyles = reactive({
  163. maxHeight: getSizeToUnit(baseSize.maxHeight),
  164. minWidth: getSizeToUnit(baseSize.minWidth),
  165. minHeight: getSizeToUnit(baseSize.minHeight)
  166. });
  167. watch(dragShow, () => {
  168. if (dragShow.value) {
  169. // 初始化pos值
  170. window.addEventListener('resize', refreshPos);
  171. window.addEventListener('fullscreenchange', onFullscreenchange);
  172. nextTick(() => {
  173. document
  174. .querySelectorAll(`.${boxClass} .dragDirectionPoint`)
  175. .forEach((value: Element) => {
  176. value.remove();
  177. });
  178. const layoutTopHeight =
  179. document.querySelector('.layoutTop')?.clientHeight || 0;
  180. baseSize.layoutTopHeight = Math.ceil(layoutTopHeight);
  181. baseSize.windowHeight = window.innerHeight - layoutTopHeight;
  182. baseSize.windowWidth = window.innerWidth;
  183. baseSize.maxHeight = window.innerHeight - layoutTopHeight;
  184. baseSize.maxWidth = window.innerWidth > 1024 ? 1024 : window.innerWidth;
  185. // 判断窗口的高度与默认高度比例
  186. if (baseSize.defaultHeight >= baseSize.maxHeight) {
  187. baseSize.defaultHeight = baseSize.maxHeight - 100;
  188. }
  189. if (baseSize.winHeight >= baseSize.maxHeight) {
  190. baseSize.winHeight = baseSize.maxHeight - 100;
  191. }
  192. dragStyles.maxHeight = getSizeToUnit(baseSize.maxHeight);
  193. if (windowInfo.windowType === 'LARGE') {
  194. baseSize.transformX = (baseSize.windowWidth - baseSize.winWidth) / 2;
  195. baseSize.transformY =
  196. (baseSize.windowHeight - baseSize.winHeight) / 2 -
  197. baseSize.layoutTopHeight / 2;
  198. baseSize.width = baseSize.winWidth;
  199. baseSize.height = baseSize.winHeight;
  200. } else {
  201. const translateY =
  202. (baseSize.windowHeight - baseSize.defaultHeight) / 2;
  203. baseSize.transformX =
  204. baseSize.windowWidth - baseSize.defaultWidth - initPos.right;
  205. baseSize.transformY = translateY;
  206. baseSize.width = baseSize.defaultWidth;
  207. baseSize.height = baseSize.defaultHeight;
  208. }
  209. // 初始化定位
  210. if (initSize?.defaultPosition === 'center') {
  211. const transformX = (window.innerWidth - baseSize.defaultWidth) / 2;
  212. const transformY =
  213. (baseSize.windowHeight - baseSize.defaultHeight) / 2;
  214. baseSize.transformX = transformX;
  215. baseSize.transformY = transformY;
  216. }
  217. const boxClassDom = document.querySelector(
  218. `.${boxClass}`
  219. ) as HTMLElement;
  220. if (!boxClassDom) {
  221. return;
  222. }
  223. addReSizeDom(boxClassDom, baseSize.resizeDirection);
  224. classList.map((className: string) => {
  225. const classDom = document.querySelector(
  226. `.${className}`
  227. ) as HTMLElement;
  228. if (classDom) {
  229. classDom.style.cursor = 'move';
  230. drag(classDom, boxClassDom, baseSize);
  231. }
  232. });
  233. });
  234. } else {
  235. window.removeEventListener('resize', refreshPos);
  236. window.removeEventListener('fullscreenchange', onFullscreenchange);
  237. }
  238. });
  239. /**
  240. * 添加功能放大缩小操作DOM
  241. * @param parentElement {添加拖动父级元素}
  242. * @param direction {允许拖动的位置 上/上右/右/下右/下/下左/左/上左}
  243. */
  244. function addReSizeDom(parentElement: HTMLElement, direction: boolean[] = []) {
  245. function addResizeDirection(params: {
  246. width: string;
  247. height: string;
  248. direction: directionType;
  249. top?: string | any;
  250. right?: string | any;
  251. bottom?: string | any;
  252. left?: string | any;
  253. cursor: string;
  254. zIndex?: string;
  255. pointerEvents: string;
  256. }) {
  257. const dom = document.createElement('div');
  258. dom.className = 'dragDirectionPoint';
  259. dom.style.position = 'absolute';
  260. dom.style.userSelect = 'none';
  261. dom.style.width = params.width;
  262. dom.style.height = params.height;
  263. dom.style.left = params.left;
  264. dom.style.top = params.top;
  265. dom.style.bottom = params.bottom;
  266. dom.style.right = params.right;
  267. dom.style.zIndex = params.zIndex || '9';
  268. dom.style.cursor = params.cursor;
  269. dom.style.pointerEvents = params.pointerEvents;
  270. parentElement.appendChild(dom);
  271. drag(dom, parentElement, baseSize, 'RESIZE', params.direction);
  272. }
  273. // 上
  274. addResizeDirection({
  275. width: '100%',
  276. height: '10px',
  277. left: '0',
  278. top: '-5px',
  279. cursor: 'row-resize',
  280. direction: 'TOP',
  281. pointerEvents: direction[0] ? 'all' : 'none'
  282. });
  283. // 上右
  284. addResizeDirection({
  285. width: '20px',
  286. height: '20px',
  287. right: '-10px',
  288. top: '-10px',
  289. zIndex: '10',
  290. cursor: 'ne-resize',
  291. direction: 'TOP_RIGHT',
  292. pointerEvents: direction[1] ? 'all' : 'none'
  293. });
  294. // 右
  295. addResizeDirection({
  296. width: '10px',
  297. height: '100%',
  298. top: '0',
  299. right: '-5px',
  300. cursor: 'col-resize',
  301. direction: 'RIGHT',
  302. pointerEvents: direction[2] ? 'all' : 'none'
  303. });
  304. // 下右
  305. addResizeDirection({
  306. width: '20px',
  307. height: '20px',
  308. right: '-10px',
  309. bottom: '-10px',
  310. cursor: 'se-resize',
  311. zIndex: '10',
  312. direction: 'BOTTOM_RIGHT',
  313. pointerEvents: direction[3] ? 'all' : 'none'
  314. });
  315. // 下
  316. addResizeDirection({
  317. width: '100%',
  318. height: '10px',
  319. left: '0',
  320. bottom: '-5px',
  321. cursor: 'row-resize',
  322. direction: 'BOTTOM',
  323. pointerEvents: direction[4] ? 'all' : 'none'
  324. });
  325. // 下左
  326. addResizeDirection({
  327. width: '20px',
  328. height: '20px',
  329. left: '-10px',
  330. bottom: '-10px',
  331. cursor: 'sw-resize',
  332. zIndex: '10',
  333. direction: 'BOTTOM_LEFT',
  334. pointerEvents: direction[5] ? 'all' : 'none'
  335. });
  336. // 左
  337. addResizeDirection({
  338. width: '10px',
  339. height: '100%',
  340. top: '0',
  341. left: '-5px',
  342. cursor: 'col-resize',
  343. direction: 'LEFT',
  344. pointerEvents: direction[6] ? 'all' : 'none'
  345. });
  346. // 上左
  347. addResizeDirection({
  348. width: '20px',
  349. height: '20px',
  350. left: '-10px',
  351. top: '-10px',
  352. cursor: 'nw-resize',
  353. zIndex: '10',
  354. direction: 'TOP_LEFT',
  355. pointerEvents: direction[7] ? 'all' : 'none'
  356. });
  357. }
  358. let isResizeBlocked = false; // 标志是否阻止 resize 事件
  359. /** 全屏 */
  360. function isFullscreen() {
  361. return document.fullscreenElement || // 支持标准
  362. (document as any).mozFullScreenElement || // Firefox
  363. (document as any).webkitFullscreenElement // Chrome, Safari
  364. ? true
  365. : false;
  366. }
  367. function onFullscreenchange() {
  368. if (isFullscreen()) {
  369. isResizeBlocked = true;
  370. } else {
  371. setTimeout(() => {
  372. isResizeBlocked = false;
  373. }, 100);
  374. }
  375. }
  376. function refreshPos() {
  377. // 全屏切换的时候不做重置
  378. if (isResizeBlocked || isFullscreen()) return;
  379. windowInfo.currentType = 'SMALL';
  380. windowInfo.windowType = 'SMALL';
  381. baseSize.winMinWidth = initSize?.minWidth || 800;
  382. baseSize.minWidth = initSize?.minWidth || 400;
  383. baseSize.minHeight = initSize?.minHeight || 340;
  384. baseSize.defaultWidth = initSize?.width || 400;
  385. baseSize.defaultHeight = initSize?.height || 640;
  386. isResizeBlocked = false;
  387. dragShow.value = false;
  388. }
  389. /** 切换窗口 */
  390. function onScreen() {
  391. if (windowInfo.windowType === 'SMALL') {
  392. windowInfo.windowType = 'LARGE';
  393. baseSize.transformX = (baseSize.windowWidth - baseSize.winWidth) / 2;
  394. baseSize.transformY =
  395. (baseSize.windowHeight - baseSize.winHeight) / 2 -
  396. baseSize.layoutTopHeight / 2;
  397. baseSize.width = baseSize.winWidth;
  398. baseSize.height = baseSize.winHeight;
  399. } else if (windowInfo.windowType === 'LARGE') {
  400. windowInfo.windowType = 'SMALL';
  401. if (windowInfo.currentType === 'SMALL') {
  402. baseSize.transformX =
  403. baseSize.windowWidth - baseSize.defaultWidth - initPos.right;
  404. baseSize.transformY =
  405. baseSize.windowHeight - baseSize.defaultHeight - initPos.top;
  406. baseSize.width = baseSize.defaultWidth;
  407. baseSize.height = baseSize.defaultHeight;
  408. } else if (windowInfo.currentType === 'LARGE') {
  409. baseSize.transformX = baseSize.windowWidth - baseSize.defaultWidth;
  410. baseSize.transformY = 0;
  411. baseSize.width = baseSize.defaultWidth;
  412. baseSize.height = baseSize.maxHeight;
  413. }
  414. }
  415. }
  416. /** 格式化尺寸 */
  417. function onResize() {
  418. windowInfo.windowType = 'SMALL';
  419. if (windowInfo.currentType === 'SMALL') {
  420. windowInfo.currentType = 'LARGE';
  421. baseSize.transformX = baseSize.windowWidth - baseSize.defaultWidth;
  422. baseSize.transformY = 0;
  423. baseSize.width = baseSize.defaultWidth;
  424. baseSize.height = baseSize.maxHeight;
  425. } else if (windowInfo.currentType === 'LARGE') {
  426. windowInfo.currentType = 'SMALL';
  427. baseSize.transformX =
  428. baseSize.windowWidth - baseSize.defaultWidth - initPos.right;
  429. baseSize.transformY =
  430. baseSize.windowHeight - baseSize.defaultHeight - initPos.top;
  431. baseSize.width = baseSize.defaultWidth;
  432. baseSize.height = baseSize.defaultHeight;
  433. }
  434. }
  435. /** 重置样式 */
  436. function onReset() {
  437. windowInfo.currentType = 'SMALL';
  438. windowInfo.windowType = 'SMALL';
  439. if (initSize?.defaultPosition === 'center') {
  440. const transformX = (window.innerWidth - baseSize.defaultWidth) / 2;
  441. const transformY = (baseSize.windowHeight - baseSize.defaultHeight) / 2;
  442. baseSize.transformX = transformX;
  443. baseSize.transformY = transformY;
  444. } else {
  445. baseSize.transformX =
  446. baseSize.windowWidth - baseSize.defaultWidth - initPos.right;
  447. baseSize.transformY =
  448. baseSize.windowHeight - baseSize.defaultHeight - initPos.top;
  449. }
  450. baseSize.width = baseSize.defaultWidth;
  451. baseSize.height = baseSize.defaultHeight;
  452. }
  453. return {
  454. pos,
  455. baseSize,
  456. windowInfo,
  457. styleDrag,
  458. onScreen,
  459. onResize,
  460. onReset
  461. };
  462. }
  463. // 拖动
  464. function drag(
  465. el: HTMLElement,
  466. parentElement: HTMLElement,
  467. baseSize: baseSizeType,
  468. type = 'MOVE' as 'MOVE' | 'RESIZE',
  469. direction?: directionType
  470. ) {
  471. function onDown(e: MouseEvent | TouchEvent) {
  472. const isTouchEv = isTouchEvent(e);
  473. const event = isTouchEv ? e.touches[0] : e;
  474. const parentElementRect = parentElement.getBoundingClientRect();
  475. const downX = event.clientX;
  476. const downY = event.clientY;
  477. const clientWidth = document.documentElement.clientWidth;
  478. const clientHeight = document.documentElement.clientHeight;
  479. const maxLeft = clientWidth - parentElementRect.width;
  480. const maxTop =
  481. clientHeight - parentElementRect.height - baseSize.layoutTopHeight;
  482. const maxResizeLeft =
  483. clientWidth -
  484. baseSize.winMinWidth -
  485. (clientWidth - parentElementRect.right);
  486. // const maxResizeTop =
  487. // clientHeight - baseSize.minHeight - baseSize.layoutTopHeight;
  488. const minLeft = 0;
  489. const minTop = 0;
  490. const baseHeight = JSON.parse(JSON.stringify(baseSize.height));
  491. const baseWidth = JSON.parse(JSON.stringify(baseSize.width));
  492. function onTop(moveY: number) {
  493. const maxSuffix =
  494. parentElementRect.bottom -
  495. baseSize.minHeight -
  496. baseSize.layoutTopHeight;
  497. moveY = moveY > maxSuffix ? maxSuffix : moveY;
  498. const suffix = baseSize.transformY - moveY;
  499. if (suffix > 0 || baseSize.height > baseSize.minHeight) {
  500. baseSize.transformY = moveY;
  501. baseSize.height = baseSize.height + suffix;
  502. }
  503. }
  504. function onRight(moveX: number) {
  505. const suffix = Math.ceil(
  506. baseWidth + moveX - (baseSize.width + baseSize.transformX)
  507. );
  508. if (suffix > 0 || baseSize.width > baseSize.winMinWidth) {
  509. baseSize.width =
  510. baseSize.width + suffix >= baseSize.maxWidth
  511. ? baseSize.maxWidth
  512. : baseSize.width + suffix;
  513. }
  514. }
  515. function onBottom(moveY: number) {
  516. const suffix = Math.ceil(
  517. baseHeight + moveY - (baseSize.height + baseSize.transformY)
  518. );
  519. if (suffix > 0 || baseSize.height >= baseSize.minHeight) {
  520. if (baseSize.height + suffix <= baseSize.maxHeight) {
  521. baseSize.height = baseSize.height + suffix;
  522. }
  523. }
  524. }
  525. function onLeft(moveX: number) {
  526. moveX =
  527. moveX < minLeft
  528. ? minLeft
  529. : moveX > maxResizeLeft
  530. ? maxResizeLeft
  531. : moveX;
  532. const suffix = baseSize.transformX - moveX;
  533. if (suffix > 0 || baseSize.width > baseSize.winMinWidth) {
  534. if (baseSize.width + suffix <= baseSize.maxWidth) {
  535. baseSize.transformX = moveX;
  536. baseSize.width = baseSize.width + suffix;
  537. }
  538. }
  539. }
  540. function onMove(e: MouseEvent | TouchEvent) {
  541. const event = isTouchEvent(e) ? e.touches[0] : e;
  542. if (type === 'MOVE') {
  543. let moveX = parentElementRect.left + (event.clientX - downX);
  544. let moveY =
  545. parentElementRect.top -
  546. baseSize.layoutTopHeight +
  547. (event.clientY - downY);
  548. moveX = moveX < minLeft ? minLeft : moveX > maxLeft ? maxLeft : moveX;
  549. moveY = moveY < minTop ? minTop : moveY > maxTop ? maxTop : moveY;
  550. // 移动
  551. baseSize.transformY = moveY;
  552. baseSize.transformX = moveX;
  553. } else if (type === 'RESIZE') {
  554. const moveY =
  555. parentElementRect.top -
  556. baseSize.layoutTopHeight +
  557. (event.clientY - downY);
  558. const moveX = parentElementRect.left + (event.clientX - downX);
  559. // 拖动
  560. if (direction === 'TOP') {
  561. onTop(moveY);
  562. } else if (direction === 'RIGHT') {
  563. onRight(moveX);
  564. } else if (direction === 'BOTTOM') {
  565. onBottom(moveY);
  566. } else if (direction === 'LEFT') {
  567. onLeft(moveX);
  568. } else if (direction === 'TOP_RIGHT') {
  569. onTop(moveY);
  570. onRight(moveX);
  571. } else if (direction === 'BOTTOM_RIGHT') {
  572. onBottom(moveY);
  573. onRight(moveX);
  574. } else if (direction === 'BOTTOM_LEFT') {
  575. onBottom(moveY);
  576. onLeft(moveX);
  577. } else if (direction === 'TOP_LEFT') {
  578. onTop(moveY);
  579. onLeft(moveX);
  580. }
  581. }
  582. }
  583. function onUp() {
  584. document.removeEventListener(
  585. isTouchEv ? 'touchmove' : 'mousemove',
  586. onMove
  587. );
  588. document.removeEventListener(isTouchEv ? 'touchend' : 'mouseup', onUp);
  589. }
  590. document.addEventListener(isTouchEv ? 'touchmove' : 'mousemove', onMove);
  591. document.addEventListener(isTouchEv ? 'touchend' : 'mouseup', onUp);
  592. }
  593. el.addEventListener('mousedown', onDown);
  594. el.addEventListener('touchstart', onDown);
  595. }
  596. function isTouchEvent(e: MouseEvent | TouchEvent): e is TouchEvent {
  597. return window.TouchEvent && e instanceof window.TouchEvent;
  598. }