resize.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269
  1. <script lang="ts" setup>
  2. /**
  3. * This components is refactored from vue-drag-resize: https://github.com/kirillmurashov/vue-drag-resize
  4. */
  5. import {
  6. computed,
  7. getCurrentInstance,
  8. nextTick,
  9. onBeforeUnmount,
  10. onMounted,
  11. ref,
  12. toRefs,
  13. watch,
  14. } from 'vue';
  15. const props = defineProps({
  16. stickSize: {
  17. type: Number,
  18. default: 8,
  19. },
  20. parentScaleX: {
  21. type: Number,
  22. default: 1,
  23. },
  24. parentScaleY: {
  25. type: Number,
  26. default: 1,
  27. },
  28. isActive: {
  29. type: Boolean,
  30. default: false,
  31. },
  32. preventActiveBehavior: {
  33. type: Boolean,
  34. default: false,
  35. },
  36. isDraggable: {
  37. type: Boolean,
  38. default: true,
  39. },
  40. isResizable: {
  41. type: Boolean,
  42. default: true,
  43. },
  44. aspectRatio: {
  45. type: Boolean,
  46. default: false,
  47. },
  48. parentLimitation: {
  49. type: Boolean,
  50. default: false,
  51. },
  52. snapToGrid: {
  53. type: Boolean,
  54. default: false,
  55. },
  56. gridX: {
  57. type: Number,
  58. default: 50,
  59. validator(val: number) {
  60. return val >= 0;
  61. },
  62. },
  63. gridY: {
  64. type: Number,
  65. default: 50,
  66. validator(val: number) {
  67. return val >= 0;
  68. },
  69. },
  70. parentW: {
  71. type: Number,
  72. default: 0,
  73. validator(val: number) {
  74. return val >= 0;
  75. },
  76. },
  77. parentH: {
  78. type: Number,
  79. default: 0,
  80. validator(val: number) {
  81. return val >= 0;
  82. },
  83. },
  84. w: {
  85. type: [String, Number],
  86. default: 200,
  87. validator(val: number) {
  88. return typeof val === 'string' ? val === 'auto' : val >= 0;
  89. },
  90. },
  91. h: {
  92. type: [String, Number],
  93. default: 200,
  94. validator(val: number) {
  95. return typeof val === 'string' ? val === 'auto' : val >= 0;
  96. },
  97. },
  98. minw: {
  99. type: Number,
  100. default: 50,
  101. validator(val: number) {
  102. return val >= 0;
  103. },
  104. },
  105. minh: {
  106. type: Number,
  107. default: 50,
  108. validator(val: number) {
  109. return val >= 0;
  110. },
  111. },
  112. x: {
  113. type: Number,
  114. default: 0,
  115. validator(val: number) {
  116. return typeof val === 'number';
  117. },
  118. },
  119. y: {
  120. type: Number,
  121. default: 0,
  122. validator(val: number) {
  123. return typeof val === 'number';
  124. },
  125. },
  126. z: {
  127. type: [String, Number],
  128. default: 'auto',
  129. validator(val: number) {
  130. return typeof val === 'string' ? val === 'auto' : val >= 0;
  131. },
  132. },
  133. dragHandle: {
  134. type: String,
  135. default: null,
  136. },
  137. dragCancel: {
  138. type: String,
  139. default: null,
  140. },
  141. sticks: {
  142. type: Array<'bl' | 'bm' | 'br' | 'ml' | 'mr' | 'tl' | 'tm' | 'tr'>,
  143. default() {
  144. return ['tl', 'tm', 'tr', 'mr', 'br', 'bm', 'bl', 'ml'];
  145. },
  146. },
  147. axis: {
  148. type: String,
  149. default: 'both',
  150. validator(val: string) {
  151. return ['both', 'none', 'x', 'y'].includes(val);
  152. },
  153. },
  154. contentClass: {
  155. type: String,
  156. required: false,
  157. default: '',
  158. },
  159. });
  160. const emit = defineEmits([
  161. 'clicked',
  162. 'dragging',
  163. 'dragstop',
  164. 'resizing',
  165. 'resizestop',
  166. 'activated',
  167. 'deactivated',
  168. ]);
  169. const styleMapping = {
  170. y: {
  171. t: 'top',
  172. m: 'marginTop',
  173. b: 'bottom',
  174. },
  175. x: {
  176. l: 'left',
  177. m: 'marginLeft',
  178. r: 'right',
  179. },
  180. };
  181. function addEvents(events: Map<string, (...args: any[]) => void>) {
  182. events.forEach((cb, eventName) => {
  183. document.documentElement.addEventListener(eventName, cb);
  184. });
  185. }
  186. function removeEvents(events: Map<string, (...args: any[]) => void>) {
  187. events.forEach((cb, eventName) => {
  188. document.documentElement.removeEventListener(eventName, cb);
  189. });
  190. }
  191. const {
  192. stickSize,
  193. parentScaleX,
  194. parentScaleY,
  195. isActive,
  196. preventActiveBehavior,
  197. isDraggable,
  198. isResizable,
  199. aspectRatio,
  200. parentLimitation,
  201. snapToGrid,
  202. gridX,
  203. gridY,
  204. parentW,
  205. parentH,
  206. w,
  207. h,
  208. minw,
  209. minh,
  210. x,
  211. y,
  212. z,
  213. dragHandle,
  214. dragCancel,
  215. sticks,
  216. axis,
  217. contentClass,
  218. } = toRefs(props);
  219. // states
  220. const active = ref(false);
  221. const zIndex = ref<null | number>(null);
  222. const parentWidth = ref<null | number>(null);
  223. const parentHeight = ref<null | number>(null);
  224. const left = ref<null | number>(null);
  225. const top = ref<null | number>(null);
  226. const right = ref<null | number>(null);
  227. const bottom = ref<null | number>(null);
  228. const aspectFactor = ref<null | number>(null);
  229. // state end
  230. const stickDrag = ref(false);
  231. const bodyDrag = ref(false);
  232. const dimensionsBeforeMove = ref({
  233. pointerX: 0,
  234. pointerY: 0,
  235. x: 0,
  236. y: 0,
  237. w: 0,
  238. h: 0,
  239. top: 0,
  240. right: 0,
  241. bottom: 0,
  242. left: 0,
  243. width: 0,
  244. height: 0,
  245. });
  246. const limits = ref({
  247. left: { min: null as null | number, max: null as null | number },
  248. right: { min: null as null | number, max: null as null | number },
  249. top: { min: null as null | number, max: null as null | number },
  250. bottom: { min: null as null | number, max: null as null | number },
  251. });
  252. const currentStick = ref<null | string>(null);
  253. const parentElement = ref<HTMLElement | null>(null);
  254. function getPointerPosition(
  255. ev: Partial<{
  256. pageX: number;
  257. pageY: number;
  258. touches: ArrayLike<{ pageX: number; pageY: number }>;
  259. }>,
  260. ): null | { pointerX: number; pointerY: number } {
  261. const touch = ev.touches?.[0];
  262. const pointerX = ev.pageX ?? touch?.pageX;
  263. const pointerY = ev.pageY ?? touch?.pageY;
  264. if (
  265. pointerX === null ||
  266. pointerX === undefined ||
  267. pointerY === null ||
  268. pointerY === undefined
  269. ) {
  270. return null;
  271. }
  272. return { pointerX, pointerY };
  273. }
  274. const width = computed(() => {
  275. const currentParentWidth = parentWidth.value;
  276. const currentLeft = left.value;
  277. const currentRight = right.value;
  278. if (
  279. currentParentWidth === null ||
  280. currentLeft === null ||
  281. currentRight === null
  282. ) {
  283. return 0;
  284. }
  285. return currentParentWidth - currentLeft - currentRight;
  286. });
  287. const height = computed(() => {
  288. const currentParentHeight = parentHeight.value;
  289. const currentTop = top.value;
  290. const currentBottom = bottom.value;
  291. if (
  292. currentParentHeight === null ||
  293. currentTop === null ||
  294. currentBottom === null
  295. ) {
  296. return 0;
  297. }
  298. return currentParentHeight - currentTop - currentBottom;
  299. });
  300. const rect = computed(() => ({
  301. left: Math.round(left.value ?? 0),
  302. top: Math.round(top.value ?? 0),
  303. width: Math.round(width.value),
  304. height: Math.round(height.value),
  305. }));
  306. const saveDimensionsBeforeMove = ({
  307. pointerX,
  308. pointerY,
  309. }: {
  310. pointerX: number;
  311. pointerY: number;
  312. }) => {
  313. dimensionsBeforeMove.value.pointerX = pointerX;
  314. dimensionsBeforeMove.value.pointerY = pointerY;
  315. dimensionsBeforeMove.value.left = left.value as number;
  316. dimensionsBeforeMove.value.right = right.value as number;
  317. dimensionsBeforeMove.value.top = top.value as number;
  318. dimensionsBeforeMove.value.bottom = bottom.value as number;
  319. dimensionsBeforeMove.value.width = width.value as number;
  320. dimensionsBeforeMove.value.height = height.value as number;
  321. aspectFactor.value = width.value / height.value;
  322. };
  323. const sideCorrectionByLimit = (
  324. limit: { max: number; min: number },
  325. current: number,
  326. ) => {
  327. let value = current;
  328. if (limit.min !== null && current < limit.min) {
  329. value = limit.min;
  330. } else if (limit.max !== null && limit.max < current) {
  331. value = limit.max;
  332. }
  333. return value;
  334. };
  335. const rectCorrectionByLimit = (rect: {
  336. newBottom: number;
  337. newLeft: number;
  338. newRight: number;
  339. newTop: number;
  340. }) => {
  341. // const { limits } = this;
  342. let { newRight, newLeft, newBottom, newTop } = rect;
  343. type RectRange = {
  344. max: number;
  345. min: number;
  346. };
  347. newLeft = sideCorrectionByLimit(limits.value.left as RectRange, newLeft);
  348. newRight = sideCorrectionByLimit(limits.value.right as RectRange, newRight);
  349. newTop = sideCorrectionByLimit(limits.value.top as RectRange, newTop);
  350. newBottom = sideCorrectionByLimit(
  351. limits.value.bottom as RectRange,
  352. newBottom,
  353. );
  354. return {
  355. newLeft,
  356. newRight,
  357. newTop,
  358. newBottom,
  359. };
  360. };
  361. const rectCorrectionByAspectRatio = (rect: {
  362. newBottom: number;
  363. newLeft: number;
  364. newRight: number;
  365. newTop: number;
  366. }) => {
  367. let { newLeft, newRight, newTop, newBottom } = rect;
  368. const currentParentWidth = parentWidth.value;
  369. const currentParentHeight = parentHeight.value;
  370. const stick = currentStick.value;
  371. const factor = aspectFactor.value;
  372. if (
  373. currentParentWidth === null ||
  374. currentParentHeight === null ||
  375. !stick ||
  376. factor === null
  377. ) {
  378. return { newLeft, newRight, newTop, newBottom };
  379. }
  380. let newWidth = currentParentWidth - newLeft - newRight;
  381. let newHeight = currentParentHeight - newTop - newBottom;
  382. if (stick[1] === 'm') {
  383. const deltaHeight = newHeight - dimensionsBeforeMove.value.height;
  384. newLeft -= (deltaHeight * factor) / 2;
  385. newRight -= (deltaHeight * factor) / 2;
  386. } else if (stick[0] === 'm') {
  387. const deltaWidth = newWidth - dimensionsBeforeMove.value.width;
  388. newTop -= deltaWidth / factor / 2;
  389. newBottom -= deltaWidth / factor / 2;
  390. } else if (newWidth / newHeight > factor) {
  391. newWidth = factor * newHeight;
  392. if (stick[1] === 'l') {
  393. newLeft = currentParentWidth - newRight - newWidth;
  394. } else {
  395. newRight = currentParentWidth - newLeft - newWidth;
  396. }
  397. } else {
  398. newHeight = newWidth / factor;
  399. if (stick[0] === 't') {
  400. newTop = currentParentHeight - newBottom - newHeight;
  401. } else {
  402. newBottom = currentParentHeight - newTop - newHeight;
  403. }
  404. }
  405. return { newLeft, newRight, newTop, newBottom };
  406. };
  407. const stickMove = (delta: { x: number; y: number }) => {
  408. const stick = currentStick.value;
  409. if (!stick) {
  410. return;
  411. }
  412. let newTop = dimensionsBeforeMove.value.top;
  413. let newBottom = dimensionsBeforeMove.value.bottom;
  414. let newLeft = dimensionsBeforeMove.value.left;
  415. let newRight = dimensionsBeforeMove.value.right;
  416. switch (stick[0]) {
  417. case 'b': {
  418. newBottom = dimensionsBeforeMove.value.bottom + delta.y;
  419. if (snapToGrid.value) {
  420. newBottom =
  421. (parentHeight.value as number) -
  422. Math.round(
  423. ((parentHeight.value as number) - newBottom) / gridY.value,
  424. ) *
  425. gridY.value;
  426. }
  427. break;
  428. }
  429. case 't': {
  430. newTop = dimensionsBeforeMove.value.top - delta.y;
  431. if (snapToGrid.value) {
  432. newTop = Math.round(newTop / gridY.value) * gridY.value;
  433. }
  434. break;
  435. }
  436. default: {
  437. break;
  438. }
  439. }
  440. switch (stick[1]) {
  441. case 'l': {
  442. newLeft = dimensionsBeforeMove.value.left - delta.x;
  443. if (snapToGrid.value) {
  444. newLeft = Math.round(newLeft / gridX.value) * gridX.value;
  445. }
  446. break;
  447. }
  448. case 'r': {
  449. newRight = dimensionsBeforeMove.value.right + delta.x;
  450. if (snapToGrid.value) {
  451. newRight =
  452. (parentWidth.value as number) -
  453. Math.round(((parentWidth.value as number) - newRight) / gridX.value) *
  454. gridX.value;
  455. }
  456. break;
  457. }
  458. default: {
  459. break;
  460. }
  461. }
  462. ({ newLeft, newRight, newTop, newBottom } = rectCorrectionByLimit({
  463. newLeft,
  464. newRight,
  465. newTop,
  466. newBottom,
  467. }));
  468. if (aspectRatio.value) {
  469. ({ newLeft, newRight, newTop, newBottom } = rectCorrectionByAspectRatio({
  470. newLeft,
  471. newRight,
  472. newTop,
  473. newBottom,
  474. }));
  475. }
  476. left.value = newLeft;
  477. right.value = newRight;
  478. top.value = newTop;
  479. bottom.value = newBottom;
  480. emit('resizing', rect.value);
  481. };
  482. const stickUp = () => {
  483. stickDrag.value = false;
  484. // dimensionsBeforeMove.value = {
  485. // pointerX: 0,
  486. // pointerY: 0,
  487. // x: 0,
  488. // y: 0,
  489. // w: 0,
  490. // h: 0,
  491. // };
  492. Object.assign(dimensionsBeforeMove.value, {
  493. pointerX: 0,
  494. pointerY: 0,
  495. x: 0,
  496. y: 0,
  497. w: 0,
  498. h: 0,
  499. });
  500. limits.value = {
  501. left: { min: null, max: null },
  502. right: { min: null, max: null },
  503. top: { min: null, max: null },
  504. bottom: { min: null, max: null },
  505. };
  506. emit('resizing', rect.value);
  507. emit('resizestop', rect.value);
  508. };
  509. const calcDragLimitation = () => {
  510. return {
  511. left: { min: 0, max: (parentWidth.value as number) - width.value },
  512. right: { min: 0, max: (parentWidth.value as number) - width.value },
  513. top: { min: 0, max: (parentHeight.value as number) - height.value },
  514. bottom: { min: 0, max: (parentHeight.value as number) - height.value },
  515. };
  516. };
  517. const calcResizeLimits = () => {
  518. const parentLim = parentLimitation.value ? 0 : null;
  519. const currentAspectFactor = aspectFactor.value;
  520. const currentLeft = left.value;
  521. const currentRight = right.value;
  522. const currentTop = top.value;
  523. const currentBottom = bottom.value;
  524. const stick = currentStick.value;
  525. if (
  526. currentLeft === null ||
  527. currentRight === null ||
  528. currentTop === null ||
  529. currentBottom === null
  530. ) {
  531. return {
  532. left: { min: parentLim, max: parentLim },
  533. right: { min: parentLim, max: parentLim },
  534. top: { min: parentLim, max: parentLim },
  535. bottom: { min: parentLim, max: parentLim },
  536. };
  537. }
  538. let minWidth = minw.value;
  539. let minHeight = minh.value;
  540. if (aspectRatio.value && currentAspectFactor) {
  541. if (minWidth / minHeight > currentAspectFactor) {
  542. minHeight = minWidth / currentAspectFactor;
  543. } else {
  544. minWidth = currentAspectFactor * minHeight;
  545. }
  546. }
  547. const limits = {
  548. left: {
  549. min: parentLim,
  550. max: currentLeft + (width.value - minWidth),
  551. },
  552. right: {
  553. min: parentLim,
  554. max: currentRight + (width.value - minWidth),
  555. },
  556. top: {
  557. min: parentLim,
  558. max: currentTop + (height.value - minHeight),
  559. },
  560. bottom: {
  561. min: parentLim,
  562. max: currentBottom + (height.value - minHeight),
  563. },
  564. };
  565. if (aspectRatio.value && currentAspectFactor) {
  566. const aspectLimits = {
  567. left: {
  568. min:
  569. currentLeft -
  570. Math.min(currentTop, currentBottom) * currentAspectFactor * 2,
  571. max:
  572. currentLeft +
  573. ((height.value - minHeight) / 2) * currentAspectFactor * 2,
  574. },
  575. right: {
  576. min:
  577. currentRight -
  578. Math.min(currentTop, currentBottom) * currentAspectFactor * 2,
  579. max:
  580. currentRight +
  581. ((height.value - minHeight) / 2) * currentAspectFactor * 2,
  582. },
  583. top: {
  584. min:
  585. currentTop -
  586. (Math.min(currentLeft, currentRight) / currentAspectFactor) * 2,
  587. max:
  588. currentTop + ((width.value - minWidth) / 2 / currentAspectFactor) * 2,
  589. },
  590. bottom: {
  591. min:
  592. currentBottom -
  593. (Math.min(currentLeft, currentRight) / currentAspectFactor) * 2,
  594. max:
  595. currentBottom +
  596. ((width.value - minWidth) / 2 / currentAspectFactor) * 2,
  597. },
  598. };
  599. if (stick?.[0] === 'm') {
  600. limits.left = {
  601. min: Math.max(
  602. limits.left.min ?? aspectLimits.left.min,
  603. aspectLimits.left.min,
  604. ),
  605. max: Math.min(limits.left.max, aspectLimits.left.max),
  606. };
  607. limits.right = {
  608. min: Math.max(
  609. limits.right.min ?? aspectLimits.right.min,
  610. aspectLimits.right.min,
  611. ),
  612. max: Math.min(limits.right.max, aspectLimits.right.max),
  613. };
  614. } else if (stick?.[1] === 'm') {
  615. limits.top = {
  616. min: Math.max(
  617. limits.top.min ?? aspectLimits.top.min,
  618. aspectLimits.top.min,
  619. ),
  620. max: Math.min(limits.top.max, aspectLimits.top.max),
  621. };
  622. limits.bottom = {
  623. min: Math.max(
  624. limits.bottom.min ?? aspectLimits.bottom.min,
  625. aspectLimits.bottom.min,
  626. ),
  627. max: Math.min(limits.bottom.max, aspectLimits.bottom.max),
  628. };
  629. }
  630. }
  631. return limits;
  632. };
  633. const positionStyle = computed(() => ({
  634. top: `${top.value ?? 0}px`,
  635. left: `${left.value ?? 0}px`,
  636. zIndex: zIndex.value ?? 'auto',
  637. }));
  638. const sizeStyle = computed(() => ({
  639. width: w.value === 'auto' ? 'auto' : `${width.value}px`,
  640. height: h.value === 'auto' ? 'auto' : `${height.value}px`,
  641. }));
  642. const stickStyles = computed(() => (stick: string) => {
  643. const stickStyle = {
  644. width: `${stickSize.value / parentScaleX.value}px`,
  645. height: `${stickSize.value / parentScaleY.value}px`,
  646. [styleMapping.y[stick[0] as 'b' | 'm' | 't'] as 'height' | 'width']:
  647. `${stickSize.value / parentScaleX.value / -2}px`,
  648. [styleMapping.x[stick[1] as 'l' | 'm' | 'r'] as 'height' | 'width']:
  649. `${stickSize.value / parentScaleX.value / -2}px`,
  650. };
  651. return stickStyle;
  652. });
  653. const bodyMove = (delta: { x: number; y: number }) => {
  654. let newTop = dimensionsBeforeMove.value.top - delta.y;
  655. let newBottom = dimensionsBeforeMove.value.bottom + delta.y;
  656. let newLeft = dimensionsBeforeMove.value.left - delta.x;
  657. let newRight = dimensionsBeforeMove.value.right + delta.x;
  658. if (snapToGrid.value) {
  659. let alignTop = true;
  660. let alignLeft = true;
  661. let diffT = newTop - Math.floor(newTop / gridY.value) * gridY.value;
  662. let diffB =
  663. (parentHeight.value as number) -
  664. newBottom -
  665. Math.floor(((parentHeight.value as number) - newBottom) / gridY.value) *
  666. gridY.value;
  667. let diffL = newLeft - Math.floor(newLeft / gridX.value) * gridX.value;
  668. let diffR =
  669. (parentWidth.value as number) -
  670. newRight -
  671. Math.floor(((parentWidth.value as number) - newRight) / gridX.value) *
  672. gridX.value;
  673. if (diffT > gridY.value / 2) {
  674. diffT -= gridY.value;
  675. }
  676. if (diffB > gridY.value / 2) {
  677. diffB -= gridY.value;
  678. }
  679. if (diffL > gridX.value / 2) {
  680. diffL -= gridX.value;
  681. }
  682. if (diffR > gridX.value / 2) {
  683. diffR -= gridX.value;
  684. }
  685. if (Math.abs(diffB) < Math.abs(diffT)) {
  686. alignTop = false;
  687. }
  688. if (Math.abs(diffR) < Math.abs(diffL)) {
  689. alignLeft = false;
  690. }
  691. newTop -= alignTop ? diffT : diffB;
  692. newBottom = (parentHeight.value as number) - height.value - newTop;
  693. newLeft -= alignLeft ? diffL : diffR;
  694. newRight = (parentWidth.value as number) - width.value - newLeft;
  695. }
  696. ({
  697. newLeft: left.value,
  698. newRight: right.value,
  699. newTop: top.value,
  700. newBottom: bottom.value,
  701. } = rectCorrectionByLimit({ newLeft, newRight, newTop, newBottom }));
  702. emit('dragging', rect.value);
  703. };
  704. const bodyUp = () => {
  705. bodyDrag.value = false;
  706. emit('dragging', rect.value);
  707. emit('dragstop', rect.value);
  708. // dimensionsBeforeMove.value = { pointerX: 0, pointerY: 0, x: 0, y: 0, w: 0, h: 0 };
  709. Object.assign(dimensionsBeforeMove.value, {
  710. pointerX: 0,
  711. pointerY: 0,
  712. x: 0,
  713. y: 0,
  714. w: 0,
  715. h: 0,
  716. });
  717. limits.value = {
  718. left: { min: null, max: null },
  719. right: { min: null, max: null },
  720. top: { min: null, max: null },
  721. bottom: { min: null, max: null },
  722. };
  723. };
  724. const stickDown = (
  725. stick: string,
  726. ev: { pageX: any; pageY: any; touches?: any },
  727. force = false,
  728. ) => {
  729. if ((!isResizable.value || !active.value) && !force) {
  730. return;
  731. }
  732. const pointerPosition = getPointerPosition(ev);
  733. if (!pointerPosition) {
  734. return;
  735. }
  736. stickDrag.value = true;
  737. saveDimensionsBeforeMove(pointerPosition);
  738. currentStick.value = stick;
  739. limits.value = calcResizeLimits();
  740. };
  741. const move = (ev: MouseEvent & TouchEvent) => {
  742. if (!stickDrag.value && !bodyDrag.value) {
  743. return;
  744. }
  745. ev.stopPropagation();
  746. const pointerPosition = getPointerPosition(ev);
  747. if (!pointerPosition) {
  748. return;
  749. }
  750. const delta = {
  751. x:
  752. (dimensionsBeforeMove.value.pointerX - pointerPosition.pointerX) /
  753. parentScaleX.value,
  754. y:
  755. (dimensionsBeforeMove.value.pointerY - pointerPosition.pointerY) /
  756. parentScaleY.value,
  757. };
  758. if (stickDrag.value) {
  759. stickMove(delta);
  760. }
  761. if (bodyDrag.value) {
  762. switch (axis.value) {
  763. case 'none': {
  764. return;
  765. }
  766. case 'x': {
  767. delta.y = 0;
  768. break;
  769. }
  770. case 'y': {
  771. delta.x = 0;
  772. break;
  773. }
  774. // No default
  775. }
  776. bodyMove(delta);
  777. }
  778. };
  779. const up = () => {
  780. if (stickDrag.value) {
  781. stickUp();
  782. } else if (bodyDrag.value) {
  783. bodyUp();
  784. }
  785. };
  786. const deselect = () => {
  787. if (preventActiveBehavior.value) {
  788. return;
  789. }
  790. active.value = false;
  791. };
  792. const domEvents = ref(
  793. new Map([
  794. ['mousedown', deselect],
  795. ['mouseleave', up],
  796. ['mousemove', move],
  797. ['mouseup', up],
  798. ['touchcancel', up],
  799. ['touchend', up],
  800. ['touchmove', move],
  801. ['touchstart', up],
  802. ]),
  803. );
  804. const container = ref<HTMLDivElement>();
  805. onMounted(() => {
  806. const currentInstance = getCurrentInstance();
  807. const $el = currentInstance?.vnode.el as HTMLElement;
  808. parentElement.value = $el?.parentNode as HTMLElement;
  809. parentWidth.value = parentW.value ?? parentElement.value?.clientWidth;
  810. parentHeight.value = parentH.value ?? parentElement.value?.clientHeight;
  811. left.value = x.value;
  812. top.value = y.value;
  813. const containerElement = container.value;
  814. const contentWidth =
  815. w.value === 'auto'
  816. ? (containerElement?.scrollWidth ?? 0)
  817. : (w.value as number);
  818. const contentHeight =
  819. h.value === 'auto'
  820. ? (containerElement?.scrollHeight ?? 0)
  821. : (h.value as number);
  822. right.value = (parentWidth.value ?? 0) - contentWidth - (left.value ?? 0);
  823. bottom.value = (parentHeight.value ?? 0) - contentHeight - (top.value ?? 0);
  824. addEvents(domEvents.value);
  825. if (dragHandle.value) {
  826. [...($el?.querySelectorAll(dragHandle.value) || [])].forEach(
  827. (dragHandle) => {
  828. (dragHandle as HTMLElement).dataset.dragHandle = String(
  829. currentInstance?.uid,
  830. );
  831. },
  832. );
  833. }
  834. if (dragCancel.value) {
  835. [...($el?.querySelectorAll(dragCancel.value) || [])].forEach(
  836. (cancelHandle) => {
  837. (cancelHandle as HTMLElement).dataset.dragCancel = String(
  838. currentInstance?.uid,
  839. );
  840. },
  841. );
  842. }
  843. });
  844. onBeforeUnmount(() => {
  845. removeEvents(domEvents.value);
  846. });
  847. const bodyDown = (ev: MouseEvent & TouchEvent) => {
  848. const { target, button } = ev;
  849. const targetElement = target instanceof HTMLElement ? target : null;
  850. const uid = getCurrentInstance()?.uid.toString();
  851. if (!preventActiveBehavior.value) {
  852. active.value = true;
  853. }
  854. if (button && button !== 0) {
  855. return;
  856. }
  857. emit('clicked', ev);
  858. if (!active.value) {
  859. return;
  860. }
  861. if (
  862. dragHandle.value &&
  863. targetElement &&
  864. targetElement.dataset.dragHandle !== uid
  865. ) {
  866. return;
  867. }
  868. if (dragCancel.value && targetElement?.dataset.dragCancel === uid) {
  869. return;
  870. }
  871. if (ev.stopPropagation !== undefined) {
  872. ev.stopPropagation();
  873. }
  874. if (ev.preventDefault !== undefined) {
  875. ev.preventDefault();
  876. }
  877. const pointerPosition = getPointerPosition(ev);
  878. if (!pointerPosition) {
  879. return;
  880. }
  881. if (isDraggable.value) {
  882. bodyDrag.value = true;
  883. }
  884. saveDimensionsBeforeMove(pointerPosition);
  885. if (parentLimitation.value) {
  886. limits.value = calcDragLimitation();
  887. }
  888. };
  889. watch(
  890. () => active.value,
  891. (isActive) => {
  892. if (isActive) {
  893. emit('activated');
  894. } else {
  895. emit('deactivated');
  896. }
  897. },
  898. );
  899. watch(
  900. () => isActive.value,
  901. (val) => {
  902. active.value = val;
  903. },
  904. { immediate: true },
  905. );
  906. watch(
  907. () => z.value,
  908. (val) => {
  909. if (typeof val === 'number' && val >= 0) {
  910. zIndex.value = val;
  911. } else if (val === 'auto') {
  912. zIndex.value = null;
  913. }
  914. },
  915. { immediate: true },
  916. );
  917. watch(
  918. () => x.value,
  919. (newVal, oldVal) => {
  920. const currentLeft = left.value;
  921. const currentTop = top.value;
  922. if (
  923. stickDrag.value ||
  924. bodyDrag.value ||
  925. currentLeft === null ||
  926. currentTop === null ||
  927. newVal === currentLeft
  928. ) {
  929. return;
  930. }
  931. const delta = oldVal - newVal;
  932. bodyDown({ pageX: currentLeft, pageY: currentTop } as MouseEvent &
  933. TouchEvent);
  934. bodyMove({ x: delta, y: 0 });
  935. nextTick(() => {
  936. bodyUp();
  937. });
  938. },
  939. );
  940. watch(
  941. () => y.value,
  942. (newVal, oldVal) => {
  943. const currentLeft = left.value;
  944. const currentTop = top.value;
  945. if (
  946. stickDrag.value ||
  947. bodyDrag.value ||
  948. currentLeft === null ||
  949. currentTop === null ||
  950. newVal === currentTop
  951. ) {
  952. return;
  953. }
  954. const delta = oldVal - newVal;
  955. bodyDown({ pageX: currentLeft, pageY: currentTop } as MouseEvent &
  956. TouchEvent);
  957. bodyMove({ x: 0, y: delta });
  958. nextTick(() => {
  959. bodyUp();
  960. });
  961. },
  962. );
  963. watch(
  964. () => w.value,
  965. (newVal, oldVal) => {
  966. const currentRight = right.value;
  967. const currentTop = top.value;
  968. if (stickDrag.value || bodyDrag.value || newVal === width.value) {
  969. return;
  970. }
  971. if (currentRight === null || currentTop === null) {
  972. return;
  973. }
  974. const stick = 'mr';
  975. const delta = (oldVal as number) - (newVal as number);
  976. stickDown(
  977. stick,
  978. { pageX: currentRight, pageY: currentTop + height.value / 2 },
  979. true,
  980. );
  981. stickMove({ x: delta, y: 0 });
  982. nextTick(() => {
  983. stickUp();
  984. });
  985. },
  986. );
  987. watch(
  988. () => h.value,
  989. (newVal, oldVal) => {
  990. const currentLeft = left.value;
  991. const currentBottom = bottom.value;
  992. if (stickDrag.value || bodyDrag.value || newVal === height.value) {
  993. return;
  994. }
  995. if (currentLeft === null || currentBottom === null) {
  996. return;
  997. }
  998. const stick = 'bm';
  999. const delta = (oldVal as number) - (newVal as number);
  1000. stickDown(
  1001. stick,
  1002. { pageX: currentLeft + width.value / 2, pageY: currentBottom },
  1003. true,
  1004. );
  1005. stickMove({ x: 0, y: delta });
  1006. nextTick(() => {
  1007. stickUp();
  1008. });
  1009. },
  1010. );
  1011. watch(
  1012. () => parentW.value,
  1013. (val) => {
  1014. right.value = val - width.value - (left.value ?? 0);
  1015. parentWidth.value = val;
  1016. },
  1017. );
  1018. watch(
  1019. () => parentH.value,
  1020. (val) => {
  1021. bottom.value = val - height.value - (top.value ?? 0);
  1022. parentHeight.value = val;
  1023. },
  1024. );
  1025. </script>
  1026. <template>
  1027. <div
  1028. :class="`${active || isActive ? 'active' : 'inactive'} ${contentClass ? contentClass : ''}`"
  1029. :style="positionStyle"
  1030. class="resize"
  1031. @mousedown="bodyDown($event as TouchEvent & MouseEvent)"
  1032. @touchend="up"
  1033. @touchstart="bodyDown($event as TouchEvent & MouseEvent)"
  1034. >
  1035. <div ref="container" :style="sizeStyle" class="content-container">
  1036. <slot></slot>
  1037. </div>
  1038. <div
  1039. v-for="(stick, index) of sticks"
  1040. :key="index"
  1041. :class="[`resize-stick-${stick}`, isResizable ? '' : 'not-resizable']"
  1042. :style="stickStyles(stick)"
  1043. class="resize-stick"
  1044. @mousedown.stop.prevent="
  1045. stickDown(stick, $event as TouchEvent & MouseEvent)
  1046. "
  1047. @touchstart.stop.prevent="
  1048. stickDown(stick, $event as TouchEvent & MouseEvent)
  1049. "
  1050. ></div>
  1051. </div>
  1052. </template>
  1053. <style lang="css" scoped>
  1054. .resize {
  1055. position: absolute;
  1056. box-sizing: border-box;
  1057. }
  1058. .resize.active::before {
  1059. position: absolute;
  1060. top: 0;
  1061. left: 0;
  1062. box-sizing: border-box;
  1063. width: 100%;
  1064. height: 100%;
  1065. outline: 1px dashed #d6d6d6;
  1066. content: '';
  1067. }
  1068. .resize-stick {
  1069. position: absolute;
  1070. box-sizing: border-box;
  1071. font-size: 1px;
  1072. background: #fff;
  1073. border: 1px solid #6c6c6c;
  1074. box-shadow: 0 0 2px #bbb;
  1075. }
  1076. .inactive .resize-stick {
  1077. display: none;
  1078. }
  1079. .resize-stick-tl,
  1080. .resize-stick-br {
  1081. cursor: nwse-resize;
  1082. }
  1083. .resize-stick-tm,
  1084. .resize-stick-bm {
  1085. left: 50%;
  1086. cursor: ns-resize;
  1087. }
  1088. .resize-stick-tr,
  1089. .resize-stick-bl {
  1090. cursor: nesw-resize;
  1091. }
  1092. .resize-stick-ml,
  1093. .resize-stick-mr {
  1094. top: 50%;
  1095. cursor: ew-resize;
  1096. }
  1097. .resize-stick.not-resizable {
  1098. display: none;
  1099. }
  1100. .content-container {
  1101. position: relative;
  1102. display: block;
  1103. }
  1104. </style>