guide-drag.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  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. console.log('first - show', boxClass);
  173. nextTick(() => {
  174. const layoutTopHeight =
  175. document.querySelector('.layoutTop')?.clientHeight || 0;
  176. baseSize.layoutTopHeight = Math.ceil(layoutTopHeight);
  177. baseSize.windowHeight = window.innerHeight - layoutTopHeight;
  178. baseSize.windowWidth = window.innerWidth;
  179. baseSize.maxHeight = window.innerHeight - layoutTopHeight;
  180. baseSize.maxWidth = window.innerWidth > 1024 ? 1024 : window.innerWidth;
  181. // 判断窗口的高度与默认高度比例
  182. if (baseSize.defaultHeight >= baseSize.maxHeight) {
  183. baseSize.defaultHeight = baseSize.maxHeight - 100;
  184. }
  185. if (baseSize.winHeight >= baseSize.maxHeight) {
  186. baseSize.winHeight = baseSize.maxHeight - 100;
  187. }
  188. const translateY = (baseSize.windowHeight - baseSize.defaultHeight) / 2;
  189. baseSize.transformX =
  190. baseSize.windowWidth - baseSize.defaultWidth - initPos.right;
  191. baseSize.transformY = translateY;
  192. dragStyles.maxHeight = getSizeToUnit(baseSize.maxHeight);
  193. baseSize.width = baseSize.defaultWidth;
  194. baseSize.height = baseSize.defaultHeight;
  195. // 初始化定位
  196. if (initSize?.defaultPosition === 'center') {
  197. const transformX = (window.innerWidth - baseSize.defaultWidth) / 2;
  198. const transformY =
  199. (baseSize.windowHeight - baseSize.defaultHeight) / 2;
  200. baseSize.transformX = transformX;
  201. baseSize.transformY = transformY;
  202. }
  203. const boxClassDom = document.querySelector(
  204. `.${boxClass}`
  205. ) as HTMLElement;
  206. if (!boxClassDom) {
  207. return;
  208. }
  209. addReSizeDom(boxClassDom, baseSize.resizeDirection);
  210. classList.map((className: string) => {
  211. const classDom = document.querySelector(
  212. `.${className}`
  213. ) as HTMLElement;
  214. if (classDom) {
  215. classDom.style.cursor = 'move';
  216. drag(classDom, boxClassDom, baseSize);
  217. }
  218. });
  219. });
  220. } else {
  221. window.removeEventListener('resize', refreshPos);
  222. window.removeEventListener('fullscreenchange', onFullscreenchange);
  223. }
  224. });
  225. /**
  226. * 添加功能放大缩小操作DOM
  227. * @param parentElement {添加拖动父级元素}
  228. * @param direction {允许拖动的位置 上/上右/右/下右/下/下左/左/上左}
  229. */
  230. function addReSizeDom(parentElement: HTMLElement, direction: boolean[] = []) {
  231. function addResizeDirection(params: {
  232. width: string;
  233. height: string;
  234. direction: directionType;
  235. top?: string | any;
  236. right?: string | any;
  237. bottom?: string | any;
  238. left?: string | any;
  239. cursor: string;
  240. zIndex?: string;
  241. pointerEvents: string;
  242. }) {
  243. const dom = document.createElement('div');
  244. dom.className = 'dragDirectionPoint';
  245. dom.style.position = 'absolute';
  246. dom.style.userSelect = 'none';
  247. dom.style.width = params.width;
  248. dom.style.height = params.height;
  249. dom.style.left = params.left;
  250. dom.style.top = params.top;
  251. dom.style.bottom = params.bottom;
  252. dom.style.right = params.right;
  253. dom.style.zIndex = params.zIndex || '9';
  254. dom.style.cursor = params.cursor;
  255. dom.style.pointerEvents = params.pointerEvents;
  256. parentElement.appendChild(dom);
  257. drag(dom, parentElement, baseSize, 'RESIZE', params.direction);
  258. }
  259. // 上
  260. addResizeDirection({
  261. width: '100%',
  262. height: '10px',
  263. left: '0',
  264. top: '-5px',
  265. cursor: 'row-resize',
  266. direction: 'TOP',
  267. pointerEvents: direction[0] ? 'all' : 'none'
  268. });
  269. // 上右
  270. addResizeDirection({
  271. width: '20px',
  272. height: '20px',
  273. right: '-10px',
  274. top: '-10px',
  275. zIndex: '10',
  276. cursor: 'ne-resize',
  277. direction: 'TOP_RIGHT',
  278. pointerEvents: direction[1] ? 'all' : 'none'
  279. });
  280. // 右
  281. addResizeDirection({
  282. width: '10px',
  283. height: '100%',
  284. top: '0',
  285. right: '-5px',
  286. cursor: 'col-resize',
  287. direction: 'RIGHT',
  288. pointerEvents: direction[2] ? 'all' : 'none'
  289. });
  290. // 下右
  291. addResizeDirection({
  292. width: '20px',
  293. height: '20px',
  294. right: '-10px',
  295. bottom: '-10px',
  296. cursor: 'se-resize',
  297. zIndex: '10',
  298. direction: 'BOTTOM_RIGHT',
  299. pointerEvents: direction[3] ? 'all' : 'none'
  300. });
  301. // 下
  302. addResizeDirection({
  303. width: '100%',
  304. height: '10px',
  305. left: '0',
  306. bottom: '-5px',
  307. cursor: 'row-resize',
  308. direction: 'BOTTOM',
  309. pointerEvents: direction[4] ? 'all' : 'none'
  310. });
  311. // 下左
  312. addResizeDirection({
  313. width: '20px',
  314. height: '20px',
  315. left: '-10px',
  316. bottom: '-10px',
  317. cursor: 'sw-resize',
  318. zIndex: '10',
  319. direction: 'BOTTOM_LEFT',
  320. pointerEvents: direction[5] ? 'all' : 'none'
  321. });
  322. // 左
  323. addResizeDirection({
  324. width: '10px',
  325. height: '100%',
  326. top: '0',
  327. left: '-5px',
  328. cursor: 'col-resize',
  329. direction: 'LEFT',
  330. pointerEvents: direction[6] ? 'all' : 'none'
  331. });
  332. // 上左
  333. addResizeDirection({
  334. width: '20px',
  335. height: '20px',
  336. left: '-10px',
  337. top: '-10px',
  338. cursor: 'nw-resize',
  339. zIndex: '10',
  340. direction: 'TOP_LEFT',
  341. pointerEvents: direction[7] ? 'all' : 'none'
  342. });
  343. }
  344. let isResizeBlocked = false; // 标志是否阻止 resize 事件
  345. /** 全屏 */
  346. function isFullscreen() {
  347. return document.fullscreenElement || // 支持标准
  348. (document as any).mozFullScreenElement || // Firefox
  349. (document as any).webkitFullscreenElement // Chrome, Safari
  350. ? true
  351. : false;
  352. }
  353. function onFullscreenchange() {
  354. if (isFullscreen()) {
  355. isResizeBlocked = true;
  356. } else {
  357. setTimeout(() => {
  358. isResizeBlocked = false;
  359. }, 100);
  360. }
  361. }
  362. function refreshPos() {
  363. // 全屏切换的时候不做重置
  364. if (isResizeBlocked || isFullscreen()) return;
  365. windowInfo.currentType = 'SMALL';
  366. windowInfo.windowType = 'SMALL';
  367. baseSize.winMinWidth = initSize?.minWidth || 800;
  368. baseSize.minWidth = initSize?.minWidth || 400;
  369. baseSize.minHeight = initSize?.minHeight || 340;
  370. baseSize.defaultWidth = initSize?.width || 400;
  371. baseSize.defaultHeight = initSize?.height || 640;
  372. isResizeBlocked = false;
  373. dragShow.value = false;
  374. }
  375. /** 切换窗口 */
  376. function onScreen() {
  377. if (windowInfo.windowType === 'SMALL') {
  378. windowInfo.windowType = 'LARGE';
  379. baseSize.transformX = (baseSize.windowWidth - baseSize.winWidth) / 2;
  380. baseSize.transformY =
  381. (baseSize.windowHeight - baseSize.winHeight) / 2 -
  382. baseSize.layoutTopHeight / 2;
  383. baseSize.width = baseSize.winWidth;
  384. baseSize.height = baseSize.winHeight;
  385. } else if (windowInfo.windowType === 'LARGE') {
  386. windowInfo.windowType = 'SMALL';
  387. if (windowInfo.currentType === 'SMALL') {
  388. baseSize.transformX =
  389. baseSize.windowWidth - baseSize.defaultWidth - initPos.right;
  390. baseSize.transformY =
  391. baseSize.windowHeight - baseSize.defaultHeight - initPos.top;
  392. baseSize.width = baseSize.defaultWidth;
  393. baseSize.height = baseSize.defaultHeight;
  394. } else if (windowInfo.currentType === 'LARGE') {
  395. baseSize.transformX = baseSize.windowWidth - baseSize.defaultWidth;
  396. baseSize.transformY = 0;
  397. baseSize.width = baseSize.defaultWidth;
  398. baseSize.height = baseSize.maxHeight;
  399. }
  400. }
  401. }
  402. /** 格式化尺寸 */
  403. function onResize() {
  404. windowInfo.windowType = 'SMALL';
  405. if (windowInfo.currentType === 'SMALL') {
  406. windowInfo.currentType = 'LARGE';
  407. baseSize.transformX = baseSize.windowWidth - baseSize.defaultWidth;
  408. baseSize.transformY = 0;
  409. baseSize.width = baseSize.defaultWidth;
  410. baseSize.height = baseSize.maxHeight;
  411. } else if (windowInfo.currentType === 'LARGE') {
  412. windowInfo.currentType = 'SMALL';
  413. baseSize.transformX =
  414. baseSize.windowWidth - baseSize.defaultWidth - initPos.right;
  415. baseSize.transformY =
  416. baseSize.windowHeight - baseSize.defaultHeight - initPos.top;
  417. baseSize.width = baseSize.defaultWidth;
  418. baseSize.height = baseSize.defaultHeight;
  419. }
  420. }
  421. /** 重置样式 */
  422. function onReset() {
  423. windowInfo.currentType = 'SMALL';
  424. windowInfo.windowType = 'SMALL';
  425. if (initSize?.defaultPosition === 'center') {
  426. const transformX = (window.innerWidth - baseSize.defaultWidth) / 2;
  427. const transformY = (baseSize.windowHeight - baseSize.defaultHeight) / 2;
  428. baseSize.transformX = transformX;
  429. baseSize.transformY = transformY;
  430. } else {
  431. baseSize.transformX =
  432. baseSize.windowWidth - baseSize.defaultWidth - initPos.right;
  433. baseSize.transformY =
  434. baseSize.windowHeight - baseSize.defaultHeight - initPos.top;
  435. }
  436. baseSize.width = baseSize.defaultWidth;
  437. baseSize.height = baseSize.defaultHeight;
  438. }
  439. return {
  440. pos,
  441. baseSize,
  442. windowInfo,
  443. styleDrag,
  444. onScreen,
  445. onResize,
  446. onReset
  447. };
  448. }
  449. // 拖动
  450. function drag(
  451. el: HTMLElement,
  452. parentElement: HTMLElement,
  453. baseSize: baseSizeType,
  454. type = 'MOVE' as 'MOVE' | 'RESIZE',
  455. direction?: directionType
  456. ) {
  457. function onDown(e: MouseEvent | TouchEvent) {
  458. const isTouchEv = isTouchEvent(e);
  459. const event = isTouchEv ? e.touches[0] : e;
  460. const parentElementRect = parentElement.getBoundingClientRect();
  461. const downX = event.clientX;
  462. const downY = event.clientY;
  463. const clientWidth = document.documentElement.clientWidth;
  464. const clientHeight = document.documentElement.clientHeight;
  465. const maxLeft = clientWidth - parentElementRect.width;
  466. const maxTop =
  467. clientHeight - parentElementRect.height - baseSize.layoutTopHeight;
  468. const maxResizeLeft =
  469. clientWidth -
  470. baseSize.winMinWidth -
  471. (clientWidth - parentElementRect.right);
  472. // const maxResizeTop =
  473. // clientHeight - baseSize.minHeight - baseSize.layoutTopHeight;
  474. const minLeft = 0;
  475. const minTop = 0;
  476. const baseHeight = JSON.parse(JSON.stringify(baseSize.height));
  477. const baseWidth = JSON.parse(JSON.stringify(baseSize.width));
  478. function onTop(moveY: number) {
  479. const maxSuffix =
  480. parentElementRect.bottom -
  481. baseSize.minHeight -
  482. baseSize.layoutTopHeight;
  483. moveY = moveY > maxSuffix ? maxSuffix : moveY;
  484. const suffix = baseSize.transformY - moveY;
  485. if (suffix > 0 || baseSize.height > baseSize.minHeight) {
  486. baseSize.transformY = moveY;
  487. baseSize.height = baseSize.height + suffix;
  488. }
  489. }
  490. function onRight(moveX: number) {
  491. const suffix = Math.ceil(
  492. baseWidth + moveX - (baseSize.width + baseSize.transformX)
  493. );
  494. if (suffix > 0 || baseSize.width > baseSize.winMinWidth) {
  495. baseSize.width =
  496. baseSize.width + suffix >= baseSize.maxWidth
  497. ? baseSize.maxWidth
  498. : baseSize.width + suffix;
  499. }
  500. }
  501. function onBottom(moveY: number) {
  502. const suffix = Math.ceil(
  503. baseHeight + moveY - (baseSize.height + baseSize.transformY)
  504. );
  505. if (suffix > 0 || baseSize.height >= baseSize.minHeight) {
  506. if (baseSize.height + suffix <= baseSize.maxHeight) {
  507. baseSize.height = baseSize.height + suffix;
  508. }
  509. }
  510. }
  511. function onLeft(moveX: number) {
  512. moveX =
  513. moveX < minLeft
  514. ? minLeft
  515. : moveX > maxResizeLeft
  516. ? maxResizeLeft
  517. : moveX;
  518. const suffix = baseSize.transformX - moveX;
  519. if (suffix > 0 || baseSize.width > baseSize.winMinWidth) {
  520. if (baseSize.width + suffix <= baseSize.maxWidth) {
  521. baseSize.transformX = moveX;
  522. baseSize.width = baseSize.width + suffix;
  523. }
  524. }
  525. }
  526. function onMove(e: MouseEvent | TouchEvent) {
  527. const event = isTouchEvent(e) ? e.touches[0] : e;
  528. if (type === 'MOVE') {
  529. let moveX = parentElementRect.left + (event.clientX - downX);
  530. let moveY =
  531. parentElementRect.top -
  532. baseSize.layoutTopHeight +
  533. (event.clientY - downY);
  534. moveX = moveX < minLeft ? minLeft : moveX > maxLeft ? maxLeft : moveX;
  535. moveY = moveY < minTop ? minTop : moveY > maxTop ? maxTop : moveY;
  536. // 移动
  537. baseSize.transformY = moveY;
  538. baseSize.transformX = moveX;
  539. } else if (type === 'RESIZE') {
  540. const moveY =
  541. parentElementRect.top -
  542. baseSize.layoutTopHeight +
  543. (event.clientY - downY);
  544. const moveX = parentElementRect.left + (event.clientX - downX);
  545. // 拖动
  546. if (direction === 'TOP') {
  547. onTop(moveY);
  548. } else if (direction === 'RIGHT') {
  549. onRight(moveX);
  550. } else if (direction === 'BOTTOM') {
  551. onBottom(moveY);
  552. } else if (direction === 'LEFT') {
  553. onLeft(moveX);
  554. } else if (direction === 'TOP_RIGHT') {
  555. onTop(moveY);
  556. onRight(moveX);
  557. } else if (direction === 'BOTTOM_RIGHT') {
  558. onBottom(moveY);
  559. onRight(moveX);
  560. } else if (direction === 'BOTTOM_LEFT') {
  561. onBottom(moveY);
  562. onLeft(moveX);
  563. } else if (direction === 'TOP_LEFT') {
  564. onTop(moveY);
  565. onLeft(moveX);
  566. }
  567. }
  568. }
  569. function onUp() {
  570. document.removeEventListener(
  571. isTouchEv ? 'touchmove' : 'mousemove',
  572. onMove
  573. );
  574. document.removeEventListener(isTouchEv ? 'touchend' : 'mouseup', onUp);
  575. }
  576. document.addEventListener(isTouchEv ? 'touchmove' : 'mousemove', onMove);
  577. document.addEventListener(isTouchEv ? 'touchend' : 'mouseup', onUp);
  578. }
  579. el.addEventListener('mousedown', onDown);
  580. el.addEventListener('touchstart', onDown);
  581. }
  582. function isTouchEvent(e: MouseEvent | TouchEvent): e is TouchEvent {
  583. return window.TouchEvent && e instanceof window.TouchEvent;
  584. }