UNPKG

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.

397 lines (351 loc) 16.6 kB
import Sync from "@/units/grid/other/Sync.js"; import Item from "@/units/grid/Item.js"; import DomFunctionImpl from "@/units/grid/DomFunctionImpl.js"; import Engine from "@/units/grid/Engine.js"; import TempStore from "@/units/grid/other/TempStore.js"; import {defaultStyle} from "@/units/grid/style/defaultStyle.js"; import EditEvent from '@/units/grid/other/EditEvent.js' import ItemPos from '@/units/grid/ItemPos.js' const tempStore = TempStore.containerStore /** 栅格容器, 所有对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 { //----------------内部需要的参数---------------------// 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 = {} // 当前使用的布局方案配置 childContainer = [] // 所有该Container的直接子嵌套容器 isNesting = false // 该Container自身是否[被]嵌套 parentItem = null //----------------外部传进的的参数---------------------// responsive = true static = true layout = [] // 其中的px字段表示 XXX 像素以下执行指定布局方案 col = null row = null // 当前自动 暂未支持固定 margin = [null, null] marginX = null marginY = null size = [null, null] sizeWidth = null sizeHeight = null minCol = null maxCol = null minRow = null // 最小行数 只是容器高度,未和布局算法挂钩 maxRow = null // 最大行数 只是容器高度,未和布局算法挂钩 ratio = 0.1 // 只有col的情况下margin和size自动分配margin/size的比例 1:1 ratio值为1 data = [] // 传入后就不会再变,等于备份原数据 global = {} sensitivity = 0.8 // 拖拽移动的灵敏度,表示每秒移动X像素触发交换检测,这里默认每秒36px ## 不稳定性高,自用 style = defaultStyle.containerStyleConfigField // 可以外部传入直接替换 nestedOutExchange = false // 如果是嵌套页面,从嵌套页面里面拖动出来Item是否立即允许该被嵌套的容器参与响应布局,true是允许,false是不允许,参数给被嵌套容器 itemConfig = { } // 单位栅格倍数{minW,maxW,minH,maxH} ,接受的Item大小限制,同样适用于嵌套Item交换通信,建议最好在外部限制 //----------------保持状态所用参数---------------------// _mounted = false __store__ = TempStore.containerStore __ownTemp__ = { //----------只读变量-----------// exchangeLock: false, firstInitColNum: null, containerViewWidth: null, // container视图第一次加载时候所占用的像素宽度 //-----内部可写外部只读变量------// offsetPageX: 0, // 容器距离浏览器可视区域左边的距离 offsetPageY: 0, // 容器距离浏览器可视区域上边的距离 //----------可写变量-----------// } constructor(option) { super() if (option.el === null) new Error('请指定需要绑定的el,是一个id或者class值或者原生的element') this.el = option.el this.engine = new Engine(option) this.engine.setContainer(this) if (option.itemConfig) this.itemConfig = new ItemPos(option.itemConfig) // 这里的ItemPos不是真的pos,只是懒,用写好的来校验而已 } /** 设置列数量,必须设置,可通过实例化参数传入而不一定使用该函数,该函数用于中途临时更换列数可用 */ 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() } /** 在页面上添加一行的空间 */ addRowSpace(num = 1) { this.row += num this.updateStyle(this.genContainerStyle()) } /** 在页面上删除一行的空间 */ removeRowSpace(num = 1) { this.row = this.row = num if (this.row < 0) throw new Error('行数不应该小于0,请设置一个大于0的值') this.updateStyle(this.genContainerStyle()) } /** * el 参数可以传入一个具名ID 或者一个原生的 Element 对象 * 直接渲染Container到实例化传入的所指 ID 元素中, 将实例化时候传入的 data 数据渲染出来, * 如果实例化不传入 data 可以在后面自行创建item之后手动渲染 * */ mount() { if (this._mounted) return if (this.el instanceof Element) this.element = this.el Sync.run(() => { if (this.element === null) { this.element = document.querySelector(this.el) if (this.element === null) throw new Error('未找到指定ID:' + this.el + '元素') } this.updateStyle(defaultStyle.containerDefaults) // 必须在engine.init之前 if (!this.element.clientWidth) throw new Error('您应该为Container指定一个宽度,响应式布局使用指定动态宽度,静态布局可以直接设定固定宽度') this.classList = Array.from(this.element.classList) this.attr = Array.from(this.element.attributes) this.engine.init() // 初始化后就能找到用户指定的 this.useLayout this._childCollect() this.engine.initItems() this.engine.mountAll() this._isNestingContainer_() this.updateStyle(this.genContainerStyle()) this.element._gridContainer_ = this this.element._isGridContainer_ = true this.__ownTemp__.firstInitColNum = this.col this.__store__.screenWidth = window.screen.width this.__store__.screenHeight = window.screen.height this.__ownTemp__.containerViewWidth = this.element.clientWidth this.responsiveLayout() this._mounted = true }) } _isNestingContainer_(element = null) { element = element ? element : this.element if (!element) return while (true) { if (element.parentElement === null) { // 父元素往body方向遍历上去为null表示该Container是第一层 this.__ownTemp__.offsetPageX = this.element.offsetLeft this.__ownTemp__.offsetPageY = this.element.offsetTop break } element = element.parentElement // 不是null在链中往上取父元素 if (element._isGridItem_) { // 上级是Item表示是嵌套的, 父元素是Container元素执行自身offset加上父元素offset const upperItem = element._gridItem_ this.__ownTemp__.offsetPageX = upperItem.element.offsetLeft + upperItem.container.__ownTemp__.offsetPageX this.__ownTemp__.offsetPageY = upperItem.element.offsetTop + upperItem.container.__ownTemp__.offsetPageY element._gridItem_.container.childContainer.push({ parent: element._gridItem_.container, container: this, nestingItem: element._gridItem_ }) this.isNesting = true this.parentItem = upperItem upperItem.updateStyle({ overflow: 'scroll' }) break } } } /** 将item成员从Container中全部移除 */ unmount() { this.engine.unmount() this._mounted = false } /** 将item成员从Container中全部移除,之后重新渲染 */ remount() { this.engine.remount() } remove(removeItem) { this.engine.items.forEach((item) => { if (removeItem === item) item.remove() }) } /** 以现有所有的Item pos信息更新Container中的全部Item布局,可以用于对某个单Item做修改后重新规划更新布局 */ updateLayout() { this.engine.updateLayout() } /** 是否开启所有Item位置或大小变化的过渡动画 * @param {Number|Object|Boolean} transition Item移动或者大小癌变要进行变化过渡的时间,单位ms,可以传入true使用默认时间180ms,或者传入false关闭动画 * @param {String} fieldString 要进行变化的CSS属性字段,使用逗号 * */ animation(transition = true, fieldString = null) { Sync.run(() => { this.engine.items.forEach((item) => item.animation(transition, fieldString)) }) } /** 开启响应式布局 , 非静态自动补全前面的空位,紧凑布局 */ 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.__ownTemp__.containerViewWidth * (browserViewWidth / this.__store__.screenWidth)) // let gridColNum = Math.floor( newContainerWidth / (this.size[0] + this.margin[0])) // let gridColNum = Math.round(this.__ownTemp__.firstInitColNum * (browserViewWidth / this.__store__.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) { item.container = this item.parentElement = this.element 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 = {}) { // console.log(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} } if (editOption.draggable || editOption.resize) { EditEvent.startEvent(this) } else { EditEvent.removeEvent(this) } this.engine.items.forEach((item) => item.edit(editOption)) }) } /** 获取现在的Container宽度,只涉及宽度度,未和布局算法挂钩 */ nowWidth = () => { let marginWidth = 0 let nowCol = this.col if (this.maxCol !== null && this.col > this.maxCol) nowCol = this.maxCol if (this.minCol !== null && this.col < this.minCol) nowCol = this.minCol if ((nowCol) > 1) marginWidth = (nowCol - 1) * this.margin[0] // console.log(this.col * this.size[0] + marginWidth) return ((nowCol) * this.size[0]) + marginWidth || 0 } /** 获取现在的Container高度,只涉及高度,未和布局算法挂钩 */ nowHeight = () => { let marginHeight = 0 let nowRow = this.row if (this.maxRow !== null && this.row > this.maxRow) nowRow = this.maxRow if (this.minRow !== null && this.row < this.minRow) nowRow = this.minRow if ((nowRow) > 1) marginHeight = (nowRow - 1) * this.margin[1] // console.log(this.row * this.size[1] + marginHeight) // console.log(this.row); return ((nowRow) * 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 }) } 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) }) } }