- 画尺寸ruler
background: repeating-linear-gradient(
90deg
, #000 0px, transparent 1px, transparent 4px), repeating-linear-gradient(
90deg
, #000 0px, transparent 1px, transparent 20px), repeating-linear-gradient(#000 0px, transparent 1px, transparent 4px), repeating-linear-gradient(#000 0px, transparent 1px, transparent 20px), #e6e6e6;
background-size: 100% 10px, 100% 15px, 10px 100%, 15px 100%;
background-position: 40px 0, 40px 0, 0 40px, 0 40px;
background-repeat: no-repeat;
- 画格子
background: linear-gradient(
-90deg
, #eee 1px, transparent 1px), linear-gradient(#eee 1px, transparent 1px), #fff;
background-size: 8px 8px;
background-repeat: repeat;
background-position: 1px 1px, 4px 4px;
- 代码展示
monaco-editor
- 导出代码 ```css import JSZip from ‘jszip’; import FileSaver from ‘file-saver’; import { CodeFile } from ‘@/types/code-editor’;
export function exportZip(fileList: CodeFile[]) { // const blobs = codeStrArray.map(codeStr => new Blob([codeStr], {type: ‘text/plain’})); const blobs = fileList.map((file: CodeFile) => { const code = Object.values(file.codeMap).join(‘\n’); return { blob: new Blob([code], {type: ‘text/plain’}), name: file.name, }; }); const zip = JSZip(); blobs.forEach((file) => { zip.file(file.name, file.blob); }); zip.generateAsync({type: ‘blob’}).then(zipFile => { const fileName = ‘combined.zip’; return FileSaver.saveAs(zipFile, fileName); }); }
5. 组件拖拽和resize
```css
<template>
<div
:style="style"
:class="[
{
[classNameActive]: enabled,
[classNameDragging]: dragging,
[classNameResizing]: resizing,
[classNameDraggable]: draggable,
[classNameResizable]: resizable,
},
className,
]"
@mousedown="elementMouseDown"
@touchstart="elementTouchDown"
@click="selectCurComponent"
>
<div
v-for="(handle, index) in actualHandles"
:key="handle + index"
:class="[classNameHandle, classNameHandle + '-' + handle]"
:style="handleStyle(handle)"
@mousedown.stop.prevent="handleDown(handle, $event)"
@touchstart.stop.prevent="handleTouchDown(handle, $event)"
>
<slot :name="handle"></slot>
</div>
<slot></slot>
</div>
</template>
<script>
import { matchesSelectorToParentElements, getComputedSize, addEvent, removeEvent } from '../utils/dom';
import { computeWidth, computeHeight, restrictToBounds, snapToGrid } from '../utils/fns';
const events = {
mouse: {
start: 'mousedown',
move: 'mousemove',
stop: 'mouseup'
},
touch: {
start: 'touchstart',
move: 'touchmove',
stop: 'touchend'
}
};
// 禁止用户选取
const userSelectNone = {
userSelect: 'none',
MozUserSelect: 'none',
WebkitUserSelect: 'none',
MsUserSelect: 'none'
};
// 用户选中自动
const userSelectAuto = {
userSelect: 'auto',
MozUserSelect: 'auto',
WebkitUserSelect: 'auto',
MsUserSelect: 'auto'
};
let eventsFor = events.mouse;
export default {
replace: true,
name: 'vue-draggable-resizable',
props: {
className: {
type: String,
default: 'vdr'
},
classNameDraggable: {
type: String,
default: 'draggable'
},
classNameResizable: {
type: String,
default: 'resizable'
},
classNameDragging: {
type: String,
default: 'dragging'
},
classNameResizing: {
type: String,
default: 'resizing'
},
classNameActive: {
type: String,
default: 'active'
},
classNameHandle: {
type: String,
default: 'handle'
},
disableUserSelect: {
type: Boolean,
default: true
},
enableNativeDrag: {
type: Boolean,
default: false
},
preventDeactivation: {
type: Boolean,
default: false
},
active: {
type: Boolean,
default: false
},
draggable: {
type: Boolean,
default: true
},
resizable: {
type: Boolean,
default: true
},
// 锁定宽高比
lockAspectRatio: {
type: Boolean,
default: false
},
w: {
type: [Number, String],
default: 200,
validator: (val) => {
if (typeof val === 'number') {
return val > 0;
}
return val === 'auto';
}
},
h: {
type: [Number, String],
default: 200,
validator: (val) => {
if (typeof val === 'number') {
return val > 0;
}
return val === 'auto';
}
},
minWidth: {
type: Number,
default: 0,
validator: (val) => val >= 0
},
minHeight: {
type: Number,
default: 0,
validator: (val) => val >= 0
},
maxWidth: {
type: Number,
default: null,
validator: (val) => val >= 0
},
maxHeight: {
type: Number,
default: null,
validator: (val) => val >= 0
},
x: {
type: Number,
default: 0
},
y: {
type: Number,
default: 0
},
z: {
type: [String, Number],
default: 'auto',
validator: (val) => (typeof val === 'string' ? val === 'auto' : val >= 0)
},
handles: {
type: Array,
default: () => ['tl', 'tm', 'tr', 'mr', 'br', 'bm', 'bl', 'ml'],
validator: (val) => {
const s = new Set(['tl', 'tm', 'tr', 'mr', 'br', 'bm', 'bl', 'ml']);
return new Set(val.filter(h => s.has(h))).size === val.length;
}
},
dragHandle: {
type: String,
default: null
},
dragCancel: {
type: String,
default: null
},
axis: {
type: String,
default: 'both',
validator: (val) => ['x', 'y', 'both'].includes(val)
},
grid: {
type: Array,
default: () => [1, 1]
},
parent: {
type: [Boolean, String],
default: false
},
onDragStart: {
type: Function,
default: () => true
},
onDrag: {
type: Function,
default: () => true
},
onResizeStart: {
type: Function,
default: () => true
},
onResize: {
type: Function,
default: () => true
},
// 冲突检测
isConflictCheck: {
type: Boolean, default: false
},
// 元素对齐
snap: {
type: Boolean, default: false
},
// 当调用对齐时,用来设置组件与组件之间的对齐距离,以像素为单位
snapTolerance: {
type: Number,
default: 5,
validator: function (val) {
return typeof val === 'number';
}
},
// 缩放比例
scaleRatio: {
type: Number,
default: 1,
validator: (val) => typeof val === 'number'
},
// handle是否缩放
handleInfo: {
type: Object,
default: () => {
return {
size: 8,
offset: -5,
switch: true
};
}
},
componentName: {
type: String,
default: ''
},
componentKey: {
type: String,
default: ''
},
},
data: function () {
return {
left: this.x,
top: this.y,
right: null,
bottom: null,
width: null,
height: null,
widthTouched: false,
heightTouched: false,
aspectFactor: null,
parentWidth: null,
parentHeight: null,
minW: this.minWidth,
minH: this.minHeight,
maxW: this.maxWidth,
maxH: this.maxHeight,
handle: null,
enabled: this.active,
resizing: false,
dragging: false,
zIndex: this.z,
hasMove: false,
hasResize: false,
};
},
created: function () {
// eslint-disable-next-line
if (this.maxWidth && this.minWidth > this.maxWidth) console.warn('[Vdr warn]: Invalid prop: minWidth cannot be greater than maxWidth')
// eslint-disable-next-line
if (this.maxWidth && this.minHeight > this.maxHeight) console.warn('[Vdr warn]: Invalid prop: minHeight cannot be greater than maxHeight')
this.resetBoundsAndMouseState();
},
mounted: function () {
if (!this.enableNativeDrag) {
this.$el.ondragstart = () => false;
}
const [parentWidth, parentHeight] = this.getParentSize();
this.parentWidth = parentWidth;
this.parentHeight = parentHeight;
const [width, height] = getComputedSize(this.$el);
this.aspectFactor = (this.w !== 'auto' ? this.w : width) / (this.h !== 'auto' ? this.h : height);
this.width = this.w !== 'auto' ? this.w : width;
this.height = this.h !== 'auto' ? this.h : height;
this.right = this.parentWidth - this.width - this.left;
this.bottom = this.parentHeight - this.height - this.top;
this.settingAttribute();
addEvent(document.documentElement, 'mousedown', this.deselect);
addEvent(document.documentElement, 'touchend touchcancel', this.deselect);
addEvent(window, 'resize', this.checkParentSize);
},
beforeDestroy: function () {
removeEvent(document.documentElement, 'mousedown', this.deselect);
removeEvent(document.documentElement, 'touchstart', this.handleUp);
removeEvent(document.documentElement, 'mousemove', this.move);
removeEvent(document.documentElement, 'touchmove', this.move);
removeEvent(document.documentElement, 'mouseup', this.handleUp);
removeEvent(document.documentElement, 'touchend touchcancel', this.deselect);
removeEvent(window, 'resize', this.checkParentSize);
},
methods: {
// 重置边界和鼠标状态
resetBoundsAndMouseState () {
this.mouseClickPosition = { mouseX: 0, mouseY: 0, x: 0, y: 0, w: 0, h: 0 };
this.bounds = {
minLeft: null,
maxLeft: null,
minRight: null,
maxRight: null,
minTop: null,
maxTop: null,
minBottom: null,
maxBottom: null
};
},
// 检查父元素大小
checkParentSize () {
if (this.parent) {
const [newParentWidth, newParentHeight] = this.getParentSize();
// 修复父元素改变大小后,组件resizing时活动异常
this.right = newParentWidth - this.width - this.left;
this.bottom = newParentHeight - this.height - this.top;
this.parentWidth = newParentWidth;
this.parentHeight = newParentHeight;
}
},
// 获取父元素大小
getParentSize () {
if (this.parent === true) {
const style = window.getComputedStyle(this.$el.parentNode, null);
return [
parseInt(style.getPropertyValue('width'), 10),
parseInt(style.getPropertyValue('height'), 10)
];
}
if (typeof this.parent === 'string') {
const parentNode = document.querySelector(this.parent);
if (!(parentNode instanceof HTMLElement)) {
throw new Error(`The selector ${this.parent} does not match any element`);
}
return [parentNode.offsetWidth, parentNode.offsetHeight];
}
return [null, null];
},
// 元素触摸按下
elementTouchDown (e) {
eventsFor = events.touch;
this.elementDown(e);
},
elementMouseDown (e) {
eventsFor = events.mouse;
this.elementDown(e);
},
// 元素按下
elementDown (e) {
if (e instanceof MouseEvent && e.which !== 1 && e.which !== 3) {
return;
}
const target = e.target || e.srcElement;
if (this.$el.contains(target)) {
if (this.onDragStart(e) === false) {
return;
}
if (
(this.dragHandle && !matchesSelectorToParentElements(target, this.dragHandle, this.$el)) ||
(this.dragCancel && matchesSelectorToParentElements(target, this.dragCancel, this.$el))
) {
this.dragging = false;
return;
}
if (!this.enabled) {
this.enabled = true;
this.$emit('activated', this.componentKey);
this.$emit('update:active', true);
}
if (this.draggable) {
this.dragging = true;
}
this.mouseClickPosition.mouseX = e.touches ? e.touches[0].pageX : e.pageX;
this.mouseClickPosition.mouseY = e.touches ? e.touches[0].pageY : e.pageY;
this.mouseClickPosition.left = this.left;
this.mouseClickPosition.right = this.right;
this.mouseClickPosition.top = this.top;
this.mouseClickPosition.bottom = this.bottom;
this.mouseClickPosition.w = this.width;
this.mouseClickPosition.h = this.height;
if (this.parent) {
this.bounds = this.calcDragLimits();
}
this.hasMove = false;
addEvent(document.documentElement, eventsFor.move, this.move);
addEvent(document.documentElement, eventsFor.stop, this.handleUp);
}
},
selectCurComponent (e) {
// 阻止向父组件冒泡
e.stopPropagation();
e.preventDefault();
this.$store.commit('hideContextMenu');
},
// 计算移动范围
calcDragLimits () {
return {
minLeft: this.left % this.grid[0],
maxLeft: Math.floor((this.parentWidth - this.width - this.left) / this.grid[0]) * this.grid[0] + this.left,
minRight: this.right % this.grid[0],
maxRight: Math.floor((this.parentWidth - this.width - this.right) / this.grid[0]) * this.grid[0] + this.right,
minTop: this.top % this.grid[1],
maxTop: Math.floor((this.parentHeight - this.height - this.top) / this.grid[1]) * this.grid[1] + this.top,
minBottom: this.bottom % this.grid[1],
maxBottom: Math.floor((this.parentHeight - this.height - this.bottom) / this.grid[1]) * this.grid[1] + this.bottom
};
},
// 取消
deselect (e) {
const target = e.target || e.srcElement;
const regex = new RegExp(this.className + '-([trmbl]{2})', '');
if (!this.$el.contains(target) && !regex.test(target.className)) {
if (this.enabled && !this.preventDeactivation) {
this.enabled = false;
this.$emit('deactivated');
this.$emit('update:active', false);
}
removeEvent(document.documentElement, eventsFor.move, this.handleResize);
}
this.resetBoundsAndMouseState();
},
// 控制柄触摸按下
handleTouchDown (handle, e) {
eventsFor = events.touch;
this.handleDown(handle, e);
},
// 控制柄按下
handleDown (handle, e) {
if (e instanceof MouseEvent && e.which !== 1) {
return;
}
if (this.onResizeStart(handle, e) === false) {
return;
}
if (e.stopPropagation) e.stopPropagation();
// Here we avoid a dangerous recursion by faking
// corner handles as middle handles
if (this.lockAspectRatio && !handle.includes('m')) {
this.handle = 'm' + handle.substring(1);
} else {
this.handle = handle;
}
this.resizing = true;
this.mouseClickPosition.mouseX = e.touches ? e.touches[0].pageX : e.pageX;
this.mouseClickPosition.mouseY = e.touches ? e.touches[0].pageY : e.pageY;
this.mouseClickPosition.left = this.left;
this.mouseClickPosition.right = this.right;
this.mouseClickPosition.top = this.top;
this.mouseClickPosition.bottom = this.bottom;
this.mouseClickPosition.w = this.width;
this.mouseClickPosition.h = this.height;
this.bounds = this.calcResizeLimits();
this.hasResize = false;
addEvent(document.documentElement, eventsFor.move, this.handleResize);
addEvent(document.documentElement, eventsFor.stop, this.handleUp);
},
// 计算调整大小范围
calcResizeLimits () {
let minW = this.minW;
let minH = this.minH;
let maxW = this.maxW;
let maxH = this.maxH;
const aspectFactor = this.aspectFactor;
const [gridX, gridY] = this.grid;
const width = this.width;
const height = this.height;
const left = this.left;
const top = this.top;
const right = this.right;
const bottom = this.bottom;
if (this.lockAspectRatio) {
if (minW / minH > aspectFactor) {
minH = minW / aspectFactor;
} else {
minW = aspectFactor * minH;
}
if (maxW && maxH) {
maxW = Math.min(maxW, aspectFactor * maxH);
maxH = Math.min(maxH, maxW / aspectFactor);
} else if (maxW) {
maxH = maxW / aspectFactor;
} else if (maxH) {
maxW = aspectFactor * maxH;
}
}
maxW = maxW - (maxW % gridX);
maxH = maxH - (maxH % gridY);
const limits = {
minLeft: null,
maxLeft: null,
minTop: null,
maxTop: null,
minRight: null,
maxRight: null,
minBottom: null,
maxBottom: null
};
if (this.parent) {
limits.minLeft = left % gridX;
limits.maxLeft = left + Math.floor((width - minW) / gridX) * gridX;
limits.minTop = top % gridY;
limits.maxTop = top + Math.floor((height - minH) / gridY) * gridY;
limits.minRight = right % gridX;
limits.maxRight = right + Math.floor((width - minW) / gridX) * gridX;
limits.minBottom = bottom % gridY;
limits.maxBottom = bottom + Math.floor((height - minH) / gridY) * gridY;
if (maxW) {
limits.minLeft = Math.max(limits.minLeft, this.parentWidth - right - maxW);
limits.minRight = Math.max(limits.minRight, this.parentWidth - left - maxW);
}
if (maxH) {
limits.minTop = Math.max(limits.minTop, this.parentHeight - bottom - maxH);
limits.minBottom = Math.max(limits.minBottom, this.parentHeight - top - maxH);
}
if (this.lockAspectRatio) {
limits.minLeft = Math.max(limits.minLeft, left - top * aspectFactor);
limits.minTop = Math.max(limits.minTop, top - left / aspectFactor);
limits.minRight = Math.max(limits.minRight, right - bottom * aspectFactor);
limits.minBottom = Math.max(limits.minBottom, bottom - right / aspectFactor);
}
} else {
limits.minLeft = null;
limits.maxLeft = left + Math.floor((width - minW) / gridX) * gridX;
limits.minTop = null;
limits.maxTop = top + Math.floor((height - minH) / gridY) * gridY;
limits.minRight = null;
limits.maxRight = right + Math.floor((width - minW) / gridX) * gridX;
limits.minBottom = null;
limits.maxBottom = bottom + Math.floor((height - minH) / gridY) * gridY;
if (maxW) {
limits.minLeft = -(right + maxW);
limits.minRight = -(left + maxW);
}
if (maxH) {
limits.minTop = -(bottom + maxH);
limits.minBottom = -(top + maxH);
}
if (this.lockAspectRatio && (maxW && maxH)) {
limits.minLeft = Math.min(limits.minLeft, -(right + maxW));
limits.minTop = Math.min(limits.minTop, -(maxH + bottom));
limits.minRight = Math.min(limits.minRight, -left - maxW);
limits.minBottom = Math.min(limits.minBottom, -top - maxH);
}
}
return limits;
},
// 移动
move (e) {
if (this.resizing) {
this.handleResize(e);
} else if (this.dragging) {
this.handleDrag(e);
}
},
// 元素移动
async handleDrag (e) {
const axis = this.axis;
const grid = this.grid;
const bounds = this.bounds;
const mouseClickPosition = this.mouseClickPosition;
const tmpDeltaX = axis && axis !== 'y' ? mouseClickPosition.mouseX - (e.touches ? e.touches[0].pageX : e.pageX) : 0;
const tmpDeltaY = axis && axis !== 'x' ? mouseClickPosition.mouseY - (e.touches ? e.touches[0].pageY : e.pageY) : 0;
const [deltaX, deltaY] = snapToGrid(grid, tmpDeltaX, tmpDeltaY, this.scaleRatio);
const left = restrictToBounds(mouseClickPosition.left - deltaX, bounds.minLeft, bounds.maxLeft);
const top = restrictToBounds(mouseClickPosition.top - deltaY, bounds.minTop, bounds.maxTop);
if (this.onDrag(left, top, this.componentKey) === false) {
return;
}
const right = restrictToBounds(mouseClickPosition.right + deltaX, bounds.minRight, bounds.maxRight);
const bottom = restrictToBounds(mouseClickPosition.bottom + deltaY, bounds.minBottom, bounds.maxBottom);
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
await this.snapCheck();
this.hasMove = true;
this.$emit('dragging', this.left, this.top, this.componentKey);
},
moveHorizontally (val) {
// eslint-disable-next-line
const [deltaX, _] = snapToGrid(this.grid, val, this.top, this.scale)
const left = restrictToBounds(deltaX, this.bounds.minLeft, this.bounds.maxLeft);
this.left = left;
this.right = this.parentWidth - this.width - left;
},
moveVertically (val) {
// eslint-disable-next-line
const [_, deltaY] = snapToGrid(this.grid, this.left, val, this.scale)
const top = restrictToBounds(deltaY, this.bounds.minTop, this.bounds.maxTop);
this.top = top;
this.bottom = this.parentHeight - this.height - top;
},
// 控制柄移动
handleResize (e) {
console.log('handleResize-------');
let left = this.left;
let top = this.top;
let right = this.right;
let bottom = this.bottom;
const mouseClickPosition = this.mouseClickPosition;
// eslint-disable-next-line
const lockAspectRatio = this.lockAspectRatio
const aspectFactor = this.aspectFactor;
const tmpDeltaX = mouseClickPosition.mouseX - (e.touches ? e.touches[0].pageX : e.pageX);
const tmpDeltaY = mouseClickPosition.mouseY - (e.touches ? e.touches[0].pageY : e.pageY);
if (!this.widthTouched && tmpDeltaX) {
this.widthTouched = true;
}
if (!this.heightTouched && tmpDeltaY) {
this.heightTouched = true;
}
const [deltaX, deltaY] = snapToGrid(this.grid, tmpDeltaX, tmpDeltaY, this.scaleRatio);
if (this.handle.includes('b')) {
bottom = restrictToBounds(
mouseClickPosition.bottom + deltaY,
this.bounds.minBottom,
this.bounds.maxBottom
);
if (this.lockAspectRatio && this.resizingOnY) {
right = this.right - (this.bottom - bottom) * aspectFactor;
}
} else if (this.handle.includes('t')) {
top = restrictToBounds(
mouseClickPosition.top - deltaY,
this.bounds.minTop,
this.bounds.maxTop
);
if (this.lockAspectRatio && this.resizingOnY) {
left = this.left - (this.top - top) * aspectFactor;
}
}
if (this.handle.includes('r')) {
right = restrictToBounds(
mouseClickPosition.right + deltaX,
this.bounds.minRight,
this.bounds.maxRight
);
if (this.lockAspectRatio && this.resizingOnX) {
bottom = this.bottom - (this.right - right) / aspectFactor;
}
} else if (this.handle.includes('l')) {
left = restrictToBounds(
mouseClickPosition.left - deltaX,
this.bounds.minLeft,
this.bounds.maxLeft
);
if (this.lockAspectRatio && this.resizingOnX) {
top = this.top - (this.left - left) / aspectFactor;
}
}
const width = computeWidth(this.parentWidth, left, right);
const height = computeHeight(this.parentHeight, top, bottom);
if (this.onResize(this.handle, left, top, width, height) === false) {
return;
}
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
this.width = width;
this.height = height;
this.hasResize = true;
this.$emit('resizing', this.left, this.top, this.width, this.height, this.componentKey);
},
changeWidth (val) {
// eslint-disable-next-line
const [newWidth, _] = snapToGrid(this.grid, val, 0, this.scale)
let right = restrictToBounds(
(this.parentWidth - newWidth - this.left),
this.bounds.minRight,
this.bounds.maxRight
);
let bottom = this.bottom;
if (this.lockAspectRatio) {
bottom = this.bottom - (this.right - right) / this.aspectFactor;
}
const width = computeWidth(this.parentWidth, this.left, right);
const height = computeHeight(this.parentHeight, this.top, bottom);
this.right = right;
this.bottom = bottom;
this.width = width;
this.height = height;
},
changeHeight (val) {
// eslint-disable-next-line
const [_, newHeight] = snapToGrid(this.grid, 0, val, this.scale)
let bottom = restrictToBounds(
(this.parentHeight - newHeight - this.top),
this.bounds.minBottom,
this.bounds.maxBottom
);
let right = this.right;
if (this.lockAspectRatio) {
right = this.right - (this.bottom - bottom) * this.aspectFactor;
}
const width = computeWidth(this.parentWidth, this.left, right);
const height = computeHeight(this.parentHeight, this.top, bottom);
this.right = right;
this.bottom = bottom;
this.width = width;
this.height = height;
},
// 从控制柄松开
async handleUp () {
this.handle = null;
// 初始化辅助线数据
const temArr = new Array(3).fill({ display: false, position: '', origin: '', lineLength: '' });
const refLine = { vLine: [], hLine: [] };
for (let i in refLine) { refLine[i] = JSON.parse(JSON.stringify(temArr)); }
if (this.resizing) {
this.resizing = false;
await this.conflictCheck();
this.$emit('refLineParams', refLine);
this.$emit('resizestop', this.left, this.top, this.width, this.height, this.componentKey);
this.hasResize && this.$store.commit('recordSnapshot');
}
if (this.dragging) {
this.dragging = false;
await this.conflictCheck();
this.$emit('refLineParams', refLine);
this.$emit('dragstop', this.left, this.top, this.componentKey);
this.hasMove && this.$store.commit('recordSnapshot');
}
this.resetBoundsAndMouseState();
removeEvent(document.documentElement, eventsFor.move, this.handleResize);
},
// 新增方法 ↓↓↓
// 设置属性
settingAttribute () {
// 设置冲突检测
this.$el.setAttribute('data-is-check', `${this.isConflictCheck}`);
// 设置对齐元素
this.$el.setAttribute('data-is-snap', `${this.snap}`);
},
// 冲突检测
conflictCheck () {
const top = this.top;
const left = this.left;
const width = this.width;
const height = this.height;
if (this.isConflictCheck) {
const nodes = this.$el.parentNode.childNodes; // 获取当前父节点下所有子节点
for (let item of nodes) {
if (item.className !== undefined && !item.className.includes(this.classNameActive) && item.getAttribute('data-is-check') !== null && item.getAttribute('data-is-check') !== 'false') {
const tw = item.offsetWidth;
const th = item.offsetHeight;
// 正则获取left与right
let [tl, tt] = this.formatTransformVal(item.style.transform);
// 左上角与右下角重叠
const tfAndBr = (top >= tt && left >= tl && tt + th > top && tl + tw > left) || (top <= tt && left < tl && top + height > tt && left + width > tl);
// 右上角与左下角重叠
const brAndTf = (left <= tl && top >= tt && left + width > tl && top < tt + th) || (top < tt && left > tl && top + height > tt && left < tl + tw);
// 下边与上边重叠
const bAndT = (top <= tt && left >= tl && top + height > tt && left < tl + tw) || (top >= tt && left <= tl && top < tt + th && left > tl + tw);
// 上边与下边重叠(宽度不一样)
const tAndB = (top <= tt && left >= tl && top + height > tt && left < tl + tw) || (top >= tt && left <= tl && top < tt + th && left > tl + tw);
// 左边与右边重叠
const lAndR = (left >= tl && top >= tt && left < tl + tw && top < tt + th) || (top > tt && left <= tl && left + width > tl && top < tt + th);
// 左边与右边重叠(高度不一样)
const rAndL = (top <= tt && left >= tl && top + height > tt && left < tl + tw) || (top >= tt && left <= tl && top < tt + th && left + width > tl);
// 如果冲突,就将回退到移动前的位置
if (tfAndBr || brAndTf || bAndT || tAndB || lAndR || rAndL) {
this.top = this.mouseClickPosition.top;
this.left = this.mouseClickPosition.left;
this.right = this.mouseClickPosition.right;
this.bottom = this.mouseClickPosition.bottom;
this.width = this.mouseClickPosition.w;
this.height = this.mouseClickPosition.h;
}
}
}
}
},
// 检测对齐元素
async snapCheck () {
let width = this.width;
let height = this.height;
if (this.snap) {
let activeLeft = this.left;
let activeRight = this.left + width;
let activeTop = this.top;
let activeBottom = this.top + height;
// 初始化辅助线数据
const temArr = new Array(3).fill({ display: false, position: '', origin: '', lineLength: '' });
const refLine = { vLine: [], hLine: [] };
for (let i in refLine) { refLine[i] = JSON.parse(JSON.stringify(temArr)); }
// 获取当前父节点下所有子节点
const nodes = this.$el.parentNode.childNodes;
let tem = {
value: { x: [[], [], []], y: [[], [], []] },
display: [],
position: []
};
const { groupWidth, groupHeight, groupLeft, groupTop, bln } = await this.getActiveAll(nodes);
if (!bln) {
width = groupWidth;
height = groupHeight;
activeLeft = groupLeft;
activeRight = groupLeft + groupWidth;
activeTop = groupTop;
activeBottom = groupTop + groupHeight;
}
for (let item of nodes) {
if (item.className !== undefined && !item.className.includes(this.classNameActive) && item.getAttribute('data-is-snap') !== null && item.getAttribute('data-is-snap') !== 'false') {
const w = item.offsetWidth;
const h = item.offsetHeight;
const [l, t] = this.formatTransformVal(item.style.transform);
const r = l + w; // 对齐目标right
const b = t + h; // 对齐目标的bottom
const hc = Math.abs((activeTop + height / 2) - (t + h / 2)) <= this.snapTolerance; // 水平中线
const vc = Math.abs((activeLeft + width / 2) - (l + w / 2)) <= this.snapTolerance; // 垂直中线
const ts = Math.abs(t - activeBottom) <= this.snapTolerance; // 从上到下
const TS = Math.abs(b - activeBottom) <= this.snapTolerance; // 从上到下
const bs = Math.abs(t - activeTop) <= this.snapTolerance; // 从下到上
const BS = Math.abs(b - activeTop) <= this.snapTolerance; // 从下到上
const ls = Math.abs(l - activeRight) <= this.snapTolerance; // 外左
const LS = Math.abs(r - activeRight) <= this.snapTolerance; // 外左
const rs = Math.abs(l - activeLeft) <= this.snapTolerance; // 外右
const RS = Math.abs(r - activeLeft) <= this.snapTolerance; // 外右
tem['display'] = [ts, TS, bs, BS, hc, hc, ls, LS, rs, RS, vc, vc];
tem['position'] = [t, b, t, b, t + h / 2, t + h / 2, l, r, l, r, l + w / 2, l + w / 2];
if (ts) {
if (bln) {
this.top = t - height;
this.bottom = this.parentHeight - this.top - height;
}
tem.value.y[0].push(l, r, activeLeft, activeRight);
}
if (bs) {
if (bln) {
this.top = t;
this.bottom = this.parentHeight - this.top - height;
}
tem.value.y[0].push(l, r, activeLeft, activeRight);
}
if (TS) {
if (bln) {
this.top = b - height;
this.bottom = this.parentHeight - this.top - height;
}
tem.value.y[1].push(l, r, activeLeft, activeRight);
}
if (BS) {
if (bln) {
this.top = b;
this.bottom = this.parentHeight - this.top - height;
}
tem.value.y[1].push(l, r, activeLeft, activeRight);
}
if (ls) {
if (bln) {
this.left = l - width;
this.right = this.parentWidth - this.left - width;
}
tem.value.x[0].push(t, b, activeTop, activeBottom);
}
if (rs) {
if (bln) {
this.left = l;
this.right = this.parentWidth - this.left - width;
}
tem.value.x[0].push(t, b, activeTop, activeBottom);
}
if (LS) {
if (bln) {
this.left = r - width;
this.right = this.parentWidth - this.left - width;
}
tem.value.x[1].push(t, b, activeTop, activeBottom);
}
if (RS) {
if (bln) {
this.left = r;
this.right = this.parentWidth - this.left - width;
}
tem.value.x[1].push(t, b, activeTop, activeBottom);
}
if (hc) {
if (bln) {
this.top = t + h / 2 - height / 2;
this.bottom = this.parentHeight - this.top - height;
}
tem.value.y[2].push(l, r, activeLeft, activeRight);
}
if (vc) {
if (bln) {
this.left = l + w / 2 - width / 2;
this.right = this.parentWidth - this.left - width;
}
tem.value.x[2].push(t, b, activeTop, activeBottom);
}
// 辅助线坐标与是否显示(display)对应的数组,易于循环遍历
const arrTem = [0, 1, 0, 1, 2, 2, 0, 1, 0, 1, 2, 2];
for (let i = 0; i <= arrTem.length; i++) {
// 前6为Y辅助线,后6为X辅助线
const xory = i < 6 ? 'y' : 'x';
const horv = i < 6 ? 'hLine' : 'vLine';
if (tem.display[i]) {
const { origin, length } = this.calcLineValues(tem.value[xory][arrTem[i]]);
refLine[horv][arrTem[i]].display = tem.display[i];
refLine[horv][arrTem[i]].position = tem.position[i] + 'px';
refLine[horv][arrTem[i]].origin = origin;
refLine[horv][arrTem[i]].lineLength = length;
}
}
}
}
this.$emit('refLineParams', refLine);
}
},
calcLineValues (arr) {
const length = Math.max(...arr) - Math.min(...arr) + 'px';
const origin = Math.min(...arr) + 'px';
return { length, origin };
},
async getActiveAll (nodes) {
const activeAll = [];
const XArray = [];
const YArray = [];
let groupWidth = 0;
let groupHeight = 0;
let groupLeft = 0;
let groupTop = 0;
for (let item of nodes) {
if (item.className !== undefined && item.className.includes(this.classNameActive)) {
activeAll.push(item);
}
}
const AllLength = activeAll.length;
if (AllLength > 1) {
for (let i of activeAll) {
const l = i.offsetLeft;
const r = l + i.offsetWidth;
const t = i.offsetTop;
const b = t + i.offsetHeight;
XArray.push(t, b);
YArray.push(l, r);
}
groupWidth = Math.max(...YArray) - Math.min(...YArray);
groupHeight = Math.max(...XArray) - Math.min(...XArray);
groupLeft = Math.min(...YArray);
groupTop = Math.min(...XArray);
}
const bln = AllLength === 1;
return { groupWidth, groupHeight, groupLeft, groupTop, bln };
},
// 正则获取left与top
formatTransformVal (string) {
let [left, top] = string.replace(/[^0-9\-,]/g, '').split(',');
if (top === undefined) top = 0;
return [+left, +top];
}
},
computed: {
handleStyle () {
return (stick) => {
if (!this.handleInfo.switch) return { display: this.enabled ? 'block' : 'none' };
const size = (this.handleInfo.size / this.scaleRatio).toFixed(2);
const offset = (this.handleInfo.offset / this.scaleRatio).toFixed(2);
const center = (size / 2).toFixed(2);
const styleMap = {
tl: {
top: `${offset}px`,
left: `${offset}px`
},
tm: {
top: `${offset}px`,
left: `calc(50% - ${center}px)`
},
tr: {
top: `${offset}px`,
right: `${offset}px`
},
mr: {
top: `calc(50% - ${center}px)`,
right: `${offset}px`
},
br: {
bottom: `${offset}px`,
right: `${offset}px`
},
bm: {
bottom: `${offset}px`,
right: `calc(50% - ${center}px)`
},
bl: {
bottom: `${offset}px`,
left: `${offset}px`
},
ml: {
top: `calc(50% - ${center}px)`,
left: `${offset}px`
}
};
const stickStyle = {
width: `${size}px`,
height: `${size}px`,
top: styleMap[stick].top,
left: styleMap[stick].left,
right: styleMap[stick].right,
bottom: styleMap[stick].bottom
};
stickStyle.display = this.enabled ? 'block' : 'none';
return stickStyle;
};
},
style () {
return {
transform: `translate(${this.left}px, ${this.top}px)`,
width: this.computedWidth,
height: this.computedHeight,
zIndex: this.zIndex,
...(this.dragging && this.disableUserSelect ? userSelectNone : userSelectAuto)
};
},
// 控制柄显示与否
actualHandles () {
if (!this.resizable) return [];
return this.handles;
},
computedWidth () {
if (this.w === 'auto') {
if (!this.widthTouched) {
return 'auto';
}
}
return this.width + 'px';
},
computedHeight () {
if (this.h === 'auto') {
if (!this.heightTouched) {
return 'auto';
}
}
return this.height + 'px';
},
resizingOnX () {
return (Boolean(this.handle) && (this.handle.includes('l') || this.handle.includes('r')));
},
resizingOnY () {
return (Boolean(this.handle) && (this.handle.includes('t') || this.handle.includes('b')));
},
isCornerHandle () {
return (Boolean(this.handle) && ['tl', 'tr', 'br', 'bl'].includes(this.handle));
}
},
watch: {
active (val) {
this.enabled = val;
if (val) {
this.$emit('activated');
} else {
this.$emit('deactivated');
}
},
z (val) {
if (val >= 0 || val === 'auto') {
this.zIndex = val;
}
},
x (val) {
if (this.resizing || this.dragging) {
return;
}
if (this.parent) {
this.bounds = this.calcDragLimits();
}
this.moveHorizontally(val);
},
y (val) {
if (this.resizing || this.dragging) {
return;
}
if (this.parent) {
this.bounds = this.calcDragLimits();
}
this.moveVertically(val);
},
lockAspectRatio (val) {
if (val) {
this.aspectFactor = this.width / this.height;
} else {
this.aspectFactor = undefined;
}
},
minWidth (val) {
if (val > 0 && val <= this.width) {
this.minW = val;
}
},
minHeight (val) {
if (val > 0 && val <= this.height) {
this.minH = val;
}
},
maxWidth (val) {
this.maxW = val;
},
maxHeight (val) {
this.maxH = val;
},
w (val) {
if (this.resizing || this.dragging) {
return;
}
if (this.parent) {
this.bounds = this.calcResizeLimits();
}
this.changeWidth(val);
},
h (val) {
if (this.resizing || this.dragging) {
return;
}
if (this.parent) {
this.bounds = this.calcResizeLimits();
}
this.changeHeight(val);
}
}
};
</script>
<template>
<div
:style="style"
:class="[
{
[classNameActive]: enabled,
[classNameDragging]: dragging,
[classNameResizing]: resizing,
[classNameDraggable]: draggable,
[classNameResizable]: resizable,
},
className,
]"
@mousedown="elementMouseDown"
@touchstart="elementTouchDown"
@click="selectCurComponent"
>
<div
v-for="(handle, index) in actualHandles"
:key="handle + index"
:class="[classNameHandle, classNameHandle + '-' + handle]"
:style="handleStyle(handle)"
@mousedown.stop.prevent="handleDown(handle, $event)"
@touchstart.stop.prevent="handleTouchDown(handle, $event)"
>
<slot :name="handle"></slot>
</div>
<slot></slot>
</div>
</template>
<script>
import { matchesSelectorToParentElements, getComputedSize, addEvent, removeEvent } from '../utils/dom';
import { computeWidth, computeHeight, restrictToBounds, snapToGrid } from '../utils/fns';
const events = {
mouse: {
start: 'mousedown',
move: 'mousemove',
stop: 'mouseup'
},
touch: {
start: 'touchstart',
move: 'touchmove',
stop: 'touchend'
}
};
// 禁止用户选取
const userSelectNone = {
userSelect: 'none',
MozUserSelect: 'none',
WebkitUserSelect: 'none',
MsUserSelect: 'none'
};
// 用户选中自动
const userSelectAuto = {
userSelect: 'auto',
MozUserSelect: 'auto',
WebkitUserSelect: 'auto',
MsUserSelect: 'auto'
};
let eventsFor = events.mouse;
export default {
replace: true,
name: 'vue-draggable-resizable',
props: {
className: {
type: String,
default: 'vdr'
},
classNameDraggable: {
type: String,
default: 'draggable'
},
classNameResizable: {
type: String,
default: 'resizable'
},
classNameDragging: {
type: String,
default: 'dragging'
},
classNameResizing: {
type: String,
default: 'resizing'
},
classNameActive: {
type: String,
default: 'active'
},
classNameHandle: {
type: String,
default: 'handle'
},
disableUserSelect: {
type: Boolean,
default: true
},
enableNativeDrag: {
type: Boolean,
default: false
},
preventDeactivation: {
type: Boolean,
default: false
},
active: {
type: Boolean,
default: false
},
draggable: {
type: Boolean,
default: true
},
resizable: {
type: Boolean,
default: true
},
// 锁定宽高比
lockAspectRatio: {
type: Boolean,
default: false
},
w: {
type: [Number, String],
default: 200,
validator: (val) => {
if (typeof val === 'number') {
return val > 0;
}
return val === 'auto';
}
},
h: {
type: [Number, String],
default: 200,
validator: (val) => {
if (typeof val === 'number') {
return val > 0;
}
return val === 'auto';
}
},
minWidth: {
type: Number,
default: 0,
validator: (val) => val >= 0
},
minHeight: {
type: Number,
default: 0,
validator: (val) => val >= 0
},
maxWidth: {
type: Number,
default: null,
validator: (val) => val >= 0
},
maxHeight: {
type: Number,
default: null,
validator: (val) => val >= 0
},
x: {
type: Number,
default: 0
},
y: {
type: Number,
default: 0
},
z: {
type: [String, Number],
default: 'auto',
validator: (val) => (typeof val === 'string' ? val === 'auto' : val >= 0)
},
handles: {
type: Array,
default: () => ['tl', 'tm', 'tr', 'mr', 'br', 'bm', 'bl', 'ml'],
validator: (val) => {
const s = new Set(['tl', 'tm', 'tr', 'mr', 'br', 'bm', 'bl', 'ml']);
return new Set(val.filter(h => s.has(h))).size === val.length;
}
},
dragHandle: {
type: String,
default: null
},
dragCancel: {
type: String,
default: null
},
axis: {
type: String,
default: 'both',
validator: (val) => ['x', 'y', 'both'].includes(val)
},
grid: {
type: Array,
default: () => [1, 1]
},
parent: {
type: [Boolean, String],
default: false
},
onDragStart: {
type: Function,
default: () => true
},
onDrag: {
type: Function,
default: () => true
},
onResizeStart: {
type: Function,
default: () => true
},
onResize: {
type: Function,
default: () => true
},
// 冲突检测
isConflictCheck: {
type: Boolean, default: false
},
// 元素对齐
snap: {
type: Boolean, default: false
},
// 当调用对齐时,用来设置组件与组件之间的对齐距离,以像素为单位
snapTolerance: {
type: Number,
default: 5,
validator: function (val) {
return typeof val === 'number';
}
},
// 缩放比例
scaleRatio: {
type: Number,
default: 1,
validator: (val) => typeof val === 'number'
},
// handle是否缩放
handleInfo: {
type: Object,
default: () => {
return {
size: 8,
offset: -5,
switch: true
};
}
},
componentName: {
type: String,
default: ''
},
componentKey: {
type: String,
default: ''
},
},
data: function () {
return {
left: this.x,
top: this.y,
right: null,
bottom: null,
width: null,
height: null,
widthTouched: false,
heightTouched: false,
aspectFactor: null,
parentWidth: null,
parentHeight: null,
minW: this.minWidth,
minH: this.minHeight,
maxW: this.maxWidth,
maxH: this.maxHeight,
handle: null,
enabled: this.active,
resizing: false,
dragging: false,
zIndex: this.z,
hasMove: false,
hasResize: false,
};
},
created: function () {
// eslint-disable-next-line
if (this.maxWidth && this.minWidth > this.maxWidth) console.warn('[Vdr warn]: Invalid prop: minWidth cannot be greater than maxWidth')
// eslint-disable-next-line
if (this.maxWidth && this.minHeight > this.maxHeight) console.warn('[Vdr warn]: Invalid prop: minHeight cannot be greater than maxHeight')
this.resetBoundsAndMouseState();
},
mounted: function () {
if (!this.enableNativeDrag) {
this.$el.ondragstart = () => false;
}
const [parentWidth, parentHeight] = this.getParentSize();
this.parentWidth = parentWidth;
this.parentHeight = parentHeight;
const [width, height] = getComputedSize(this.$el);
this.aspectFactor = (this.w !== 'auto' ? this.w : width) / (this.h !== 'auto' ? this.h : height);
this.width = this.w !== 'auto' ? this.w : width;
this.height = this.h !== 'auto' ? this.h : height;
this.right = this.parentWidth - this.width - this.left;
this.bottom = this.parentHeight - this.height - this.top;
this.settingAttribute();
addEvent(document.documentElement, 'mousedown', this.deselect);
addEvent(document.documentElement, 'touchend touchcancel', this.deselect);
addEvent(window, 'resize', this.checkParentSize);
},
beforeDestroy: function () {
removeEvent(document.documentElement, 'mousedown', this.deselect);
removeEvent(document.documentElement, 'touchstart', this.handleUp);
removeEvent(document.documentElement, 'mousemove', this.move);
removeEvent(document.documentElement, 'touchmove', this.move);
removeEvent(document.documentElement, 'mouseup', this.handleUp);
removeEvent(document.documentElement, 'touchend touchcancel', this.deselect);
removeEvent(window, 'resize', this.checkParentSize);
},
methods: {
// 重置边界和鼠标状态
resetBoundsAndMouseState () {
this.mouseClickPosition = { mouseX: 0, mouseY: 0, x: 0, y: 0, w: 0, h: 0 };
this.bounds = {
minLeft: null,
maxLeft: null,
minRight: null,
maxRight: null,
minTop: null,
maxTop: null,
minBottom: null,
maxBottom: null
};
},
// 检查父元素大小
checkParentSize () {
if (this.parent) {
const [newParentWidth, newParentHeight] = this.getParentSize();
// 修复父元素改变大小后,组件resizing时活动异常
this.right = newParentWidth - this.width - this.left;
this.bottom = newParentHeight - this.height - this.top;
this.parentWidth = newParentWidth;
this.parentHeight = newParentHeight;
}
},
// 获取父元素大小
getParentSize () {
if (this.parent === true) {
const style = window.getComputedStyle(this.$el.parentNode, null);
return [
parseInt(style.getPropertyValue('width'), 10),
parseInt(style.getPropertyValue('height'), 10)
];
}
if (typeof this.parent === 'string') {
const parentNode = document.querySelector(this.parent);
if (!(parentNode instanceof HTMLElement)) {
throw new Error(`The selector ${this.parent} does not match any element`);
}
return [parentNode.offsetWidth, parentNode.offsetHeight];
}
return [null, null];
},
// 元素触摸按下
elementTouchDown (e) {
eventsFor = events.touch;
this.elementDown(e);
},
elementMouseDown (e) {
eventsFor = events.mouse;
this.elementDown(e);
},
// 元素按下
elementDown (e) {
if (e instanceof MouseEvent && e.which !== 1 && e.which !== 3) {
return;
}
const target = e.target || e.srcElement;
if (this.$el.contains(target)) {
if (this.onDragStart(e) === false) {
return;
}
if (
(this.dragHandle && !matchesSelectorToParentElements(target, this.dragHandle, this.$el)) ||
(this.dragCancel && matchesSelectorToParentElements(target, this.dragCancel, this.$el))
) {
this.dragging = false;
return;
}
if (!this.enabled) {
this.enabled = true;
this.$emit('activated', this.componentKey);
this.$emit('update:active', true);
}
if (this.draggable) {
this.dragging = true;
}
this.mouseClickPosition.mouseX = e.touches ? e.touches[0].pageX : e.pageX;
this.mouseClickPosition.mouseY = e.touches ? e.touches[0].pageY : e.pageY;
this.mouseClickPosition.left = this.left;
this.mouseClickPosition.right = this.right;
this.mouseClickPosition.top = this.top;
this.mouseClickPosition.bottom = this.bottom;
this.mouseClickPosition.w = this.width;
this.mouseClickPosition.h = this.height;
if (this.parent) {
this.bounds = this.calcDragLimits();
}
this.hasMove = false;
addEvent(document.documentElement, eventsFor.move, this.move);
addEvent(document.documentElement, eventsFor.stop, this.handleUp);
}
},
selectCurComponent (e) {
// 阻止向父组件冒泡
e.stopPropagation();
e.preventDefault();
this.$store.commit('hideContextMenu');
},
// 计算移动范围
calcDragLimits () {
return {
minLeft: this.left % this.grid[0],
maxLeft: Math.floor((this.parentWidth - this.width - this.left) / this.grid[0]) * this.grid[0] + this.left,
minRight: this.right % this.grid[0],
maxRight: Math.floor((this.parentWidth - this.width - this.right) / this.grid[0]) * this.grid[0] + this.right,
minTop: this.top % this.grid[1],
maxTop: Math.floor((this.parentHeight - this.height - this.top) / this.grid[1]) * this.grid[1] + this.top,
minBottom: this.bottom % this.grid[1],
maxBottom: Math.floor((this.parentHeight - this.height - this.bottom) / this.grid[1]) * this.grid[1] + this.bottom
};
},
// 取消
deselect (e) {
const target = e.target || e.srcElement;
const regex = new RegExp(this.className + '-([trmbl]{2})', '');
if (!this.$el.contains(target) && !regex.test(target.className)) {
if (this.enabled && !this.preventDeactivation) {
this.enabled = false;
this.$emit('deactivated');
this.$emit('update:active', false);
}
removeEvent(document.documentElement, eventsFor.move, this.handleResize);
}
this.resetBoundsAndMouseState();
},
// 控制柄触摸按下
handleTouchDown (handle, e) {
eventsFor = events.touch;
this.handleDown(handle, e);
},
// 控制柄按下
handleDown (handle, e) {
if (e instanceof MouseEvent && e.which !== 1) {
return;
}
if (this.onResizeStart(handle, e) === false) {
return;
}
if (e.stopPropagation) e.stopPropagation();
// Here we avoid a dangerous recursion by faking
// corner handles as middle handles
if (this.lockAspectRatio && !handle.includes('m')) {
this.handle = 'm' + handle.substring(1);
} else {
this.handle = handle;
}
this.resizing = true;
this.mouseClickPosition.mouseX = e.touches ? e.touches[0].pageX : e.pageX;
this.mouseClickPosition.mouseY = e.touches ? e.touches[0].pageY : e.pageY;
this.mouseClickPosition.left = this.left;
this.mouseClickPosition.right = this.right;
this.mouseClickPosition.top = this.top;
this.mouseClickPosition.bottom = this.bottom;
this.mouseClickPosition.w = this.width;
this.mouseClickPosition.h = this.height;
this.bounds = this.calcResizeLimits();
this.hasResize = false;
addEvent(document.documentElement, eventsFor.move, this.handleResize);
addEvent(document.documentElement, eventsFor.stop, this.handleUp);
},
// 计算调整大小范围
calcResizeLimits () {
let minW = this.minW;
let minH = this.minH;
let maxW = this.maxW;
let maxH = this.maxH;
const aspectFactor = this.aspectFactor;
const [gridX, gridY] = this.grid;
const width = this.width;
const height = this.height;
const left = this.left;
const top = this.top;
const right = this.right;
const bottom = this.bottom;
if (this.lockAspectRatio) {
if (minW / minH > aspectFactor) {
minH = minW / aspectFactor;
} else {
minW = aspectFactor * minH;
}
if (maxW && maxH) {
maxW = Math.min(maxW, aspectFactor * maxH);
maxH = Math.min(maxH, maxW / aspectFactor);
} else if (maxW) {
maxH = maxW / aspectFactor;
} else if (maxH) {
maxW = aspectFactor * maxH;
}
}
maxW = maxW - (maxW % gridX);
maxH = maxH - (maxH % gridY);
const limits = {
minLeft: null,
maxLeft: null,
minTop: null,
maxTop: null,
minRight: null,
maxRight: null,
minBottom: null,
maxBottom: null
};
if (this.parent) {
limits.minLeft = left % gridX;
limits.maxLeft = left + Math.floor((width - minW) / gridX) * gridX;
limits.minTop = top % gridY;
limits.maxTop = top + Math.floor((height - minH) / gridY) * gridY;
limits.minRight = right % gridX;
limits.maxRight = right + Math.floor((width - minW) / gridX) * gridX;
limits.minBottom = bottom % gridY;
limits.maxBottom = bottom + Math.floor((height - minH) / gridY) * gridY;
if (maxW) {
limits.minLeft = Math.max(limits.minLeft, this.parentWidth - right - maxW);
limits.minRight = Math.max(limits.minRight, this.parentWidth - left - maxW);
}
if (maxH) {
limits.minTop = Math.max(limits.minTop, this.parentHeight - bottom - maxH);
limits.minBottom = Math.max(limits.minBottom, this.parentHeight - top - maxH);
}
if (this.lockAspectRatio) {
limits.minLeft = Math.max(limits.minLeft, left - top * aspectFactor);
limits.minTop = Math.max(limits.minTop, top - left / aspectFactor);
limits.minRight = Math.max(limits.minRight, right - bottom * aspectFactor);
limits.minBottom = Math.max(limits.minBottom, bottom - right / aspectFactor);
}
} else {
limits.minLeft = null;
limits.maxLeft = left + Math.floor((width - minW) / gridX) * gridX;
limits.minTop = null;
limits.maxTop = top + Math.floor((height - minH) / gridY) * gridY;
limits.minRight = null;
limits.maxRight = right + Math.floor((width - minW) / gridX) * gridX;
limits.minBottom = null;
limits.maxBottom = bottom + Math.floor((height - minH) / gridY) * gridY;
if (maxW) {
limits.minLeft = -(right + maxW);
limits.minRight = -(left + maxW);
}
if (maxH) {
limits.minTop = -(bottom + maxH);
limits.minBottom = -(top + maxH);
}
if (this.lockAspectRatio && (maxW && maxH)) {
limits.minLeft = Math.min(limits.minLeft, -(right + maxW));
limits.minTop = Math.min(limits.minTop, -(maxH + bottom));
limits.minRight = Math.min(limits.minRight, -left - maxW);
limits.minBottom = Math.min(limits.minBottom, -top - maxH);
}
}
return limits;
},
// 移动
move (e) {
if (this.resizing) {
this.handleResize(e);
} else if (this.dragging) {
this.handleDrag(e);
}
},
// 元素移动
async handleDrag (e) {
const axis = this.axis;
const grid = this.grid;
const bounds = this.bounds;
const mouseClickPosition = this.mouseClickPosition;
const tmpDeltaX = axis && axis !== 'y' ? mouseClickPosition.mouseX - (e.touches ? e.touches[0].pageX : e.pageX) : 0;
const tmpDeltaY = axis && axis !== 'x' ? mouseClickPosition.mouseY - (e.touches ? e.touches[0].pageY : e.pageY) : 0;
const [deltaX, deltaY] = snapToGrid(grid, tmpDeltaX, tmpDeltaY, this.scaleRatio);
const left = restrictToBounds(mouseClickPosition.left - deltaX, bounds.minLeft, bounds.maxLeft);
const top = restrictToBounds(mouseClickPosition.top - deltaY, bounds.minTop, bounds.maxTop);
if (this.onDrag(left, top, this.componentKey) === false) {
return;
}
const right = restrictToBounds(mouseClickPosition.right + deltaX, bounds.minRight, bounds.maxRight);
const bottom = restrictToBounds(mouseClickPosition.bottom + deltaY, bounds.minBottom, bounds.maxBottom);
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
await this.snapCheck();
this.hasMove = true;
this.$emit('dragging', this.left, this.top, this.componentKey);
},
moveHorizontally (val) {
// eslint-disable-next-line
const [deltaX, _] = snapToGrid(this.grid, val, this.top, this.scale)
const left = restrictToBounds(deltaX, this.bounds.minLeft, this.bounds.maxLeft);
this.left = left;
this.right = this.parentWidth - this.width - left;
},
moveVertically (val) {
// eslint-disable-next-line
const [_, deltaY] = snapToGrid(this.grid, this.left, val, this.scale)
const top = restrictToBounds(deltaY, this.bounds.minTop, this.bounds.maxTop);
this.top = top;
this.bottom = this.parentHeight - this.height - top;
},
// 控制柄移动
handleResize (e) {
console.log('handleResize-------');
let left = this.left;
let top = this.top;
let right = this.right;
let bottom = this.bottom;
const mouseClickPosition = this.mouseClickPosition;
// eslint-disable-next-line
const lockAspectRatio = this.lockAspectRatio
const aspectFactor = this.aspectFactor;
const tmpDeltaX = mouseClickPosition.mouseX - (e.touches ? e.touches[0].pageX : e.pageX);
const tmpDeltaY = mouseClickPosition.mouseY - (e.touches ? e.touches[0].pageY : e.pageY);
if (!this.widthTouched && tmpDeltaX) {
this.widthTouched = true;
}
if (!this.heightTouched && tmpDeltaY) {
this.heightTouched = true;
}
const [deltaX, deltaY] = snapToGrid(this.grid, tmpDeltaX, tmpDeltaY, this.scaleRatio);
if (this.handle.includes('b')) {
bottom = restrictToBounds(
mouseClickPosition.bottom + deltaY,
this.bounds.minBottom,
this.bounds.maxBottom
);
if (this.lockAspectRatio && this.resizingOnY) {
right = this.right - (this.bottom - bottom) * aspectFactor;
}
} else if (this.handle.includes('t')) {
top = restrictToBounds(
mouseClickPosition.top - deltaY,
this.bounds.minTop,
this.bounds.maxTop
);
if (this.lockAspectRatio && this.resizingOnY) {
left = this.left - (this.top - top) * aspectFactor;
}
}
if (this.handle.includes('r')) {
right = restrictToBounds(
mouseClickPosition.right + deltaX,
this.bounds.minRight,
this.bounds.maxRight
);
if (this.lockAspectRatio && this.resizingOnX) {
bottom = this.bottom - (this.right - right) / aspectFactor;
}
} else if (this.handle.includes('l')) {
left = restrictToBounds(
mouseClickPosition.left - deltaX,
this.bounds.minLeft,
this.bounds.maxLeft
);
if (this.lockAspectRatio && this.resizingOnX) {
top = this.top - (this.left - left) / aspectFactor;
}
}
const width = computeWidth(this.parentWidth, left, right);
const height = computeHeight(this.parentHeight, top, bottom);
if (this.onResize(this.handle, left, top, width, height) === false) {
return;
}
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
this.width = width;
this.height = height;
this.hasResize = true;
this.$emit('resizing', this.left, this.top, this.width, this.height, this.componentKey);
},
changeWidth (val) {
// eslint-disable-next-line
const [newWidth, _] = snapToGrid(this.grid, val, 0, this.scale)
let right = restrictToBounds(
(this.parentWidth - newWidth - this.left),
this.bounds.minRight,
this.bounds.maxRight
);
let bottom = this.bottom;
if (this.lockAspectRatio) {
bottom = this.bottom - (this.right - right) / this.aspectFactor;
}
const width = computeWidth(this.parentWidth, this.left, right);
const height = computeHeight(this.parentHeight, this.top, bottom);
this.right = right;
this.bottom = bottom;
this.width = width;
this.height = height;
},
changeHeight (val) {
// eslint-disable-next-line
const [_, newHeight] = snapToGrid(this.grid, 0, val, this.scale)
let bottom = restrictToBounds(
(this.parentHeight - newHeight - this.top),
this.bounds.minBottom,
this.bounds.maxBottom
);
let right = this.right;
if (this.lockAspectRatio) {
right = this.right - (this.bottom - bottom) * this.aspectFactor;
}
const width = computeWidth(this.parentWidth, this.left, right);
const height = computeHeight(this.parentHeight, this.top, bottom);
this.right = right;
this.bottom = bottom;
this.width = width;
this.height = height;
},
// 从控制柄松开
async handleUp () {
this.handle = null;
// 初始化辅助线数据
const temArr = new Array(3).fill({ display: false, position: '', origin: '', lineLength: '' });
const refLine = { vLine: [], hLine: [] };
for (let i in refLine) { refLine[i] = JSON.parse(JSON.stringify(temArr)); }
if (this.resizing) {
this.resizing = false;
await this.conflictCheck();
this.$emit('refLineParams', refLine);
this.$emit('resizestop', this.left, this.top, this.width, this.height, this.componentKey);
this.hasResize && this.$store.commit('recordSnapshot');
}
if (this.dragging) {
this.dragging = false;
await this.conflictCheck();
this.$emit('refLineParams', refLine);
this.$emit('dragstop', this.left, this.top, this.componentKey);
this.hasMove && this.$store.commit('recordSnapshot');
}
this.resetBoundsAndMouseState();
removeEvent(document.documentElement, eventsFor.move, this.handleResize);
},
// 新增方法 ↓↓↓
// 设置属性
settingAttribute () {
// 设置冲突检测
this.$el.setAttribute('data-is-check', `${this.isConflictCheck}`);
// 设置对齐元素
this.$el.setAttribute('data-is-snap', `${this.snap}`);
},
// 冲突检测
conflictCheck () {
const top = this.top;
const left = this.left;
const width = this.width;
const height = this.height;
if (this.isConflictCheck) {
const nodes = this.$el.parentNode.childNodes; // 获取当前父节点下所有子节点
for (let item of nodes) {
if (item.className !== undefined && !item.className.includes(this.classNameActive) && item.getAttribute('data-is-check') !== null && item.getAttribute('data-is-check') !== 'false') {
const tw = item.offsetWidth;
const th = item.offsetHeight;
// 正则获取left与right
let [tl, tt] = this.formatTransformVal(item.style.transform);
// 左上角与右下角重叠
const tfAndBr = (top >= tt && left >= tl && tt + th > top && tl + tw > left) || (top <= tt && left < tl && top + height > tt && left + width > tl);
// 右上角与左下角重叠
const brAndTf = (left <= tl && top >= tt && left + width > tl && top < tt + th) || (top < tt && left > tl && top + height > tt && left < tl + tw);
// 下边与上边重叠
const bAndT = (top <= tt && left >= tl && top + height > tt && left < tl + tw) || (top >= tt && left <= tl && top < tt + th && left > tl + tw);
// 上边与下边重叠(宽度不一样)
const tAndB = (top <= tt && left >= tl && top + height > tt && left < tl + tw) || (top >= tt && left <= tl && top < tt + th && left > tl + tw);
// 左边与右边重叠
const lAndR = (left >= tl && top >= tt && left < tl + tw && top < tt + th) || (top > tt && left <= tl && left + width > tl && top < tt + th);
// 左边与右边重叠(高度不一样)
const rAndL = (top <= tt && left >= tl && top + height > tt && left < tl + tw) || (top >= tt && left <= tl && top < tt + th && left + width > tl);
// 如果冲突,就将回退到移动前的位置
if (tfAndBr || brAndTf || bAndT || tAndB || lAndR || rAndL) {
this.top = this.mouseClickPosition.top;
this.left = this.mouseClickPosition.left;
this.right = this.mouseClickPosition.right;
this.bottom = this.mouseClickPosition.bottom;
this.width = this.mouseClickPosition.w;
this.height = this.mouseClickPosition.h;
}
}
}
}
},
// 检测对齐元素
async snapCheck () {
let width = this.width;
let height = this.height;
if (this.snap) {
let activeLeft = this.left;
let activeRight = this.left + width;
let activeTop = this.top;
let activeBottom = this.top + height;
// 初始化辅助线数据
const temArr = new Array(3).fill({ display: false, position: '', origin: '', lineLength: '' });
const refLine = { vLine: [], hLine: [] };
for (let i in refLine) { refLine[i] = JSON.parse(JSON.stringify(temArr)); }
// 获取当前父节点下所有子节点
const nodes = this.$el.parentNode.childNodes;
let tem = {
value: { x: [[], [], []], y: [[], [], []] },
display: [],
position: []
};
const { groupWidth, groupHeight, groupLeft, groupTop, bln } = await this.getActiveAll(nodes);
if (!bln) {
width = groupWidth;
height = groupHeight;
activeLeft = groupLeft;
activeRight = groupLeft + groupWidth;
activeTop = groupTop;
activeBottom = groupTop + groupHeight;
}
for (let item of nodes) {
if (item.className !== undefined && !item.className.includes(this.classNameActive) && item.getAttribute('data-is-snap') !== null && item.getAttribute('data-is-snap') !== 'false') {
const w = item.offsetWidth;
const h = item.offsetHeight;
const [l, t] = this.formatTransformVal(item.style.transform);
const r = l + w; // 对齐目标right
const b = t + h; // 对齐目标的bottom
const hc = Math.abs((activeTop + height / 2) - (t + h / 2)) <= this.snapTolerance; // 水平中线
const vc = Math.abs((activeLeft + width / 2) - (l + w / 2)) <= this.snapTolerance; // 垂直中线
const ts = Math.abs(t - activeBottom) <= this.snapTolerance; // 从上到下
const TS = Math.abs(b - activeBottom) <= this.snapTolerance; // 从上到下
const bs = Math.abs(t - activeTop) <= this.snapTolerance; // 从下到上
const BS = Math.abs(b - activeTop) <= this.snapTolerance; // 从下到上
const ls = Math.abs(l - activeRight) <= this.snapTolerance; // 外左
const LS = Math.abs(r - activeRight) <= this.snapTolerance; // 外左
const rs = Math.abs(l - activeLeft) <= this.snapTolerance; // 外右
const RS = Math.abs(r - activeLeft) <= this.snapTolerance; // 外右
tem['display'] = [ts, TS, bs, BS, hc, hc, ls, LS, rs, RS, vc, vc];
tem['position'] = [t, b, t, b, t + h / 2, t + h / 2, l, r, l, r, l + w / 2, l + w / 2];
if (ts) {
if (bln) {
this.top = t - height;
this.bottom = this.parentHeight - this.top - height;
}
tem.value.y[0].push(l, r, activeLeft, activeRight);
}
if (bs) {
if (bln) {
this.top = t;
this.bottom = this.parentHeight - this.top - height;
}
tem.value.y[0].push(l, r, activeLeft, activeRight);
}
if (TS) {
if (bln) {
this.top = b - height;
this.bottom = this.parentHeight - this.top - height;
}
tem.value.y[1].push(l, r, activeLeft, activeRight);
}
if (BS) {
if (bln) {
this.top = b;
this.bottom = this.parentHeight - this.top - height;
}
tem.value.y[1].push(l, r, activeLeft, activeRight);
}
if (ls) {
if (bln) {
this.left = l - width;
this.right = this.parentWidth - this.left - width;
}
tem.value.x[0].push(t, b, activeTop, activeBottom);
}
if (rs) {
if (bln) {
this.left = l;
this.right = this.parentWidth - this.left - width;
}
tem.value.x[0].push(t, b, activeTop, activeBottom);
}
if (LS) {
if (bln) {
this.left = r - width;
this.right = this.parentWidth - this.left - width;
}
tem.value.x[1].push(t, b, activeTop, activeBottom);
}
if (RS) {
if (bln) {
this.left = r;
this.right = this.parentWidth - this.left - width;
}
tem.value.x[1].push(t, b, activeTop, activeBottom);
}
if (hc) {
if (bln) {
this.top = t + h / 2 - height / 2;
this.bottom = this.parentHeight - this.top - height;
}
tem.value.y[2].push(l, r, activeLeft, activeRight);
}
if (vc) {
if (bln) {
this.left = l + w / 2 - width / 2;
this.right = this.parentWidth - this.left - width;
}
tem.value.x[2].push(t, b, activeTop, activeBottom);
}
// 辅助线坐标与是否显示(display)对应的数组,易于循环遍历
const arrTem = [0, 1, 0, 1, 2, 2, 0, 1, 0, 1, 2, 2];
for (let i = 0; i <= arrTem.length; i++) {
// 前6为Y辅助线,后6为X辅助线
const xory = i < 6 ? 'y' : 'x';
const horv = i < 6 ? 'hLine' : 'vLine';
if (tem.display[i]) {
const { origin, length } = this.calcLineValues(tem.value[xory][arrTem[i]]);
refLine[horv][arrTem[i]].display = tem.display[i];
refLine[horv][arrTem[i]].position = tem.position[i] + 'px';
refLine[horv][arrTem[i]].origin = origin;
refLine[horv][arrTem[i]].lineLength = length;
}
}
}
}
this.$emit('refLineParams', refLine);
}
},
calcLineValues (arr) {
const length = Math.max(...arr) - Math.min(...arr) + 'px';
const origin = Math.min(...arr) + 'px';
return { length, origin };
},
async getActiveAll (nodes) {
const activeAll = [];
const XArray = [];
const YArray = [];
let groupWidth = 0;
let groupHeight = 0;
let groupLeft = 0;
let groupTop = 0;
for (let item of nodes) {
if (item.className !== undefined && item.className.includes(this.classNameActive)) {
activeAll.push(item);
}
}
const AllLength = activeAll.length;
if (AllLength > 1) {
for (let i of activeAll) {
const l = i.offsetLeft;
const r = l + i.offsetWidth;
const t = i.offsetTop;
const b = t + i.offsetHeight;
XArray.push(t, b);
YArray.push(l, r);
}
groupWidth = Math.max(...YArray) - Math.min(...YArray);
groupHeight = Math.max(...XArray) - Math.min(...XArray);
groupLeft = Math.min(...YArray);
groupTop = Math.min(...XArray);
}
const bln = AllLength === 1;
return { groupWidth, groupHeight, groupLeft, groupTop, bln };
},
// 正则获取left与top
formatTransformVal (string) {
let [left, top] = string.replace(/[^0-9\-,]/g, '').split(',');
if (top === undefined) top = 0;
return [+left, +top];
}
},
computed: {
handleStyle () {
return (stick) => {
if (!this.handleInfo.switch) return { display: this.enabled ? 'block' : 'none' };
const size = (this.handleInfo.size / this.scaleRatio).toFixed(2);
const offset = (this.handleInfo.offset / this.scaleRatio).toFixed(2);
const center = (size / 2).toFixed(2);
const styleMap = {
tl: {
top: `${offset}px`,
left: `${offset}px`
},
tm: {
top: `${offset}px`,
left: `calc(50% - ${center}px)`
},
tr: {
top: `${offset}px`,
right: `${offset}px`
},
mr: {
top: `calc(50% - ${center}px)`,
right: `${offset}px`
},
br: {
bottom: `${offset}px`,
right: `${offset}px`
},
bm: {
bottom: `${offset}px`,
right: `calc(50% - ${center}px)`
},
bl: {
bottom: `${offset}px`,
left: `${offset}px`
},
ml: {
top: `calc(50% - ${center}px)`,
left: `${offset}px`
}
};
const stickStyle = {
width: `${size}px`,
height: `${size}px`,
top: styleMap[stick].top,
left: styleMap[stick].left,
right: styleMap[stick].right,
bottom: styleMap[stick].bottom
};
stickStyle.display = this.enabled ? 'block' : 'none';
return stickStyle;
};
},
style () {
return {
transform: `translate(${this.left}px, ${this.top}px)`,
width: this.computedWidth,
height: this.computedHeight,
zIndex: this.zIndex,
...(this.dragging && this.disableUserSelect ? userSelectNone : userSelectAuto)
};
},
// 控制柄显示与否
actualHandles () {
if (!this.resizable) return [];
return this.handles;
},
computedWidth () {
if (this.w === 'auto') {
if (!this.widthTouched) {
return 'auto';
}
}
return this.width + 'px';
},
computedHeight () {
if (this.h === 'auto') {
if (!this.heightTouched) {
return 'auto';
}
}
return this.height + 'px';
},
resizingOnX () {
return (Boolean(this.handle) && (this.handle.includes('l') || this.handle.includes('r')));
},
resizingOnY () {
return (Boolean(this.handle) && (this.handle.includes('t') || this.handle.includes('b')));
},
isCornerHandle () {
return (Boolean(this.handle) && ['tl', 'tr', 'br', 'bl'].includes(this.handle));
}
},
watch: {
active (val) {
this.enabled = val;
if (val) {
this.$emit('activated');
} else {
this.$emit('deactivated');
}
},
z (val) {
if (val >= 0 || val === 'auto') {
this.zIndex = val;
}
},
x (val) {
if (this.resizing || this.dragging) {
return;
}
if (this.parent) {
this.bounds = this.calcDragLimits();
}
this.moveHorizontally(val);
},
y (val) {
if (this.resizing || this.dragging) {
return;
}
if (this.parent) {
this.bounds = this.calcDragLimits();
}
this.moveVertically(val);
},
lockAspectRatio (val) {
if (val) {
this.aspectFactor = this.width / this.height;
} else {
this.aspectFactor = undefined;
}
},
minWidth (val) {
if (val > 0 && val <= this.width) {
this.minW = val;
}
},
minHeight (val) {
if (val > 0 && val <= this.height) {
this.minH = val;
}
},
maxWidth (val) {
this.maxW = val;
},
maxHeight (val) {
this.maxH = val;
},
w (val) {
if (this.resizing || this.dragging) {
return;
}
if (this.parent) {
this.bounds = this.calcResizeLimits();
}
this.changeWidth(val);
},
h (val) {
if (this.resizing || this.dragging) {
return;
}
if (this.parent) {
this.bounds = this.calcResizeLimits();
}
this.changeHeight(val);
}
}
};
</script>
.vdr {
touch-action: none;
position: absolute;
box-sizing: border-box;
border: 1px dashed #d6d6d6;
}
.vdr.active {
z-index: 1!important;
}
.handle {
box-sizing: border-box;
position: absolute;
width: 8px;
height: 8px;
background: #ffffff;
border: 1px solid #333;
box-shadow: 0 0 2px #bbb;
z-index: 2;
}
.handle-tl {
top: -5px;
left: -5px;
cursor: nw-resize;
}
.handle-tm {
top: -5px;
left: calc(50% - 4px);
/*margin-left: -5px;*/
cursor: n-resize;
}
.handle-tr {
top: -5px;
right: -5px;
cursor: ne-resize;
}
.handle-ml {
top: calc(50% - 4px);
/*margin-top: -5px;*/
left: -5px;
cursor: w-resize;
}
.handle-mr {
top: calc(50% - 4px);
/*margin-top: -5px;*/
right: -5px;
cursor: e-resize;
}
.handle-bl {
bottom: -5px;
left: -5px;
cursor: sw-resize;
}
.handle-bm {
bottom: -5px;
left: calc(50% - 4px);
/*margin-left: -5px;*/
cursor: s-resize;
}
.handle-br {
bottom: -5px;
right: -5px;
cursor: se-resize;
}
/*旋转handle*/
/*.handle-rot {
top: 0;
left: 50%;
margin-top: -20px;
margin-left: -1px;
border-radius: 50%;
width: 10px;
height: 10px;
cursor: grab;
transform: translate(-50%, 0);
}
.handle-rot:before {
content: '';
position: absolute;
top: 8px;
left: 50%;
width: 0;
height: 7px;
border-left: 1px solid #000000;
transform: translate(-50%, 0);
}*/
.ref-line{
position: absolute;
background-color: rgb(255, 0, 204);
z-index: 9999;
}
.v-line{
width: 1px;
}
.h-line{
height: 1px;
}
@media only screen and (max-width: 768px) {
[class*="handle-"]:before {
content: '';
left: -10px;
right: -10px;
bottom: -10px;
top: -10px;
position: absolute;
}
}