|
@@ -0,0 +1,1122 @@
|
|
|
+<script lang="ts" setup>
|
|
|
+/**
|
|
|
+ * This components is refactored from vue-drag-resize: https://github.com/kirillmurashov/vue-drag-resize
|
|
|
+ */
|
|
|
+
|
|
|
+import {
|
|
|
+ computed,
|
|
|
+ getCurrentInstance,
|
|
|
+ nextTick,
|
|
|
+ onBeforeUnmount,
|
|
|
+ onMounted,
|
|
|
+ ref,
|
|
|
+ toRefs,
|
|
|
+ watch,
|
|
|
+} from 'vue';
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ stickSize: {
|
|
|
+ type: Number,
|
|
|
+ default: 8,
|
|
|
+ },
|
|
|
+ parentScaleX: {
|
|
|
+ type: Number,
|
|
|
+ default: 1,
|
|
|
+ },
|
|
|
+ parentScaleY: {
|
|
|
+ type: Number,
|
|
|
+ default: 1,
|
|
|
+ },
|
|
|
+ isActive: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ preventActiveBehavior: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ isDraggable: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true,
|
|
|
+ },
|
|
|
+ isResizable: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true,
|
|
|
+ },
|
|
|
+ aspectRatio: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ parentLimitation: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ snapToGrid: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ gridX: {
|
|
|
+ type: Number,
|
|
|
+ default: 50,
|
|
|
+ validator(val: number) {
|
|
|
+ return val >= 0;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ gridY: {
|
|
|
+ type: Number,
|
|
|
+ default: 50,
|
|
|
+ validator(val: number) {
|
|
|
+ return val >= 0;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ parentW: {
|
|
|
+ type: Number,
|
|
|
+ default: 0,
|
|
|
+ validator(val: number) {
|
|
|
+ return val >= 0;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ parentH: {
|
|
|
+ type: Number,
|
|
|
+ default: 0,
|
|
|
+ validator(val: number) {
|
|
|
+ return val >= 0;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ w: {
|
|
|
+ type: [String, Number],
|
|
|
+ default: 200,
|
|
|
+ validator(val: number) {
|
|
|
+ return typeof val === 'string' ? val === 'auto' : val >= 0;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ h: {
|
|
|
+ type: [String, Number],
|
|
|
+ default: 200,
|
|
|
+ validator(val: number) {
|
|
|
+ return typeof val === 'string' ? val === 'auto' : val >= 0;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ minw: {
|
|
|
+ type: Number,
|
|
|
+ default: 50,
|
|
|
+ validator(val: number) {
|
|
|
+ return val >= 0;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ minh: {
|
|
|
+ type: Number,
|
|
|
+ default: 50,
|
|
|
+ validator(val: number) {
|
|
|
+ return val >= 0;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ x: {
|
|
|
+ type: Number,
|
|
|
+ default: 0,
|
|
|
+ validator(val: number) {
|
|
|
+ return typeof val === 'number';
|
|
|
+ },
|
|
|
+ },
|
|
|
+ y: {
|
|
|
+ type: Number,
|
|
|
+ default: 0,
|
|
|
+ validator(val: number) {
|
|
|
+ return typeof val === 'number';
|
|
|
+ },
|
|
|
+ },
|
|
|
+ z: {
|
|
|
+ type: [String, Number],
|
|
|
+ default: 'auto',
|
|
|
+ validator(val: number) {
|
|
|
+ return typeof val === 'string' ? val === 'auto' : val >= 0;
|
|
|
+ },
|
|
|
+ },
|
|
|
+ dragHandle: {
|
|
|
+ type: String,
|
|
|
+ default: null,
|
|
|
+ },
|
|
|
+ dragCancel: {
|
|
|
+ type: String,
|
|
|
+ default: null,
|
|
|
+ },
|
|
|
+ sticks: {
|
|
|
+ type: Array<'bl' | 'bm' | 'br' | 'ml' | 'mr' | 'tl' | 'tm' | 'tr'>,
|
|
|
+ default() {
|
|
|
+ return ['tl', 'tm', 'tr', 'mr', 'br', 'bm', 'bl', 'ml'];
|
|
|
+ },
|
|
|
+ },
|
|
|
+ axis: {
|
|
|
+ type: String,
|
|
|
+ default: 'both',
|
|
|
+ validator(val: string) {
|
|
|
+ return ['both', 'none', 'x', 'y'].includes(val);
|
|
|
+ },
|
|
|
+ },
|
|
|
+ contentClass: {
|
|
|
+ type: String,
|
|
|
+ required: false,
|
|
|
+ default: '',
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+const emit = defineEmits([
|
|
|
+ 'clicked',
|
|
|
+ 'dragging',
|
|
|
+ 'dragstop',
|
|
|
+ 'resizing',
|
|
|
+ 'resizestop',
|
|
|
+ 'activated',
|
|
|
+ 'deactivated',
|
|
|
+]);
|
|
|
+
|
|
|
+const styleMapping = {
|
|
|
+ y: {
|
|
|
+ t: 'top',
|
|
|
+ m: 'marginTop',
|
|
|
+ b: 'bottom',
|
|
|
+ },
|
|
|
+ x: {
|
|
|
+ l: 'left',
|
|
|
+ m: 'marginLeft',
|
|
|
+ r: 'right',
|
|
|
+ },
|
|
|
+};
|
|
|
+
|
|
|
+function addEvents(events: Map<string, (...args: any[]) => void>) {
|
|
|
+ events.forEach((cb, eventName) => {
|
|
|
+ document.documentElement.addEventListener(eventName, cb);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function removeEvents(events: Map<string, (...args: any[]) => void>) {
|
|
|
+ events.forEach((cb, eventName) => {
|
|
|
+ document.documentElement.removeEventListener(eventName, cb);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+const {
|
|
|
+ stickSize,
|
|
|
+ parentScaleX,
|
|
|
+ parentScaleY,
|
|
|
+ isActive,
|
|
|
+ preventActiveBehavior,
|
|
|
+ isDraggable,
|
|
|
+ isResizable,
|
|
|
+ aspectRatio,
|
|
|
+ parentLimitation,
|
|
|
+ snapToGrid,
|
|
|
+ gridX,
|
|
|
+ gridY,
|
|
|
+ parentW,
|
|
|
+ parentH,
|
|
|
+ w,
|
|
|
+ h,
|
|
|
+ minw,
|
|
|
+ minh,
|
|
|
+ x,
|
|
|
+ y,
|
|
|
+ z,
|
|
|
+ dragHandle,
|
|
|
+ dragCancel,
|
|
|
+ sticks,
|
|
|
+ axis,
|
|
|
+ contentClass,
|
|
|
+} = toRefs(props);
|
|
|
+
|
|
|
+// states
|
|
|
+const active = ref(false);
|
|
|
+const zIndex = ref<null | number>(null);
|
|
|
+const parentWidth = ref<null | number>(null);
|
|
|
+const parentHeight = ref<null | number>(null);
|
|
|
+const left = ref<null | number>(null);
|
|
|
+const top = ref<null | number>(null);
|
|
|
+const right = ref<null | number>(null);
|
|
|
+const bottom = ref<null | number>(null);
|
|
|
+
|
|
|
+const aspectFactor = ref<null | number>(null);
|
|
|
+
|
|
|
+// state end
|
|
|
+
|
|
|
+const stickDrag = ref(false);
|
|
|
+const bodyDrag = ref(false);
|
|
|
+const dimensionsBeforeMove = ref({
|
|
|
+ pointerX: 0,
|
|
|
+ pointerY: 0,
|
|
|
+ x: 0,
|
|
|
+ y: 0,
|
|
|
+ w: 0,
|
|
|
+ h: 0,
|
|
|
+ top: 0,
|
|
|
+ right: 0,
|
|
|
+ bottom: 0,
|
|
|
+ left: 0,
|
|
|
+ width: 0,
|
|
|
+ height: 0,
|
|
|
+});
|
|
|
+const limits = ref({
|
|
|
+ left: { min: null as null | number, max: null as null | number },
|
|
|
+ right: { min: null as null | number, max: null as null | number },
|
|
|
+ top: { min: null as null | number, max: null as null | number },
|
|
|
+ bottom: { min: null as null | number, max: null as null | number },
|
|
|
+});
|
|
|
+const currentStick = ref<null | string>(null);
|
|
|
+
|
|
|
+const parentElement = ref<HTMLElement | null>(null);
|
|
|
+
|
|
|
+const width = computed(() => parentWidth.value! - left.value! - right.value!);
|
|
|
+
|
|
|
+const height = computed(() => parentHeight.value! - top.value! - bottom.value!);
|
|
|
+
|
|
|
+const rect = computed(() => ({
|
|
|
+ left: Math.round(left.value!),
|
|
|
+ top: Math.round(top.value!),
|
|
|
+ width: Math.round(width.value),
|
|
|
+ height: Math.round(height.value),
|
|
|
+}));
|
|
|
+
|
|
|
+const saveDimensionsBeforeMove = ({
|
|
|
+ pointerX,
|
|
|
+ pointerY,
|
|
|
+}: {
|
|
|
+ pointerX: number;
|
|
|
+ pointerY: number;
|
|
|
+}) => {
|
|
|
+ dimensionsBeforeMove.value.pointerX = pointerX;
|
|
|
+ dimensionsBeforeMove.value.pointerY = pointerY;
|
|
|
+
|
|
|
+ dimensionsBeforeMove.value.left = left.value as number;
|
|
|
+ dimensionsBeforeMove.value.right = right.value as number;
|
|
|
+ dimensionsBeforeMove.value.top = top.value as number;
|
|
|
+ dimensionsBeforeMove.value.bottom = bottom.value as number;
|
|
|
+
|
|
|
+ dimensionsBeforeMove.value.width = width.value as number;
|
|
|
+ dimensionsBeforeMove.value.height = height.value as number;
|
|
|
+
|
|
|
+ aspectFactor.value = width.value / height.value;
|
|
|
+};
|
|
|
+
|
|
|
+const sideCorrectionByLimit = (
|
|
|
+ limit: { max: number; min: number },
|
|
|
+ current: number,
|
|
|
+) => {
|
|
|
+ let value = current;
|
|
|
+
|
|
|
+ if (limit.min !== null && current < limit.min) {
|
|
|
+ value = limit.min;
|
|
|
+ } else if (limit.max !== null && limit.max < current) {
|
|
|
+ value = limit.max;
|
|
|
+ }
|
|
|
+
|
|
|
+ return value;
|
|
|
+};
|
|
|
+
|
|
|
+const rectCorrectionByLimit = (rect: {
|
|
|
+ newBottom: number;
|
|
|
+ newLeft: number;
|
|
|
+ newRight: number;
|
|
|
+ newTop: number;
|
|
|
+}) => {
|
|
|
+ // const { limits } = this;
|
|
|
+ let { newRight, newLeft, newBottom, newTop } = rect;
|
|
|
+
|
|
|
+ type RectRange = {
|
|
|
+ max: number;
|
|
|
+ min: number;
|
|
|
+ };
|
|
|
+
|
|
|
+ newLeft = sideCorrectionByLimit(limits.value.left as RectRange, newLeft);
|
|
|
+ newRight = sideCorrectionByLimit(limits.value.right as RectRange, newRight);
|
|
|
+ newTop = sideCorrectionByLimit(limits.value.top as RectRange, newTop);
|
|
|
+ newBottom = sideCorrectionByLimit(
|
|
|
+ limits.value.bottom as RectRange,
|
|
|
+ newBottom,
|
|
|
+ );
|
|
|
+
|
|
|
+ return {
|
|
|
+ newLeft,
|
|
|
+ newRight,
|
|
|
+ newTop,
|
|
|
+ newBottom,
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+const rectCorrectionByAspectRatio = (rect: {
|
|
|
+ newBottom: number;
|
|
|
+ newLeft: number;
|
|
|
+ newRight: number;
|
|
|
+ newTop: number;
|
|
|
+}) => {
|
|
|
+ let { newLeft, newRight, newTop, newBottom } = rect;
|
|
|
+ // const { parentWidth, parentHeight, currentStick, aspectFactor, dimensionsBeforeMove } = this;
|
|
|
+
|
|
|
+ let newWidth = parentWidth.value! - newLeft - newRight;
|
|
|
+ let newHeight = parentHeight.value! - newTop - newBottom;
|
|
|
+
|
|
|
+ if (currentStick.value![1] === 'm') {
|
|
|
+ const deltaHeight = newHeight - dimensionsBeforeMove.value.height;
|
|
|
+
|
|
|
+ newLeft -= (deltaHeight * aspectFactor.value!) / 2;
|
|
|
+ newRight -= (deltaHeight * aspectFactor.value!) / 2;
|
|
|
+ } else if (currentStick.value![0] === 'm') {
|
|
|
+ const deltaWidth = newWidth - dimensionsBeforeMove.value.width;
|
|
|
+
|
|
|
+ newTop -= deltaWidth / aspectFactor.value! / 2;
|
|
|
+ newBottom -= deltaWidth / aspectFactor.value! / 2;
|
|
|
+ } else if (newWidth / newHeight > aspectFactor.value!) {
|
|
|
+ newWidth = aspectFactor.value! * newHeight;
|
|
|
+
|
|
|
+ if (currentStick.value![1] === 'l') {
|
|
|
+ newLeft = parentWidth.value! - newRight - newWidth;
|
|
|
+ } else {
|
|
|
+ newRight = parentWidth.value! - newLeft - newWidth;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ newHeight = newWidth / aspectFactor.value!;
|
|
|
+
|
|
|
+ if (currentStick.value![0] === 't') {
|
|
|
+ newTop = parentHeight.value! - newBottom - newHeight;
|
|
|
+ } else {
|
|
|
+ newBottom = parentHeight.value! - newTop - newHeight;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return { newLeft, newRight, newTop, newBottom };
|
|
|
+};
|
|
|
+
|
|
|
+const stickMove = (delta: { x: number; y: number }) => {
|
|
|
+ let newTop = dimensionsBeforeMove.value.top;
|
|
|
+ let newBottom = dimensionsBeforeMove.value.bottom;
|
|
|
+ let newLeft = dimensionsBeforeMove.value.left;
|
|
|
+ let newRight = dimensionsBeforeMove.value.right;
|
|
|
+ switch (currentStick.value![0]) {
|
|
|
+ case 'b': {
|
|
|
+ newBottom = dimensionsBeforeMove.value.bottom + delta.y;
|
|
|
+
|
|
|
+ if (snapToGrid.value) {
|
|
|
+ newBottom =
|
|
|
+ (parentHeight.value as number) -
|
|
|
+ Math.round(
|
|
|
+ ((parentHeight.value as number) - newBottom) / gridY.value,
|
|
|
+ ) *
|
|
|
+ gridY.value;
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ case 't': {
|
|
|
+ newTop = dimensionsBeforeMove.value.top - delta.y;
|
|
|
+
|
|
|
+ if (snapToGrid.value) {
|
|
|
+ newTop = Math.round(newTop / gridY.value) * gridY.value;
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ default: {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ switch (currentStick.value![1]) {
|
|
|
+ case 'l': {
|
|
|
+ newLeft = dimensionsBeforeMove.value.left - delta.x;
|
|
|
+
|
|
|
+ if (snapToGrid.value) {
|
|
|
+ newLeft = Math.round(newLeft / gridX.value) * gridX.value;
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ case 'r': {
|
|
|
+ newRight = dimensionsBeforeMove.value.right + delta.x;
|
|
|
+
|
|
|
+ if (snapToGrid.value) {
|
|
|
+ newRight =
|
|
|
+ (parentWidth.value as number) -
|
|
|
+ Math.round(((parentWidth.value as number) - newRight) / gridX.value) *
|
|
|
+ gridX.value;
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ default: {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ ({ newLeft, newRight, newTop, newBottom } = rectCorrectionByLimit({
|
|
|
+ newLeft,
|
|
|
+ newRight,
|
|
|
+ newTop,
|
|
|
+ newBottom,
|
|
|
+ }));
|
|
|
+
|
|
|
+ if (aspectRatio.value) {
|
|
|
+ ({ newLeft, newRight, newTop, newBottom } = rectCorrectionByAspectRatio({
|
|
|
+ newLeft,
|
|
|
+ newRight,
|
|
|
+ newTop,
|
|
|
+ newBottom,
|
|
|
+ }));
|
|
|
+ }
|
|
|
+
|
|
|
+ left.value = newLeft;
|
|
|
+ right.value = newRight;
|
|
|
+ top.value = newTop;
|
|
|
+ bottom.value = newBottom;
|
|
|
+
|
|
|
+ emit('resizing', rect.value);
|
|
|
+};
|
|
|
+
|
|
|
+const stickUp = () => {
|
|
|
+ stickDrag.value = false;
|
|
|
+ // dimensionsBeforeMove.value = {
|
|
|
+ // pointerX: 0,
|
|
|
+ // pointerY: 0,
|
|
|
+ // x: 0,
|
|
|
+ // y: 0,
|
|
|
+ // w: 0,
|
|
|
+ // h: 0,
|
|
|
+ // };
|
|
|
+
|
|
|
+ Object.assign(dimensionsBeforeMove.value, {
|
|
|
+ pointerX: 0,
|
|
|
+ pointerY: 0,
|
|
|
+ x: 0,
|
|
|
+ y: 0,
|
|
|
+ w: 0,
|
|
|
+ h: 0,
|
|
|
+ });
|
|
|
+
|
|
|
+ limits.value = {
|
|
|
+ left: { min: null, max: null },
|
|
|
+ right: { min: null, max: null },
|
|
|
+ top: { min: null, max: null },
|
|
|
+ bottom: { min: null, max: null },
|
|
|
+ };
|
|
|
+
|
|
|
+ emit('resizing', rect.value);
|
|
|
+ emit('resizestop', rect.value);
|
|
|
+};
|
|
|
+
|
|
|
+const calcDragLimitation = () => {
|
|
|
+ return {
|
|
|
+ left: { min: 0, max: (parentWidth.value as number) - width.value },
|
|
|
+ right: { min: 0, max: (parentWidth.value as number) - width.value },
|
|
|
+ top: { min: 0, max: (parentHeight.value as number) - height.value },
|
|
|
+ bottom: { min: 0, max: (parentHeight.value as number) - height.value },
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+const calcResizeLimits = () => {
|
|
|
+ // const { aspectFactor, width, height, bottom, top, left, right } = this;
|
|
|
+
|
|
|
+ const parentLim = parentLimitation.value ? 0 : null;
|
|
|
+
|
|
|
+ if (aspectRatio.value) {
|
|
|
+ if (minw.value / minh.value > (aspectFactor.value as number)) {
|
|
|
+ minh.value = minw.value / (aspectFactor.value as number);
|
|
|
+ } else {
|
|
|
+ minw.value = ((aspectFactor.value as number) * minh.value) as number;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const limits = {
|
|
|
+ left: {
|
|
|
+ min: parentLim,
|
|
|
+ max: (left.value as number) + (width.value - minw.value),
|
|
|
+ },
|
|
|
+ right: {
|
|
|
+ min: parentLim,
|
|
|
+ max: (right.value as number) + (width.value - minw.value),
|
|
|
+ },
|
|
|
+ top: {
|
|
|
+ min: parentLim,
|
|
|
+ max: (top.value as number) + (height.value - minh.value),
|
|
|
+ },
|
|
|
+ bottom: {
|
|
|
+ min: parentLim,
|
|
|
+ max: (bottom.value as number) + (height.value - minh.value),
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ if (aspectRatio.value) {
|
|
|
+ const aspectLimits = {
|
|
|
+ left: {
|
|
|
+ min:
|
|
|
+ left.value! -
|
|
|
+ Math.min(top.value!, bottom.value!) * aspectFactor.value! * 2,
|
|
|
+ max:
|
|
|
+ left.value! +
|
|
|
+ ((height.value - minh.value!) / 2) * aspectFactor.value! * 2,
|
|
|
+ },
|
|
|
+ right: {
|
|
|
+ min:
|
|
|
+ right.value! -
|
|
|
+ Math.min(top.value!, bottom.value!) * aspectFactor.value! * 2,
|
|
|
+ max:
|
|
|
+ right.value! +
|
|
|
+ ((height.value - minh.value!) / 2) * aspectFactor.value! * 2,
|
|
|
+ },
|
|
|
+ top: {
|
|
|
+ min:
|
|
|
+ top.value! -
|
|
|
+ (Math.min(left.value!, right.value!) / aspectFactor.value!) * 2,
|
|
|
+ max:
|
|
|
+ top.value! +
|
|
|
+ ((width.value - minw.value) / 2 / aspectFactor.value!) * 2,
|
|
|
+ },
|
|
|
+ bottom: {
|
|
|
+ min:
|
|
|
+ bottom.value! -
|
|
|
+ (Math.min(left.value!, right.value!) / aspectFactor.value!) * 2,
|
|
|
+ max:
|
|
|
+ bottom.value! +
|
|
|
+ ((width.value - minw.value) / 2 / aspectFactor.value!) * 2,
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ if (currentStick.value![0] === 'm') {
|
|
|
+ limits.left = {
|
|
|
+ min: Math.max(limits.left.min!, aspectLimits.left.min),
|
|
|
+ max: Math.min(limits.left.max, aspectLimits.left.max),
|
|
|
+ };
|
|
|
+ limits.right = {
|
|
|
+ min: Math.max(limits.right.min!, aspectLimits.right.min),
|
|
|
+ max: Math.min(limits.right.max, aspectLimits.right.max),
|
|
|
+ };
|
|
|
+ } else if (currentStick.value![1] === 'm') {
|
|
|
+ limits.top = {
|
|
|
+ min: Math.max(limits.top.min!, aspectLimits.top.min),
|
|
|
+ max: Math.min(limits.top.max, aspectLimits.top.max),
|
|
|
+ };
|
|
|
+ limits.bottom = {
|
|
|
+ min: Math.max(limits.bottom.min!, aspectLimits.bottom.min),
|
|
|
+ max: Math.min(limits.bottom.max, aspectLimits.bottom.max),
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return limits;
|
|
|
+};
|
|
|
+
|
|
|
+const positionStyle = computed(() => ({
|
|
|
+ top: `${top.value}px`,
|
|
|
+ left: `${left.value}px`,
|
|
|
+ zIndex: zIndex.value!,
|
|
|
+}));
|
|
|
+
|
|
|
+const sizeStyle = computed(() => ({
|
|
|
+ width: w.value === 'auto' ? 'auto' : `${width.value}px`,
|
|
|
+ height: h.value === 'auto' ? 'auto' : `${height.value}px`,
|
|
|
+}));
|
|
|
+
|
|
|
+const stickStyles = computed(() => (stick: string) => {
|
|
|
+ const stickStyle = {
|
|
|
+ width: `${stickSize.value / parentScaleX.value}px`,
|
|
|
+ height: `${stickSize.value / parentScaleY.value}px`,
|
|
|
+ };
|
|
|
+ stickStyle[
|
|
|
+ styleMapping.y[stick[0] as 'b' | 'm' | 't'] as 'height' | 'width'
|
|
|
+ ] = `${stickSize.value / parentScaleX.value / -2}px`;
|
|
|
+ stickStyle[
|
|
|
+ styleMapping.x[stick[1] as 'l' | 'm' | 'r'] as 'height' | 'width'
|
|
|
+ ] = `${stickSize.value / parentScaleX.value / -2}px`;
|
|
|
+ return stickStyle;
|
|
|
+});
|
|
|
+
|
|
|
+const bodyMove = (delta: { x: number; y: number }) => {
|
|
|
+ let newTop = dimensionsBeforeMove.value.top - delta.y;
|
|
|
+ let newBottom = dimensionsBeforeMove.value.bottom + delta.y;
|
|
|
+ let newLeft = dimensionsBeforeMove.value.left - delta.x;
|
|
|
+ let newRight = dimensionsBeforeMove.value.right + delta.x;
|
|
|
+
|
|
|
+ if (snapToGrid.value) {
|
|
|
+ let alignTop = true;
|
|
|
+ let alignLeft = true;
|
|
|
+
|
|
|
+ let diffT = newTop - Math.floor(newTop / gridY.value) * gridY.value;
|
|
|
+ let diffB =
|
|
|
+ (parentHeight.value as number) -
|
|
|
+ newBottom -
|
|
|
+ Math.floor(((parentHeight.value as number) - newBottom) / gridY.value) *
|
|
|
+ gridY.value;
|
|
|
+ let diffL = newLeft - Math.floor(newLeft / gridX.value) * gridX.value;
|
|
|
+ let diffR =
|
|
|
+ (parentWidth.value as number) -
|
|
|
+ newRight -
|
|
|
+ Math.floor(((parentWidth.value as number) - newRight) / gridX.value) *
|
|
|
+ gridX.value;
|
|
|
+
|
|
|
+ if (diffT > gridY.value / 2) {
|
|
|
+ diffT -= gridY.value;
|
|
|
+ }
|
|
|
+ if (diffB > gridY.value / 2) {
|
|
|
+ diffB -= gridY.value;
|
|
|
+ }
|
|
|
+ if (diffL > gridX.value / 2) {
|
|
|
+ diffL -= gridX.value;
|
|
|
+ }
|
|
|
+ if (diffR > gridX.value / 2) {
|
|
|
+ diffR -= gridX.value;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Math.abs(diffB) < Math.abs(diffT)) {
|
|
|
+ alignTop = false;
|
|
|
+ }
|
|
|
+ if (Math.abs(diffR) < Math.abs(diffL)) {
|
|
|
+ alignLeft = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ newTop -= alignTop ? diffT : diffB;
|
|
|
+ newBottom = (parentHeight.value as number) - height.value - newTop;
|
|
|
+ newLeft -= alignLeft ? diffL : diffR;
|
|
|
+ newRight = (parentWidth.value as number) - width.value - newLeft;
|
|
|
+ }
|
|
|
+
|
|
|
+ ({
|
|
|
+ newLeft: left.value,
|
|
|
+ newRight: right.value,
|
|
|
+ newTop: top.value,
|
|
|
+ newBottom: bottom.value,
|
|
|
+ } = rectCorrectionByLimit({ newLeft, newRight, newTop, newBottom }));
|
|
|
+
|
|
|
+ emit('dragging', rect.value);
|
|
|
+};
|
|
|
+
|
|
|
+const bodyUp = () => {
|
|
|
+ bodyDrag.value = false;
|
|
|
+ emit('dragging', rect.value);
|
|
|
+ emit('dragstop', rect.value);
|
|
|
+
|
|
|
+ // dimensionsBeforeMove.value = { pointerX: 0, pointerY: 0, x: 0, y: 0, w: 0, h: 0 };
|
|
|
+ Object.assign(dimensionsBeforeMove.value, {
|
|
|
+ pointerX: 0,
|
|
|
+ pointerY: 0,
|
|
|
+ x: 0,
|
|
|
+ y: 0,
|
|
|
+ w: 0,
|
|
|
+ h: 0,
|
|
|
+ });
|
|
|
+
|
|
|
+ limits.value = {
|
|
|
+ left: { min: null, max: null },
|
|
|
+ right: { min: null, max: null },
|
|
|
+ top: { min: null, max: null },
|
|
|
+ bottom: { min: null, max: null },
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+const stickDown = (
|
|
|
+ stick: string,
|
|
|
+ ev: { pageX: any; pageY: any; touches?: any },
|
|
|
+ force = false,
|
|
|
+) => {
|
|
|
+ if ((!isResizable.value || !active.value) && !force) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ stickDrag.value = true;
|
|
|
+
|
|
|
+ const pointerX = ev.pageX === undefined ? ev.touches[0].pageX : ev.pageX;
|
|
|
+ const pointerY = ev.pageY === undefined ? ev.touches[0].pageY : ev.pageY;
|
|
|
+
|
|
|
+ saveDimensionsBeforeMove({ pointerX, pointerY });
|
|
|
+
|
|
|
+ currentStick.value = stick;
|
|
|
+
|
|
|
+ limits.value = calcResizeLimits();
|
|
|
+};
|
|
|
+
|
|
|
+const move = (ev: MouseEvent & TouchEvent) => {
|
|
|
+ if (!stickDrag.value && !bodyDrag.value) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ ev.stopPropagation();
|
|
|
+
|
|
|
+ // touches 兼容性代码
|
|
|
+ const pageX = ev.pageX === undefined ? ev.touches![0]!.pageX : ev.pageX;
|
|
|
+ const pageY = ev.pageY === undefined ? ev.touches![0]!.pageY : ev.pageY;
|
|
|
+
|
|
|
+ const delta = {
|
|
|
+ x: (dimensionsBeforeMove.value.pointerX - pageX) / parentScaleX.value,
|
|
|
+ y: (dimensionsBeforeMove.value.pointerY - pageY) / parentScaleY.value,
|
|
|
+ };
|
|
|
+
|
|
|
+ if (stickDrag.value) {
|
|
|
+ stickMove(delta);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (bodyDrag.value) {
|
|
|
+ switch (axis.value) {
|
|
|
+ case 'none': {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ case 'x': {
|
|
|
+ delta.y = 0;
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case 'y': {
|
|
|
+ delta.x = 0;
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ // No default
|
|
|
+ }
|
|
|
+ bodyMove(delta);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const up = () => {
|
|
|
+ if (stickDrag.value) {
|
|
|
+ stickUp();
|
|
|
+ } else if (bodyDrag.value) {
|
|
|
+ bodyUp();
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const deselect = () => {
|
|
|
+ if (preventActiveBehavior.value) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ active.value = false;
|
|
|
+};
|
|
|
+
|
|
|
+const domEvents = ref(
|
|
|
+ new Map([
|
|
|
+ ['mousedown', deselect],
|
|
|
+ ['mouseleave', up],
|
|
|
+ ['mousemove', move],
|
|
|
+ ['mouseup', up],
|
|
|
+ ['touchcancel', up],
|
|
|
+ ['touchend', up],
|
|
|
+ ['touchmove', move],
|
|
|
+ ['touchstart', up],
|
|
|
+ ]),
|
|
|
+);
|
|
|
+
|
|
|
+const container = ref<HTMLDivElement>();
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ const currentInstance = getCurrentInstance();
|
|
|
+ const $el = currentInstance?.vnode.el as HTMLElement;
|
|
|
+
|
|
|
+ parentElement.value = $el?.parentNode as HTMLElement;
|
|
|
+ parentWidth.value = parentW.value ?? parentElement.value?.clientWidth;
|
|
|
+ parentHeight.value = parentH.value ?? parentElement.value?.clientHeight;
|
|
|
+
|
|
|
+ left.value = x.value;
|
|
|
+ top.value = y.value;
|
|
|
+ right.value = (parentWidth.value -
|
|
|
+ (w.value === 'auto' ? container.value!.scrollWidth : (w.value as number)) -
|
|
|
+ left.value) as number;
|
|
|
+ bottom.value = (parentHeight.value -
|
|
|
+ (h.value === 'auto' ? container.value!.scrollHeight : (h.value as number)) -
|
|
|
+ top.value) as number;
|
|
|
+
|
|
|
+ addEvents(domEvents.value);
|
|
|
+
|
|
|
+ if (dragHandle.value) {
|
|
|
+ [...($el?.querySelectorAll(dragHandle.value) || [])].forEach(
|
|
|
+ (dragHandle) => {
|
|
|
+ (dragHandle as HTMLElement).dataset.dragHandle = String(
|
|
|
+ currentInstance?.uid,
|
|
|
+ );
|
|
|
+ },
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (dragCancel.value) {
|
|
|
+ [...($el?.querySelectorAll(dragCancel.value) || [])].forEach(
|
|
|
+ (cancelHandle) => {
|
|
|
+ (cancelHandle as HTMLElement).dataset.dragCancel = String(
|
|
|
+ currentInstance?.uid,
|
|
|
+ );
|
|
|
+ },
|
|
|
+ );
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ removeEvents(domEvents.value);
|
|
|
+});
|
|
|
+
|
|
|
+const bodyDown = (ev: MouseEvent & TouchEvent) => {
|
|
|
+ const { target, button } = ev;
|
|
|
+
|
|
|
+ if (!preventActiveBehavior.value) {
|
|
|
+ active.value = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (button && button !== 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ emit('clicked', ev);
|
|
|
+
|
|
|
+ if (!active.value) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (
|
|
|
+ dragHandle.value &&
|
|
|
+ (target! as HTMLElement).dataset.dragHandle !==
|
|
|
+ getCurrentInstance()?.uid.toString()
|
|
|
+ ) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (
|
|
|
+ dragCancel.value &&
|
|
|
+ (target! as HTMLElement).dataset.dragCancel ===
|
|
|
+ getCurrentInstance()?.uid.toString()
|
|
|
+ ) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (ev.stopPropagation !== undefined) {
|
|
|
+ ev.stopPropagation();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (ev.preventDefault !== undefined) {
|
|
|
+ ev.preventDefault();
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isDraggable.value) {
|
|
|
+ bodyDrag.value = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ const pointerX = ev.pageX === undefined ? ev.touches[0]!.pageX : ev.pageX;
|
|
|
+ const pointerY = ev.pageY === undefined ? ev.touches[0]!.pageY : ev.pageY;
|
|
|
+
|
|
|
+ saveDimensionsBeforeMove({ pointerX, pointerY });
|
|
|
+
|
|
|
+ if (parentLimitation.value) {
|
|
|
+ limits.value = calcDragLimitation();
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => active.value,
|
|
|
+ (isActive) => {
|
|
|
+ if (isActive) {
|
|
|
+ emit('activated');
|
|
|
+ } else {
|
|
|
+ emit('deactivated');
|
|
|
+ }
|
|
|
+ },
|
|
|
+);
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => isActive.value,
|
|
|
+ (val) => {
|
|
|
+ active.value = val;
|
|
|
+ },
|
|
|
+ { immediate: true },
|
|
|
+);
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => z.value,
|
|
|
+ (val) => {
|
|
|
+ if ((val as number) >= 0 || val === 'auto') {
|
|
|
+ zIndex.value = val as number;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { immediate: true },
|
|
|
+);
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => x.value,
|
|
|
+ (newVal, oldVal) => {
|
|
|
+ if (stickDrag.value || bodyDrag.value || newVal === left.value) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const delta = oldVal - newVal;
|
|
|
+
|
|
|
+ bodyDown({ pageX: left.value!, pageY: top.value! } as MouseEvent &
|
|
|
+ TouchEvent);
|
|
|
+ bodyMove({ x: delta, y: 0 });
|
|
|
+
|
|
|
+ nextTick(() => {
|
|
|
+ bodyUp();
|
|
|
+ });
|
|
|
+ },
|
|
|
+);
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => y.value,
|
|
|
+ (newVal, oldVal) => {
|
|
|
+ if (stickDrag.value || bodyDrag.value || newVal === top.value) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const delta = oldVal - newVal;
|
|
|
+
|
|
|
+ bodyDown({ pageX: left.value, pageY: top.value } as MouseEvent &
|
|
|
+ TouchEvent);
|
|
|
+ bodyMove({ x: 0, y: delta });
|
|
|
+
|
|
|
+ nextTick(() => {
|
|
|
+ bodyUp();
|
|
|
+ });
|
|
|
+ },
|
|
|
+);
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => w.value,
|
|
|
+ (newVal, oldVal) => {
|
|
|
+ if (stickDrag.value || bodyDrag.value || newVal === width.value) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const stick = 'mr';
|
|
|
+ const delta = (oldVal as number) - (newVal as number);
|
|
|
+
|
|
|
+ stickDown(
|
|
|
+ stick,
|
|
|
+ { pageX: right.value, pageY: top.value! + height.value / 2 },
|
|
|
+ true,
|
|
|
+ );
|
|
|
+ stickMove({ x: delta, y: 0 });
|
|
|
+
|
|
|
+ nextTick(() => {
|
|
|
+ stickUp();
|
|
|
+ });
|
|
|
+ },
|
|
|
+);
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => h.value,
|
|
|
+ (newVal, oldVal) => {
|
|
|
+ if (stickDrag.value || bodyDrag.value || newVal === height.value) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const stick = 'bm';
|
|
|
+ const delta = (oldVal as number) - (newVal as number);
|
|
|
+
|
|
|
+ stickDown(
|
|
|
+ stick,
|
|
|
+ { pageX: left.value! + width.value / 2, pageY: bottom.value },
|
|
|
+ true,
|
|
|
+ );
|
|
|
+ stickMove({ x: 0, y: delta });
|
|
|
+
|
|
|
+ nextTick(() => {
|
|
|
+ stickUp();
|
|
|
+ });
|
|
|
+ },
|
|
|
+);
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => parentW.value,
|
|
|
+ (val) => {
|
|
|
+ right.value = val - width.value - left.value!;
|
|
|
+ parentWidth.value = val;
|
|
|
+ },
|
|
|
+);
|
|
|
+
|
|
|
+watch(
|
|
|
+ () => parentH.value,
|
|
|
+ (val) => {
|
|
|
+ bottom.value = val - height.value - top.value!;
|
|
|
+ parentHeight.value = val;
|
|
|
+ },
|
|
|
+);
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div
|
|
|
+ :class="`${active || isActive ? 'active' : 'inactive'} ${contentClass ? contentClass : ''}`"
|
|
|
+ :style="positionStyle"
|
|
|
+ class="resize"
|
|
|
+ @mousedown="bodyDown($event as TouchEvent & MouseEvent)"
|
|
|
+ @touchend="up"
|
|
|
+ @touchstart="bodyDown($event as TouchEvent & MouseEvent)"
|
|
|
+ >
|
|
|
+ <div ref="container" :style="sizeStyle" class="content-container">
|
|
|
+ <slot></slot>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ v-for="(stick, index) of sticks"
|
|
|
+ :key="index"
|
|
|
+ :class="[`resize-stick-${stick}`, isResizable ? '' : 'not-resizable']"
|
|
|
+ :style="stickStyles(stick)"
|
|
|
+ class="resize-stick"
|
|
|
+ @mousedown.stop.prevent="
|
|
|
+ stickDown(stick, $event as TouchEvent & MouseEvent)
|
|
|
+ "
|
|
|
+ @touchstart.stop.prevent="
|
|
|
+ stickDown(stick, $event as TouchEvent & MouseEvent)
|
|
|
+ "
|
|
|
+ ></div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style lang="css" scoped>
|
|
|
+.resize {
|
|
|
+ position: absolute;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.resize.active::before {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ left: 0;
|
|
|
+ box-sizing: border-box;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ content: '';
|
|
|
+ outline: 1px dashed #d6d6d6;
|
|
|
+}
|
|
|
+
|
|
|
+.resize-stick {
|
|
|
+ position: absolute;
|
|
|
+ box-sizing: border-box;
|
|
|
+ font-size: 1px;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #6c6c6c;
|
|
|
+ box-shadow: 0 0 2px #bbb;
|
|
|
+}
|
|
|
+
|
|
|
+.inactive .resize-stick {
|
|
|
+ display: none;
|
|
|
+}
|
|
|
+
|
|
|
+.resize-stick-tl,
|
|
|
+.resize-stick-br {
|
|
|
+ cursor: nwse-resize;
|
|
|
+}
|
|
|
+
|
|
|
+.resize-stick-tm,
|
|
|
+.resize-stick-bm {
|
|
|
+ left: 50%;
|
|
|
+ cursor: ns-resize;
|
|
|
+}
|
|
|
+
|
|
|
+.resize-stick-tr,
|
|
|
+.resize-stick-bl {
|
|
|
+ cursor: nesw-resize;
|
|
|
+}
|
|
|
+
|
|
|
+.resize-stick-ml,
|
|
|
+.resize-stick-mr {
|
|
|
+ top: 50%;
|
|
|
+ cursor: ew-resize;
|
|
|
+}
|
|
|
+
|
|
|
+.resize-stick.not-resizable {
|
|
|
+ display: none;
|
|
|
+}
|
|
|
+
|
|
|
+.content-container {
|
|
|
+ position: relative;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+</style>
|