any-grid-layout
Version:
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
710 lines (637 loc) • 30.1 kB
JavaScript
import Sync from "@/units/grid/other/Sync.js";
import Item from "@/units/grid/Item.js";
import {merge, throttle} from "@/units/grid/other/tool.js";
import DomFunctionImpl from "@/units/grid/DomFunctionImpl.js";
import Engine from "@/units/grid/Engine.js";
import {layoutConfig} from "@/units/grid/defaultLayoutConfig.js";
const defaultStyle = {
// minWidth: '100px',
// minHeight: '100px', // 建议和下面size[1] 值一样
height:'auto',
// width:'100%',
display: 'block',
boxSizing: 'border-box',
backgroundColor: '#2196F3',
position: 'relative',
// cursor: 'grab',
}
/** 栅格容器, 所有对DOM的操作都是安全异步执行且无返回值,无需担心获取不到document
* Container中所有对外部可以设置的属性都是在不同的布局方案下全局生效,如若有设定layout布局数组或者单对象的情况下,
* 该数组内的配置信息设置优先于Container中设定的全局设置,比如 实例化传进
* {
* col: 8,
* size:[80,80],
* layout:[{
* px:1024,
* size:[100,100]
* },
* ]}
* 此时该col生效数值是8,来自全局设置属性,size的生效值是[100,100],来自layout中指定的局部属性
* */
export default class Container extends DomFunctionImpl {
//----------------内部需要的参数---------------------//
id = ''
option = {}
element = null
classList = []
attr = []
engine = []
mode = 'pseudoStatic' // 优先级: pseudoStatic(伪静态= 响应式 + 静态) > responsive(响应式) > static(全静态)
px = null
// a = {xl: 1920, lg: 1200, md: 992, sm: 768, xs: 480, xxs: 0}
// _defaultStaticColNum = {xl: 12, lg: 10, md: 8, sm: 6, xs: 4, xxs: 0}
useLayout = { } // 当前使用的布局方案配置
//----------------外部传进的的参数---------------------//
responsive = true
static = true
layout = [] // 其中的px字段表示 XXX 像素以下执行指定布局方案
transition = true // 是否开启过渡动画
col = null
row = null // 当前自动 暂未支持固定
minRow = null // 最小行数 暂未支持
margin = [null, null]
marginX = null
marginY = null
size = [null, null]
sizeWidth = null
sizeHeight = null
minCol = null
maxCol = null
ratio = 0.1 // 只有col的情况下margin和size自动分配margin/size的比例 1:1 ratio值为1
data = [] // 传入后就不会再变,等于备份原数据
isEdit = false // 是否是编辑模式
global = {}
style = {
opacity: '0.8',
transform: 'scale(1.1)',
}
//----------------保持状态所用参数---------------------//
_mounted = false
__temp__ = {
//----------只读变量-----------//
screenWidth: null, // 用户屏幕宽度
screenHeight: null, // 用户屏幕高度
firstInitColNum: null,
containerViewWidth: null, // container视图第一次加载时候所占用的像素宽度
//----------可写变量-----------//
_isItemsDraggable: false,
_isItemsResize: false,
_mouseInContainerOuter: false,
isLeftMousedown: false,
isMousePointInContainer: false,
fromItemExchangeIndex:null, // 用户拖动交换保留最新的索引,在鼠标抬起进行最终交换
fromItem: null, // 表示在Container中的鼠标初次按下未抬起的Item, 除Item类型外的元素不会被赋值到这里
toItem: null, // 表示在Container中的鼠标按下后抬起的正下方位置的Item, 除Item类型外的元素不会被赋值到这里
cloneElement: null, // 表示在用户拖动点击拖动的瞬间克隆出来的文档
mousedownEvent: null, // 鼠标第一次点击event对象
dragOrResize: null, // drag || resize
isDragging: false,
isResizing: false,
resizeWidth: 0,
resizeHeight: 0,
}
constructor(option) {
super()
if (option.el === null) new Error('请指定需要绑定的el,是一个id值或者原生的element')
this.el = option.el
this.option = option
this.engine = new Engine(option)
this.engine.setContainer(this)
// this._layoutInit()
// this.mode === 'static' ? this.staticLayout() : this.responsiveLayout()
}
_layoutInit() {
// static [size,margin]
let aa = {
px: 1920,
mode: 'static',
keep: ['col', 'size', 'margin'],
col: 12,
size: [80, 80],
margin: [10, 10],
// data:[]
}
let layout = null
// 有响应式优先都是使用像素布局
if (this.responsive && this.static) { // 两者为 true 或者都不传或者都为false的时候使用默认的布局模式 pseudoStatic
this.mode = 'pseudoStatic'
if (this.layout === null || Object.keys(this.layout).length === 0) {
layout = this._defaultResponsivePixel
}
} else if (this.responsive) {
this.mode = 'responsive'
} else if (this.static) {
this.mode = 'static'
}
}
/** 设置列数量,必须设置,可通过实例化参数传入而不一定使用该函数,该函数用于中途临时更换列数可用 */
setColNum(col) {
if (col > 30 || col < 0) {
throw new Error('列数量只能最低为1,最高为30,如果您非要设置更高值,' +
'请直接将值给到本类中的成员col,而不是通过该函数进行设置')
}
this.col = col
this.engine.setColNum(col)
return this
}
/** 设置行数量,行数非必须设置 */
setRowNum(row) {
this.row = row
return this
}
/** 获取所有的Item,返回一个列表(数组) */
getItemList() {
return this.engine.getItemList()
}
/**
* el 参数可以传入一个具名ID 或者一个原生的 Element 对象
* 直接渲染Container到实例化传入的所指 ID 元素中, 将实例化时候传入的 data 数据渲染出来,
* 如果实例化不传入 data 可以在后面自行创建item之后手动渲染
* */
mount() {
if (this._mounted) return
if (this.el instanceof Element) this.element = this.el
else if (typeof this.el === 'string') this.id = this.el
Sync.run(() => {
if (this.element === null){
this.element = document.getElementById(this.id)
if (this.element === null) throw new Error('未找到指定ID:' + this.id + '元素')
}
this.updateStyle(defaultStyle) // 必须在engine.init之前
if (!this.element.clientWidth)throw new Error('您应该为Container指定一个宽度,响应式布局使用指定动态宽度,静态布局可以直接设定固定宽度')
this.id = this.element.id
this.classList = Array.from(this.element.classList)
this.attr = Array.from(this.element.attributes)
this.engine.init()
this._childCollect()
this.engine.initItems()
this.engine.mountAll()
this.transitions(this.transition)
this.edit(this.isEdit)
this._event()
this.updateStyle(this.genContainerStyle())
this._mounted = true
this.__temp__.firstInitColNum = this.col
this.__temp__.screenWidth = window.screen.width
this.__temp__.screenHeight = window.screen.height
this.__temp__.containerViewWidth = this.element.clientWidth
this.responsiveLayout()
})
}
/** 将item成员从Container中全部移除 */
unmount() {
this.engine.unmount()
this._mounted = false
}
/** 将item成员从Container中全部移除,之后重新渲染 */
remount() {
this.engine.remount()
}
/** 以现有所有的Item pos信息更新Container中的全部Item布局,可以用于对某个单Item做修改后重新规划更新布局 */
updateLayout() {
this.engine.updateLayout()
}
/** 是否开启Item位置变化的过渡动画
* @param {Boolean} isTransition 是否开启动画
* */
transitions(isTransition = true) {
this.transition = isTransition
this.engine.transitions(isTransition)
}
gridWidthFromPixel(px) {
}
/** 开启响应式布局 , 非静态自动补全前面的空位,紧凑布局 */
responsiveLayout() {
this.mode = "responsive"
this.engine.responsive()
window.addEventListener('resize', (ev) => {
// this.engine.setColNum()
const browserViewWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
// let browserViewHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
// containerViewWidth
const newContainerWidth = Math.round(this.__temp__.containerViewWidth * (browserViewWidth / this.__temp__.screenWidth))
// let gridColNum = Math.floor( newContainerWidth / (this.size[0] + this.margin[0]))
// let gridColNum = Math.round(this.__temp__.firstInitColNum * (browserViewWidth / this.__temp__.screenWidth))
console.log(newContainerWidth);
// console.log(gridColNum, ' ')
// this.setColNum(gridColNum)
// this.updateStyle({
// width: this.nowWidth() + 'px'
// })
// this.updateLayout(this.genContainerStyle())
//
this.engine._syncLayoutConfig(this.engine._genLayoutConfig(newContainerWidth))
// this.engine.updateLayout()
})
Sync.run({
func: () => {
},
rule: () => this.element !== null
})
}
/** 开启伪静态布局, 静态 + 响应式 */
pseudoStaticLayout() {
}
/** 开启全静态布局 */
staticLayout() {
this.mode = "static"
this.engine.static()
}
/** 为dom添加新成员
* @param { Object || Item } item 可以是一个Item实例类或者一个配置对象
* item : {
* el : 传入一个已经存在的 element
* w : 指定宽 栅格倍数,
* h : 指定高 栅格倍数
* ......
* }
* */
add(item) {
if (!(item instanceof Item)) item = this.engine.createItem(item)
this.engine.addItem(item)
return item
}
/** 使用css class 或者 Item的对应name, 或者 Element元素 找到该对应的Item,并返回所有符合条件的Item
* name的值在创建 Item的时候可以传入 或者直接在标签属性上使用name键值,在这边也能获取到
* @param { String,Element } nameOrClassOrElement 宽度 高度 是栅格的倍数
* @return {Array} 所有符合条件的Item
* */
find(nameOrClassOrElement) {
return this.engine.findItem(nameOrClassOrElement)
}
/** 生成该栅格容器布局样式 */
genContainerStyle = () => {
return {
// gridTemplateColumns: `repeat(${this.col},${this.size[0]}px)`,
// gridTemplateRows: `repeat(${this.row},${this.size[1]}px)`,
// gridAutoRows: `${this.size[1]}px`,
// gap: `${this.margin[0]}px ${this.margin[1]}px`,
// display: 'block',
// width: this.nowWidth() + 'px',
height: this.nowHeight() + 'px',
}
}
/** 开启编辑模式
* @param {Object} editOption 包含 draggable(Boolean) resize(Boolean) 表示开启或关闭哪个功能,
* 调用该函数不传参或者传入布尔值 true表示draggable和 resize 全部开启
* 传入 布尔值 false 表示全部关闭
* */
edit(editOption = {}) {
Sync.run(()=>{
if (typeof editOption === 'object') {
if (Object.keys(editOption).length === 0) editOption = true
}
if (editOption === false) {
editOption = {draggable: false, resize: false}
} else if (editOption === true) {
editOption = {draggable: true, resize: true}
}
editOption.draggable = this._isItemsDraggable = editOption.draggable === true
editOption.resize = this._isItemsResize = editOption.resize === true
this.isEdit = this._isItemsResize || this._isItemsDraggable
this.engine.edit(editOption)
})
}
/** 获取现在的Container宽度 */
nowWidth = () => {
let marginWidth = 0
if ((this.col) > 1) marginWidth = (this.col - 1) * this.margin[0]
// console.log(this.col * this.size[0] + marginWidth)
return ((this.col) * this.size[0]) + marginWidth || 0
}
/** 获取现在的Container高度 */
nowHeight = () => {
let marginHeight = 0
if ((this.row) > 1) marginHeight = (this.row - 1) * this.margin[1]
// console.log(this.row * this.size[1] + marginHeight)
console.log(this.row);
return ((this.row) * this.size[1]) + marginHeight || 0
}
/** 将用户HTML原始文档中的Container根元素的直接儿子元素收集起来并转成Item收集在this.item中,
* 并将其渲染到DOM中 */
_childCollect() {
Array.from(this.element.children).forEach((node, index) => {
let posData = Object.assign({}, node.dataset)
// console.log(posData);
const item = this.add({el: node, ...posData})
item.name = item.getAttr('name') // 开发者直接在元素标签上使用name作为名称,后续便能直接通过该名字找到对应的Item
})
}
/** 事件委托 */
_event() {
//----------------------事件委托------------------------//
let timeOutEvent = null, regularUpdateLayoutTimer = null
let EEF = this._eventEntrustFunctor()
// this.engine.items.forEach((item) => {
// item.onResize((res) => {
// if (this.__temp__.resizeWidth === res.width && this.__temp__.resizeHeight === res.height) return
// this.__temp__.resizeWidth = res.width
// this.__temp__.resizeHeight = res.height
// this.__temp__.dragOrResize = 'resize'
// // console.log(res,this.__temp__.dragOrResize);
// })
// })
const mousedown = (ev) => {
this.__temp__.fromItem = this._findItemFromElement(ev.target)
// console.log(this.__temp__.fromItem );
// timeOutEvent = setTimeout(() => { // 监控长按事件
// clearTimeout(timeOutEvent)
// // console.log('长按了');
// }, 0)
const offsetSelfLeft = ev.target.clientWidth - ev.offsetX
const offsetSelfTop = ev.target.clientHeight - ev.offsetY
if (offsetSelfLeft < 22 || offsetSelfTop < 22) this.__temp__.dragOrResize = 'resize' // 保留resize位置防止要调整大小触发drag
else this.__temp__.dragOrResize = 'drag'
if (this.isEdit) {
// console.log(ev);
//----------------------------------------------------------------//
this.__temp__.isLeftMousedown = true
this.__temp__.mousedownEvent = ev
EEF.cursor.grabbing()
if (this.__temp__.fromItem) {
this.__temp__.fromItem.transition(false)
}
//----------------------------------------------------------------//
let regularUpdateLayoutTimer = setInterval(()=>{
if (this.__temp__.isResizing){
// this.engine.updateLayout()
}
if (this.__temp__.isDragging) {
// this.engine.updateLayout()
}
},200)
let timeOutEvent = setInterval(() => {
if (this.__temp__.isLeftMousedown === false) {
if (this.__temp__.isResizing) EEF.itemResize.mouseup()
if (this.__temp__.isDragging) {
mouseup()
this.engine.updateLayout()
}
clearInterval(timeOutEvent)
clearInterval(regularUpdateLayoutTimer)
}
if (this.__temp__.isDragging) {
// if (this.__temp__.fromItem) {
// this.__temp__.fromItem.element.style.pointerEvents = 'none'
// }
}
}, 500)
}
}
const mouseenter = (ev) => {
this.__temp__.isMousePointInContainer = true
this.__temp__.toItem = this._findItemFromElement(ev.target)
if (this.__temp__.toItem === null) return false
if (this.__temp__.isDragging) EEF.itemDrag.mouseenter(ev)
}
const mousemove = (ev) => {
if (this.isEdit) {
if (this._isItemsDraggable && this.__temp__.dragOrResize === 'drag') {
this.__temp__.isDragging = true
this.__temp__.isResizing = false
} else if (this._isItemsResize && this.__temp__.dragOrResize === 'resize') {
this.__temp__.isResizing = true
this.__temp__.isDragging = false
}
// console.log(this.__temp__.fromItem);
if (this.__temp__.isLeftMousedown) {
// console.log(this.__temp__.dragOrResize);
if (this.__temp__.isDragging) {
if (EEF.cursor.cursor !== 'grabbing') EEF.cursor.grabbing() // 加判断为了禁止css重绘
EEF.itemDrag.mousemove(ev)
} else if (this.__temp__.isResizing) {
EEF.itemResize.doResize(ev)
}
}else {
if (EEF.cursor.cursor !== 'grab') EEF.cursor.grab()
}
}
}
const mouseout = (ev) => {
if (this.isEdit) {
this.__temp__.isMousePointInContainer = false
}
}
const mouseleave = (ev) => {
if (ev.target === this.element) {
this.__temp__.isLeftMousedown = false
this.__temp__._mouseInContainerOuter = true
}
console.log(ev);
if (this.__temp__.isDragging) EEF.itemDrag.mouseleave()
}
const mouseup = (ev) => {
if (this.isEdit) {
if (this.__temp__.isResizing) EEF.itemResize.mouseup()
if (this.__temp__.isDragging) EEF.itemDrag.mouseup()
if (EEF.cursor.cursor !== 'grab') EEF.cursor.grab()
}
//------------------------------//
clearInterval(timeOutEvent)
this.__temp__.toItem = null
this.__temp__.fromItem = null
this.__temp__.isDragging = false
this.__temp__.isResizing = false
this.__temp__.isLeftMousedown = false
this.__temp__.mousedownEvent = null
this.__temp__.dragOrResize = null
}
//--------------------------------------------------------------------------------------------//
this.element.addEventListener('mousedown', mousedown)
this.engine.items.forEach((item) => item.element.addEventListener('mouseenter', mouseenter))
this.element.addEventListener('mousemove', mousemove)
this.element.addEventListener('mouseup', mouseup)
this.element.addEventListener('mouseleave', mouseleave)
this.element.addEventListener('mouseout', mouseout)
this.element.addEventListener('dragstart', (ev) => ev.preventDefault())
this.element.addEventListener('drag', (ev) => ev.preventDefault())
this.element.addEventListener('dragover', (ev) => ev.preventDefault())
//--------------------------------------------------------------------------------------------//
}
/** 用于事件委托触发的函数集 */
_eventEntrustFunctor() {
return {
itemResize: {
doResize: (ev) => {
const mousedownEvent = this.__temp__.mousedownEvent
const item = this.__temp__.fromItem
if (ev.target === this.element) return
if (item === null) return
// console.log('doResize');
//----------------------------------------//
// let offset = { // 偏离鼠标 resize 的像素
// x: item.element.clientWidth - item.__temp__.clientWidth,
// y: item.element.clientHeight - item.__temp__.clientHeight
// }
const resized = {
// w: item.__temp__.w + Math.round(offset.x / (item.size[0] + item.margin[1])) || 1,
// h: item.__temp__.h + Math.round(offset.y / (item.size[1] + item.margin[0])) || 1,
w: Math.ceil(item.element.clientWidth / (item.size[0] + item.margin[0])) || 1,
h: Math.ceil(item.element.clientHeight / (item.size[1] + item.margin[1])) || 1,
}
merge(item.pos, resized)
const pos = item.pos
//----------------检测改变的大小是否符合用户限制 -------------//
if ((resized.w + item.pos.x) > pos.col) item.pos.w = pos.col - pos.x + 1 //item调整大小时在容器右边边界超出时进行限制
if (pos.w < pos.minW) item.pos.w = pos.minW
if (pos.w > pos.maxW && pos.maxW !== Infinity) item.pos.w = pos.maxW
if (pos.h < pos.minH) item.pos.h = pos.minH
if (pos.h > pos.maxH && pos.maxH !== Infinity) item.pos.h = pos.maxH
//---------------------resize 增减长宽结束--------------//
// console.log(this.minWidth(),this.minHeight(),this.maxWidth(),this.maxHeight());
item.pos.static = true // 使其变成静态不会在调整中被动态改变x,y照成错位,调整后即刻改了回来
// console.log(width, height);
throttle(()=>{
let width = ev.clientY - mousedownEvent.offsetY + this.size[0]
let height = ev.clientX - mousedownEvent.offsetX + this.size[1]
if (width > item.nowWidth()) width = item.nowWidth()
if (height > item.nowHeight()) height = item.nowHeight()
item.updateStyle({
width: width + 'px',
height: height + 'px',
})
},300)
if (item.__temp__.resized.w !== resized.w && item.__temp__.resized.h !== resized.h) { // 只有改变Item的大小才进行style重绘
// this.engine.updateLayout(null,[item])
item.__temp__.resized = resized
item.updateStyle(item._genLimitSizeStyle())
this.updateStyle(this.genContainerStyle())
}
},
mouseup: () => {
const mousedownEvent = this.__temp__.mousedownEvent
const item = this.__temp__.fromItem
if (item === null) return
//----------------------------------------//
// this.engine.updateLayout()
item.pos.static = item.__temp__.static
item.updateStyle(item._genItemStyle())
}
},
cursor: {
container: () => {
return this
},
cursor: 'grab',
grab: function () {
const container = this.container()
container.updateStyle({cursor: 'grab'}, container.element, true)
this.cursor = 'grab'
},
grabbing: function () {
const container = this.container()
container.updateStyle({cursor: 'grabbing'}, container.element, true)
this.cursor = 'grabbing'
},
},
itemDrag: {
cloneItem: null,
mouseup: (ev) => {
const fromItem = this.__temp__.fromItem
const toItem = this.__temp__.toItem
if (fromItem === null || toItem === null) return
// console.log(ev);
fromItem?.updateStyle({
transform: 'scale(1)',
pointerEvents: 'auto',
}, this.__temp__.cloneElement)
fromItem.transition(false)
let tempI
// tempI = toItem.i // 获取最新交换位置,执行两个Item的最终交换
// toItem.i = fromItem.i
// fromItem.i = tempI
// this.engine.updateLayout()
if (this.__temp__.cloneElement !== null) {
fromItem.updateStyle({opacity: '1'})
this.element.removeChild(this.__temp__.cloneElement)
this.__temp__.cloneElement = null
}
},
mouseenter: (ev) => {
// const mousedownEvent = this.__temp__.mousedownEvent
const fromItem = this.__temp__.fromItem
const toItem = this.__temp__.toItem
if (fromItem === null || toItem === null) return
// console.log(fromItem, toItem);
let tempI
tempI = fromItem.i // 将两个Item临时交换更新布局
fromItem.i = toItem.i
toItem.i = tempI
// console.log(ev.target);
this.__temp__.fromItemExchangeIndex = tempI // 保留当前最新交换位置
// this.engine.updateLayout()
console.log(toItem === fromItem);
if (toItem === fromItem){
tempI = toItem.i // 将两个Item换回来,等于恢复原样布局信息
toItem.i = fromItem.i
fromItem.i = tempI
}
},
mouseleave:(ev)=>{
return;
const fromItem = this.__temp__.fromItem
const toItem = this.__temp__.toItem
if (fromItem === null || toItem === null) return
let tempI
tempI = toItem.i // 将两个Item换回来,等于恢复原样布局信息
toItem.i = fromItem.i
fromItem.i = tempI
},
mousemove: throttle((ev) => {
const mousedownEvent = this.__temp__.mousedownEvent
const fromItem = this.__temp__.fromItem
if (fromItem === null || mousedownEvent === null) return
if (this.__temp__.cloneElement === null) {
this.__temp__.cloneElement = fromItem.element.cloneNode(true)
this.element.appendChild(this.__temp__.cloneElement)
fromItem.updateStyle({opacity: this.style.opacity})
fromItem.updateStyle({
pointerEvents: 'none',
transform: this.style.transform
}, this.__temp__.cloneElement)
}
// console.log(ev);
const left = ev.pageX - mousedownEvent.offsetX
const top = ev.pageY - mousedownEvent.offsetY
fromItem.updateStyle({
left: left + 'px',
top: top + 'px',
// height: mousedownEvent.target.clientHeight + 'px',
// width: mousedownEvent.target.clientWidth + 'px',
}, this.__temp__.cloneElement)
}, 10)
}
}
}
_findItemFromElement = (elem) => { // 找到事件委托触发的所在container中的item, 现在只做往下第一层,TODO 后面可遍历增加
const toElementItems = this.find(elem)
if (toElementItems.length <= 0) return null
else {
// console.log(toElementItems[0]);
return toElementItems[0]
}
}
test() {
this.margin = [10, 10]
this.mount()
for (let i = 0; i < 20; i++) {
let item = this.add({
w: Math.ceil(Math.random() * 2),
h: Math.ceil(Math.random() * 2)
})
item.mount()
item.updateStyle({
backgroundColor: 'yellow',
placeContent: 'center'
})
}
}
testUnmount() {
this.engine.getItemList().forEach((item, index) => {
item.mount()
setTimeout(() => {
item.unmount()
}, index * 1000)
})
}
}