UNPKG

vue-grid-layout-v3

Version:

A draggable and resizable grid layout, as a Vue component.

632 lines (570 loc) 20.1 kB
// @flow // export type LayoutItemRequired = {w: number, h: number, x: number, y: number, i: string}; // export type LayoutItem = LayoutItemRequired & // {minW?: number, minH?: number, maxW?: number, maxH?: number, // moved?: boolean, static?: boolean, // isDraggable?: ?boolean, isResizable?: ?boolean}; // export type Layout = Array<LayoutItem>; // export type Position = {left: number, top: number, width: number, height: number}; /* export type DragCallbackData = { node: HTMLElement, x: number, y: number, deltaX: number, deltaY: number, lastX: number, lastY: number }; */ // export type DragEvent = {e: Event} & DragCallbackData; // export type Size = {width: number, height: number}; // export type ResizeEvent = {e: Event, node: HTMLElement, size: Size}; // const isProduction = process.env.NODE_ENV === 'production'; /** * Return the bottom coordinate of the layout. * * @param {Array} layout Layout array. * @return {Number} Bottom coordinate. */ // export function bottom(layout: Layout): number { export function bottom(layout) { let max = 0; let bottomY; for (let i = 0, len = layout.length; i < len; i++) { bottomY = layout[i].y + layout[i].h; if (bottomY > max) max = bottomY; } return max; } // export function cloneLayout(layout: Layout): Layout { export function cloneLayout(layout) { return (layout || []).map(item => ({ ...item })); // const newLayout = Array(layout.length); // for (let i = 0, len = layout.length; i < len; i++) { // newLayout[i] = cloneLayoutItem(layout[i]); // } // return newLayout; } // Fast path to cloning, since this is monomorphic // export function cloneLayoutItem(layoutItem: LayoutItem): LayoutItem { export function cloneLayoutItem(layoutItem) { /* return { w: layoutItem.w, h: layoutItem.h, x: layoutItem.x, y: layoutItem.y, i: layoutItem.i, minW: layoutItem.minW, maxW: layoutItem.maxW, minH: layoutItem.minH, maxH: layoutItem.maxH, moved: Boolean(layoutItem.moved), static: Boolean(layoutItem.static), // These can be null isDraggable: layoutItem.isDraggable, isResizable: layoutItem.isResizable }; */ return JSON.parse(JSON.stringify(layoutItem)); } /** * Given two layoutitems, check if they collide. * * @return {Boolean} True if colliding. */ // export function collides(l1: LayoutItem, l2: LayoutItem): boolean { export function collides(l1, l2) { if (l1 === l2) return false; // same element if (l1.x + l1.w <= l2.x) return false; // l1 is left of l2 if (l1.x >= l2.x + l2.w) return false; // l1 is right of l2 if (l1.y + l1.h <= l2.y) return false; // l1 is above l2 if (l1.y >= l2.y + l2.h) return false; // l1 is below l2 return true; // boxes overlap } /** * Given a layout, compact it. This involves going down each y coordinate and removing gaps * between items. * * @param {Array} layout Layout. * @param {Boolean} verticalCompact Whether or not to compact the layout * vertically. * @param {Object} minPositions * @return {Array} Compacted Layout. */ // export function compact(layout: Layout, verticalCompact: Boolean, minPositions): Layout { export function compact(layout, verticalCompact, minPositions) { // Statics go in the compareWith array right away so items flow around them. const compareWith = getStatics(layout); // We go through the items by row and column. const sorted = sortLayoutItemsByRowCol(layout); // Holding for new items. const out = Array(layout.length); for (let i = 0, len = sorted.length; i < len; i++) { let l = sorted[i]; // Don't move static elements if (!l.static) { l = compactItem(compareWith, l, verticalCompact, minPositions); // Add to comparison array. We only collide with items before this one. // Statics are already in this array. compareWith.push(l); } // Add to output array to make sure they still come out in the right order. out[layout.indexOf(l)] = l; // Clear moved flag, if it exists. delete l.moved; // l.moved = false; } return out; } // /** * Compact an item in the layout. */ // export function compactItem(compareWith: Layout, l: LayoutItem, verticalCompact: boolean, minPositions): LayoutItem { export function compactItem(compareWith, l, verticalCompact, minPositions) { if (verticalCompact) { // Move the element up as far as it can go without colliding. while (l.y > 0 && !getFirstCollision(compareWith, l)) { l.y--; } } else if (minPositions) { const minY = minPositions[l.i].y; while (l.y > minY && !getFirstCollision(compareWith, l)) { l.y--; } } // Move it down, and keep moving it down if it's colliding. let collides; while ((collides = getFirstCollision(compareWith, l))) { l.y = collides.y + collides.h; } return l; } /** * Given a layout, make sure all elements fit within its bounds. * * @param {Array} layout Layout array. * @param {Number} bounds Number of columns. */ // export function correctBounds(layout: Layout, bounds: {cols: number}): Layout { export function correctBounds(layout, bounds) { const collidesWith = getStatics(layout); for (let i = 0, len = layout.length; i < len; i++) { const l = layout[i]; // Overflows right if (l.x + l.w > bounds.cols) l.x = bounds.cols - l.w; // Overflows left if (l.x < 0) { l.x = 0; l.w = bounds.cols; } if (!l.static) collidesWith.push(l); else { // If this is static and collides with other statics, we must move it down. // We have to do something nicer than just letting them overlap. while (getFirstCollision(collidesWith, l)) { l.y++; } } } return layout; } /** * Get a layout item by ID. Used so we can override later on if necessary. * * @param {Array} layout Layout array. * @param {String} id ID * @return {LayoutItem} Item at ID. */ // export function getLayoutItem(layout: Layout, id: string): ?LayoutItem { export function getLayoutItem(layout, id) { for (let i = 0, len = layout.length; i < len; i++) { if (layout[i].i === id) return layout[i]; } } /** * Returns the first item this layout collides with. * It doesn't appear to matter which order we approach this from, although * perhaps that is the wrong thing to do. * * @param {Object} layoutItem Layout item. * @return {Object|undefined} A colliding layout item, or undefined. */ // export function getFirstCollision(layout: Layout, layoutItem: LayoutItem): ?LayoutItem { export function getFirstCollision(layout, layoutItem) { for (let i = 0, len = layout.length; i < len; i++) { if (collides(layout[i], layoutItem)) return layout[i]; } } // export function getAllCollisions(layout: Layout, layoutItem: LayoutItem): Array<LayoutItem> { export function getAllCollisions(layout, layoutItem) { return layout.filter((l) => collides(l, layoutItem)); } /** * Get all static elements. * @param {Array} layout Array of layout objects. * @return {Array} Array of static layout items.. */ // export function getStatics(layout: Layout): Array<LayoutItem> { export function getStatics(layout) { // return []; return layout.filter((l) => l.static); } /** * Move an element. Responsible for doing cascading movements of other elements. * * @param {Array} layout Full layout to modify. * @param {LayoutItem} l element to move. * @param {Number} [x] X position in grid units. * @param {Number} [y] Y position in grid units. * @param {Boolean} [isUserAction] If true, designates that the item we're moving is * being dragged/resized by th euser. */ // export function moveElement(layout: Layout, l: LayoutItem, x: Number, y: Number, isUserAction: Boolean, preventCollision: Boolean): Layout { export function moveElement(layout, l, x, y, isUserAction, preventCollision) { if (l.static) return layout; // Short-circuit if nothing to do. // if (l.y === y && l.x === x) return layout; const oldX = l.x; const oldY = l.y; const movingUp = y && l.y > y; // This is quite a bit faster than extending the object if (typeof x === 'number') l.x = x; if (typeof y === 'number') l.y = y; l.moved = true; // If this collides with anything, move it. // When doing this comparison, we have to sort the items we compare with // to ensure, in the case of multiple collisions, that we're getting the // nearest collision. let sorted = sortLayoutItemsByRowCol(layout); if (movingUp) sorted = sorted.reverse(); const collisions = getAllCollisions(sorted, l); if (preventCollision && collisions.length) { l.x = oldX; l.y = oldY; l.moved = false; return layout; } // Move each item that collides away from this element. for (let i = 0, len = collisions.length; i < len; i++) { const collision = collisions[i]; // Short circuit so we can't infinite loop if (collision.moved) continue; // This makes it feel a bit more precise by waiting to swap for just a bit when moving up. if (l.y > collision.y && l.y - collision.y > collision.h / 4) continue; // Don't move static items - we have to move *this* element away if (collision.static) { layout = moveElementAwayFromCollision(layout, collision, l, isUserAction); } else { layout = moveElementAwayFromCollision(layout, l, collision, isUserAction); } } return layout; } /** * This is where the magic needs to happen - given a collision, move an element away from the collision. * We attempt to move it up if there's room, otherwise it goes below. * * @param {Array} layout Full layout to modify. * @param {LayoutItem} collidesWith Layout item we're colliding with. * @param {LayoutItem} itemToMove Layout item we're moving. * @param {Boolean} [isUserAction] If true, designates that the item we're moving is being dragged/resized * by the user. */ // export function moveElementAwayFromCollision(layout: Layout, collidesWith: LayoutItem, // itemToMove: LayoutItem, isUserAction: ?boolean): Layout { export function moveElementAwayFromCollision( layout, collidesWith, itemToMove, isUserAction, ) { const preventCollision = false; // we're already colliding // If there is enough space above the collision to put this element, move it there. // We only do this on the main collision as this can get funky in cascades and cause // unwanted swapping behavior. if (isUserAction) { // Make a mock item so we don't modify the item here, only modify in moveElement. // const fakeItem: LayoutItem = { const fakeItem = { x: itemToMove.x, y: itemToMove.y, w: itemToMove.w, h: itemToMove.h, i: '-1', }; fakeItem.y = Math.max(collidesWith.y - itemToMove.h, 0); if (!getFirstCollision(layout, fakeItem)) { return moveElement(layout, itemToMove, undefined, fakeItem.y, preventCollision); } } // Previously this was optimized to move below the collision directly, but this can cause problems // with cascading moves, as an item may actually leapflog a collision and cause a reversal in order. return moveElement(layout, itemToMove, undefined, itemToMove.y + 1, preventCollision); } /** * Helper to convert a number to a percentage string. * * @param {Number} num Any number * @return {String} That number as a percentage. */ // export function perc(num: number): string { export function perc(num) { return `${num * 100}%`; } // export function setTransform(top, left, width, height): Object { export function setTransform(top, left, width, height) { // Replace unitless items with px const translate = `translate3d(${left}px,${top}px, 0)`; return { transform: translate, WebkitTransform: translate, MozTransform: translate, msTransform: translate, OTransform: translate, width: `${width}px`, height: `${height}px`, position: 'absolute', }; } /** * Just like the setTransform method, but instead it will return a negative value of right. * * @param top * @param right * @param width * @param height * @returns {{transform: string, WebkitTransform: string, MozTransform: string, msTransform: string, OTransform: string, width: string, height: string, position: string}} */ // export function setTransformRtl(top, right, width, height): Object { export function setTransformRtl(top, right, width, height) { // Replace unitless items with px const translate = `translate3d(${right * -1}px,${top}px, 0)`; return { transform: translate, WebkitTransform: translate, MozTransform: translate, msTransform: translate, OTransform: translate, width: `${width}px`, height: `${height}px`, position: 'absolute', }; } // export function setTopLeft(top, left, width, height): Object { export function setTopLeft(top, left, width, height) { return { top: `${top}px`, left: `${left}px`, width: `${width}px`, height: `${height}px`, position: 'absolute', }; } /** * Just like the setTopLeft method, but instead, it will return a right property instead of left. * * @param top * @param right * @param width * @param height * @returns {{top: string, right: string, width: string, height: string, position: string}} */ // export function setTopRight(top, right, width, height): Object { export function setTopRight(top, right, width, height) { return { top: `${top}px`, right: `${right}px`, width: `${width}px`, height: `${height}px`, position: 'absolute', }; } /** * Get layout items sorted from top left to right and down. * * @return {Array} Array of layout objects. * @return {Array} Layout, sorted static items first. */ // export function sortLayoutItemsByRowCol(layout: Layout): Layout { export function sortLayoutItemsByRowCol(layout) { return [].concat(layout).sort((a, b) => { if (a.y === b.y && a.x === b.x) { return 0; } if (a.y > b.y || (a.y === b.y && a.x > b.x)) { return 1; } return -1; }); } /** * Generate a layout using the initialLayout and children as a template. * Missing entries will be added, extraneous ones will be truncated. * * @param {Array} initialLayout Layout passed in through props. * @param {String} breakpoint Current responsive breakpoint. * @param {Boolean} verticalCompact Whether or not to compact the layout vertically. * @return {Array} Working layout. */ // export function synchronizeLayoutWithChildren(initialLayout: Layout, children: Array<React.Element>|React.Element, // cols: number, verticalCompact: boolean): Layout { // // ensure 'children' is always an array // if (!Array.isArray(children)) { // children = [children]; // } // initialLayout = initialLayout || []; // // // Generate one layout item per child. // let layout: Layout = []; // for (let i = 0, len = children.length; i < len; i++) { // let newItem; // const child = children[i]; // // // Don't overwrite if it already exists. // const exists = getLayoutItem(initialLayout, child.key || "1" /!* FIXME satisfies Flow *!/); // if (exists) { // newItem = exists; // } else { // const g = child.props._grid; // // // Hey, this item has a _grid property, use it. // if (g) { // if (!isProduction) { // validateLayout([g], 'ReactGridLayout.children'); // } // // Validated; add it to the layout. Bottom 'y' possible is the bottom of the layout. // // This allows you to do nice stuff like specify {y: Infinity} // if (verticalCompact) { // newItem = cloneLayoutItem({...g, y: Math.min(bottom(layout), g.y), i: child.key}); // } else { // newItem = cloneLayoutItem({...g, y: g.y, i: child.key}); // } // } // // Nothing provided: ensure this is added to the bottom // else { // newItem = cloneLayoutItem({w: 1, h: 1, x: 0, y: bottom(layout), i: child.key || "1"}); // } // } // layout[i] = newItem; // } // // // Correct the layout. // layout = correctBounds(layout, {cols: cols}); // layout = compact(layout, verticalCompact); // // return layout; // } /** * Validate a layout. Throws errors. * * @param {Array} layout Array of layout items. * @param {String} [contextName] Context name for errors. * @throw {Error} Validation error. */ // export function validateLayout(layout: Layout, contextName: string): void { export function validateLayout(layout, contextName) { contextName = contextName || 'Layout'; const subProps = ['x', 'y', 'w', 'h']; const keyArr = []; if (!Array.isArray(layout)) throw new Error(`${contextName} must be an array!`); for (let i = 0, len = layout.length; i < len; i++) { const item = layout[i]; for (let j = 0; j < subProps.length; j++) { if (typeof item[subProps[j]] !== 'number') { throw new Error(`VueGridLayout: ${contextName}[${i}].${subProps[j]} must be a number!`); } } if (item.i === undefined || item.i === null) { throw new Error(`VueGridLayout: ${contextName}[${i}].i cannot be null!`); } if (typeof item.i !== 'number' && typeof item.i !== 'string') { throw new Error(`VueGridLayout: ${contextName}[${i}].i must be a string or number!`); } if (keyArr.indexOf(item.i) >= 0) { throw new Error(`VueGridLayout: ${contextName}[${i}].i must be unique!`); } keyArr.push(item.i); if (item.static !== undefined && typeof item.static !== 'boolean') { throw new Error(`VueGridLayout: ${contextName}[${i}].static must be a boolean!`); } } } // Flow can't really figure this out, so we just use Object // export function autoBindHandlers(el: Object, fns: Array<string>): void { export function autoBindHandlers(el, fns) { fns.forEach((key) => el[key] = el[key].bind(el)); } /** * Convert a JS object to CSS string. Similar to React's output of CSS. * @param obj * @returns {string} */ export function createMarkup(obj) { const keys = Object.keys(obj); if (!keys.length) return ''; let i; const len = keys.length; let result = ''; for (i = 0; i < len; i++) { const key = keys[i]; const val = obj[key]; result += `${hyphenate(key)}:${addPx(key, val)};`; } return result; } /* The following list is defined in React's core */ export const IS_UNITLESS = { animationIterationCount: true, boxFlex: true, boxFlexGroup: true, boxOrdinalGroup: true, columnCount: true, flex: true, flexGrow: true, flexPositive: true, flexShrink: true, flexNegative: true, flexOrder: true, gridRow: true, gridColumn: true, fontWeight: true, lineClamp: true, lineHeight: true, opacity: true, order: true, orphans: true, tabSize: true, widows: true, zIndex: true, zoom: true, // SVG-related properties fillOpacity: true, stopOpacity: true, strokeDashoffset: true, strokeOpacity: true, strokeWidth: true, }; /** * Will add px to the end of style values which are Numbers. * @param name * @param value * @returns {*} */ export function addPx(name, value) { if (typeof value === 'number' && !IS_UNITLESS[name]) { return `${value}px`; } return value; } /** * Hyphenate a camelCase string. * * @param {String} str * @return {String} */ export const hyphenateRE = /([a-z\d])([A-Z])/g; export function hyphenate(str) { return str.replace(hyphenateRE, '$1-$2').toLowerCase(); } // export function findItemInArray(array, property, value) { // for (let i = 0; i < array.length; i++) if (array[i][property] == value) return true; // // return false; // } export function findAndRemove(array, property, value) { array.forEach((result, index) => { if (result[property] === value) { // Remove from array array.splice(index, 1); } }); }