wangbd-myui
Version:
MY前端基础框架核心组件库
340 lines (312 loc) • 9.96 kB
JavaScript
/**
* 元素拖拽改变尺寸指令
* @module utils/directives/resizable
* @author 陈华春
*/
import {addClass, removeClass, getStyle, setStyle} from 'element-ui/lib/utils/dom'
import Events from '../utils//events'
// 禁止页面选择className
const USER_SELECT_NONE = 'user-select--none'
// 拖拽容器的className
const RESIZABLE_CLASS = 'xdh-resizable'
const RESIZING_CLASS = 'xdh-resizable--resizing'
// 空函数
const noop = function () {
}
/**
* Resizable实例化默认参数
* @const
* @type {object}
* @property {boolean} [disabled=false] 是否禁用
* @property {string} [handles=all] 可resize的方向,可选值 e/s/w/n/all
* @property {number} [minWidth=20] 可resize最小宽度
* @property {number} [minHeight=20] 可resize最小高度
* @property {number} [maxWidth=10000] 可resize最大宽度
* @property {number} [maxHeight=10000] 可resize最大高度
* @property {number} [edge=5] 距离边缘多少时显示鼠标Cursor
* @property {function} [onStartResize] 开始改变尺寸时回调
* @property {function} [onStopResize] 结束改变尺寸时回调
* @property {function} [onResize] 正在改变尺寸时回调
*/
const defaultOptions = {
disabled: false,
handles: 'all', // 可resize的方向,可选值 e/s/w/n/all
minWidth: 20,
minHeight: 20,
maxWidth: 10000,
maxHeight: 10000,
// 是否只改变width和height的值,在layout组件不需要改变left 和 top
onlySize: false,
edge: 5, // 距离边缘多少时显示鼠标Cursor
onStartResize: noop,
onStopResize: noop,
onResize: noop
}
/**
* Resizable 元素改变尺寸类, 继承 [Events]{@link module:utils/events~Events}
* @class
* @extends Events
*/
class Resizable extends Events {
/**
* 构造函数
* @param {HtmlDocument} document HTML 文档对象
* @param {HtmlElement} el 需要拖拽的元素
* @param {object} [options] 参数选项,默认值:[defaultOptions]{@link module:utils/directives/resizable~defaultOptions}
*/
constructor(document, el, options) {
super()
/**
* HTML文档对象
* @type {HtmlDocument}
*/
this.document = document
/**
* 需要拖拽的元素
* @type {HtmlElement}
*/
this.el = el
this.init(options)
}
init(options) {
/**
* 实例化选项参数对象
* @member {Object}
*/
this.options = Object.assign({},
defaultOptions,
options === false ? {disabled: true} : options || {})
const o = this.options
this.handleArray = o.handles.split(',')
this.isResizing = false
this.isMouseEnter = false
if (!o.disabled) {
addClass(this.el, RESIZABLE_CLASS)
this.on(this.el, 'mousedown', this.handleMouseDown)
this.on(this.el, 'mouseleave', this.handleMouseLeave)
this.on(this.el, 'mouseenter', this.handleMouseEnter)
this.on(this.document, 'mousemove', this.handleMouseMove)
}
}
/**
* 当选项参数改变时调用,更新组件
* @param {object} [options] 参数选项, 参考:[defaultOptions]{@link module:utils/directives/reizable~defaultOptions}
*/
reset(options) {
this.destroy()
this.init(options)
}
handleMouseDown(e) {
const dir = this.getDirection(e)
if (!dir) return
this.startResize(dir, e.clientX, e.clientY)
this.on(this.document, 'mouseup', this.handleMouseUp)
return false
}
handleMouseLeave() {
this.isMouseEnter = false
!this.isResizing && this.setCursor(null)
return false
}
handleMouseEnter() {
this.isMouseEnter = true
return false
}
handleMouseMove(e) {
if (this.options.disabled) return
// 鼠标进入容器才计算resize方向
if (this.isMouseEnter && !this.isResizing) {
const dir = this.getDirection(e)
this.setCursor(dir)
}
if (this.isResizing) {
this.resize(e)
this.applyResize()
}
return false
}
handleMouseUp() {
this.stopResize()
this.off(this.document, 'mouseup', this.handleMouseUp)
}
getDirection({clientX, clientY}) {
const edge = this.options.edge
const data = this.el.getBoundingClientRect()
let dir = ''
if (clientY > data.top - edge && clientY < data.top + edge) {
dir += 'n'
} else if (clientY < data.top + data.height + edge && clientY > data.top + data.height - edge) {
dir += 's'
}
if (clientX > data.left - edge && clientX < data.left + edge) {
dir += 'w'
} else if (clientX < data.left + data.width + edge && clientX > data.left + data.width - edge) {
dir += 'e'
}
for (let i = 0; i < this.handleArray.length; i++) {
const handle = this.handleArray[i].trim()
if (handle === 'all' || handle === dir) {
return dir
}
}
return null
}
startResize(dir, clientX, clientY) {
const left = this.el.offsetLeft
const top = this.el.offsetTop
const width = this.el.offsetWidth
const height = this.el.offsetHeight
this.resizeData = {
dir: dir,
startLeft: left,
startTop: top,
left: left,
top: top,
startX: clientX,
startY: clientY,
startWidth: width,
startHeight: height,
width: width, // outerWidth
height: height, // outerHeight
deltaWidth: width - Number.parseInt(getStyle(this.el, 'width')), // outerWidth 与 元素 width 的差值
deltaHeight: height - Number.parseInt(getStyle(this.el, 'height')) // outerHeight 与 元素 height 的差值
}
this.isResizing = true
this.setBodySelect(true)
this.options.onStartResize(this.resizeData)
addClass(this.el, RESIZING_CLASS)
}
stopResize() {
this.isResizing = false
this.setBodySelect()
this.options.onStopResize(this.resizeData)
removeClass(this.el, RESIZING_CLASS)
}
resize({clientX, clientY}) {
const data = this.resizeData
if (!data.dir) return
const o = this.options
if (data.dir.includes('e')) {
let width = data.startWidth + clientX - data.startX
width = Math.min(
Math.max(width, o.minWidth),
o.maxWidth
)
data.width = width
}
if (data.dir.includes('s')) {
let height = data.startHeight + clientY - data.startY
height = Math.min(
Math.max(height, o.minHeight),
o.maxHeight
)
data.height = height
}
if (data.dir.includes('w')) {
let width = data.startWidth - clientX + data.startX
width = Math.min(
Math.max(width, o.minWidth),
o.maxWidth
)
data.width = width
data.left = data.startLeft + data.startWidth - data.width
}
if (data.dir.includes('n')) {
let height = data.startHeight - clientY + data.startY
height = Math.min(
Math.max(height, o.minHeight),
o.maxHeight
)
data.height = height
data.top = data.startTop + data.startHeight - data.height
}
}
applyResize() {
const data = this.resizeData
const onlySize = this.options.onlySize
switch (data.dir) {
case 's':
setStyle(this.el, 'height', `${data.height - data.deltaHeight}px`)
break
case 'w':
setStyle(this.el, 'width', `${data.width - data.deltaWidth}px`)
!onlySize && setStyle(this.el, 'left', `${data.left}px`)
break
case 'e':
setStyle(this.el, 'width', `${data.width - data.deltaWidth}px`)
break
case 'n':
!onlySize && setStyle(this.el, 'top', `${data.top}px`)
setStyle(this.el, 'height', `${data.height - data.deltaHeight}px`)
break
default:
setStyle(this.el, 'width', `${data.width - data.deltaWidth}px`)
setStyle(this.el, 'height', `${data.height - data.deltaHeight}px`)
!onlySize && setStyle(this.el, 'left', `${data.left}px`)
!onlySize && setStyle(this.el, 'top', `${data.top}px`)
break
}
this.options.onResize(this.resizeData)
}
/**
* 为了防止拖拽过程中鼠标选中了页面的文字导致 mouseup 事件不被触发,在开始拖拽时禁止页面选择文字,在停止拖拽后再恢复
* @param none
* @private
*/
setBodySelect(none) {
none
? addClass(this.document.body, USER_SELECT_NONE)
: removeClass(this.document.body, USER_SELECT_NONE)
}
setCursor(dir) {
const cursor = dir ? `${dir}-resize` : ''
setStyle(this.document.body, 'cursor', cursor)
}
/**
* 销毁
*/
destroy() {
super.destroy()
this.off(this.el, 'mousedown', this.handleMouseDown)
this.off(this.el, 'mouseleave', this.handleMouseLeave)
this.off(this.el, 'mouseenter', this.handleMouseEnter)
this.off(this.document, 'mousemove', this.handleMouseMove)
removeClass(this.el, RESIZABLE_CLASS)
}
}
/**
* 指令 v-resizable
* @export
* @example
*
* <div v-resizable="{}"></div>
*
* import resizable from '@/utils/directives/dresizable'
* export default {
* directives: {
* resizable
* }
* }
*/
export default {
/**
* 元素在页面渲染时回调,在这里进行实例化Resizable
* @param {HtmlElement} el 指令的宿主元素
* @param {Object} binding Vue指令binding对象
*/
bind(el, binding) {
el.__resizable__ = new Resizable(window.document, el, binding.value)
},
componentUpdated(el, binding) {
// const instance = el.__resizable__
// instance.reset(binding.value)
},
/**
* 元素在页面销毁时回调,在这里销毁Resizable实例
* @param el
*/
unbind(el) {
const instance = el.__resizable__
instance.destroy()
}
}