UNPKG

@rybos/angular2gridster

Version:

[![npm version](https://badge.fury.io/js/angular2gridster.svg)](https://badge.fury.io/js/angular2gridster)

1 lines 282 kB
{"version":3,"file":"rybos-angular2gridster.mjs","sources":["../../../projects/angular2gridster/src/lib/utils/utils.ts","../../../projects/angular2gridster/src/lib/gridList/gridList.ts","../../../projects/angular2gridster/src/lib/gridster.service.ts","../../../projects/angular2gridster/src/lib/GridsterOptions.ts","../../../projects/angular2gridster/src/lib/gridster-prototype/gridster-prototype.service.ts","../../../projects/angular2gridster/src/lib/gridster.component.ts","../../../projects/angular2gridster/src/lib/gridList/GridListItem.ts","../../../projects/angular2gridster/src/lib/utils/DraggableEvent.ts","../../../projects/angular2gridster/src/lib/utils/draggable.ts","../../../projects/angular2gridster/src/lib/gridster-item/gridster-item.component.ts","../../../projects/angular2gridster/src/lib/gridster-prototype/gridster-item-prototype.directive.ts","../../../projects/angular2gridster/src/lib/gridster.module.ts","../../../projects/angular2gridster/src/public_api.ts","../../../projects/angular2gridster/src/rybos-angular2gridster.ts"],"sourcesContent":["\r\nimport { DraggableEvent } from './DraggableEvent';\r\n\r\nexport const utils = {\r\n setCssElementPosition: function ($element: HTMLElement, position: {x: number, y: number}) {\r\n $element.style.left = position.x + 'px';\r\n $element.style.top = position.y + 'px';\r\n },\r\n resetCSSElementPosition: function ($element: HTMLElement) {\r\n $element.style.left = '';\r\n $element.style.top = '';\r\n },\r\n setTransform: function ($element: HTMLElement, position: {x: number, y: number}) {\r\n const left = position.x;\r\n const top = position.y;\r\n\r\n // Replace unitless items with px\r\n const translate = `translate(${left}px,${top}px)`;\r\n\r\n $element.style['transform'] = translate;\r\n $element.style['WebkitTransform'] = translate;\r\n $element.style['MozTransform'] = translate;\r\n $element.style['msTransform'] = translate;\r\n $element.style['OTransform'] = translate;\r\n },\r\n resetTransform: function ($element: HTMLElement) {\r\n $element.style['transform'] = '';\r\n $element.style['WebkitTransform'] = '';\r\n $element.style['MozTransform'] = '';\r\n $element.style['msTransform'] = '';\r\n $element.style['OTransform'] = '';\r\n },\r\n clearSelection: () => {\r\n if (document['selection']) {\r\n document['selection'].empty();\r\n } else if (window.getSelection) {\r\n window.getSelection().removeAllRanges();\r\n }\r\n },\r\n isElementFitContainer: function (element: HTMLElement, containerEl: HTMLElement): boolean {\r\n const containerRect = containerEl.getBoundingClientRect();\r\n const elRect = element.getBoundingClientRect();\r\n\r\n return elRect.left > containerRect.left &&\r\n elRect.right < containerRect.right &&\r\n elRect.top > containerRect.top &&\r\n elRect.bottom < containerRect.bottom;\r\n },\r\n isElementIntersectContainer: function (element: HTMLElement, containerEl: HTMLElement): boolean {\r\n const containerRect = containerEl.getBoundingClientRect();\r\n const elRect = element.getBoundingClientRect();\r\n\r\n const elWidth = elRect.right - elRect.left;\r\n const elHeight = elRect.bottom - elRect.top;\r\n\r\n return (elRect.left + (elWidth / 2)) > containerRect.left &&\r\n (elRect.right - (elWidth / 2)) < containerRect.right &&\r\n (elRect.top + (elHeight / 2)) > containerRect.top &&\r\n (elRect.bottom - (elHeight / 2)) < containerRect.bottom;\r\n },\r\n isElementTouchContainer: function (element: HTMLElement, containerEl: HTMLElement): boolean {\r\n const containerRect = containerEl.getBoundingClientRect();\r\n const elRect = element.getBoundingClientRect();\r\n\r\n return elRect.right > containerRect.left &&\r\n elRect.bottom > containerRect.top &&\r\n elRect.left < containerRect.right &&\r\n elRect.top < containerRect.bottom;\r\n },\r\n isCursorAboveElement: function (event: DraggableEvent, element): boolean {\r\n const elRect = element.getBoundingClientRect();\r\n\r\n return event.pageX > elRect.left &&\r\n event.pageX < elRect.right &&\r\n event.pageY > elRect.top &&\r\n event.pageY < elRect.bottom;\r\n },\r\n getElementOuterHeight: function ($element: HTMLElement) {\r\n const styleObj = window.getComputedStyle($element);\r\n // NOTE: Manually calculating height because IE's `clientHeight` isn't always\r\n // reliable.\r\n return parseFloat(styleObj.getPropertyValue('height')) +\r\n parseFloat(styleObj.getPropertyValue('padding-top')) +\r\n parseFloat(styleObj.getPropertyValue('padding-bottom'));\r\n },\r\n getRelativeCoordinates: (element, parentElement): {top: number, left: number} => {\r\n const parentElementRect = parentElement.getBoundingClientRect();\r\n const elementRect = element.getBoundingClientRect();\r\n\r\n return {\r\n top: elementRect.top - parentElementRect.top,\r\n left: elementRect.left - parentElementRect.left\r\n };\r\n },\r\n getScrollableContainer(node) {\r\n const regex = /(auto|scroll)/;\r\n const parents = (_node, ps) => {\r\n if (_node.parentNode === null) {\r\n return ps;\r\n }\r\n return parents(_node.parentNode, ps.concat([_node]));\r\n };\r\n\r\n const style = (_node, prop) => {\r\n return getComputedStyle(_node, null).getPropertyValue(prop);\r\n };\r\n const overflow = _node => {\r\n return (\r\n style(_node, 'overflow') + style(_node, 'overflow-y') + style(_node, 'overflow-x')\r\n );\r\n };\r\n const scroll = _node => regex.test(overflow(_node));\r\n\r\n /* eslint-disable consistent-return */\r\n const scrollParent = _node => {\r\n if (!(_node instanceof HTMLElement || _node instanceof SVGElement)) {\r\n return;\r\n }\r\n\r\n const ps = parents(_node.parentNode, []);\r\n\r\n for (let i = 0; i < ps.length; i += 1) {\r\n if (scroll(ps[i])) {\r\n return ps[i];\r\n }\r\n }\r\n\r\n return document.scrollingElement || document.documentElement;\r\n };\r\n\r\n return scrollParent(node);\r\n }\r\n};\r\n","import { GridListItem } from './GridListItem';\r\nimport { IGridsterOptions } from '../IGridsterOptions';\r\n\r\nconst GridCol = function(lanes) {\r\n for (let i = 0; i < lanes; i++) {\r\n this.push(null);\r\n }\r\n};\r\n// Extend the Array prototype\r\nGridCol.prototype = [];\r\n\r\n/**\r\n * A GridList manages the two-dimensional positions from a list of items,\r\n * within a virtual matrix.\r\n *\r\n * The GridList's main function is to convert the item positions from one\r\n * grid size to another, maintaining as much of their order as possible.\r\n *\r\n * The GridList's second function is to handle collisions when moving an item\r\n * over another.\r\n *\r\n * The positioning algorithm places items in columns. Starting from left to\r\n * right, going through each column top to bottom.\r\n *\r\n * The size of an item is expressed using the number of cols and rows it\r\n * takes up within the grid (w and h)\r\n *\r\n * The position of an item is express using the col and row position within\r\n * the grid (x and y)\r\n *\r\n * An item is an object of structure:\r\n * {\r\n * w: 3, h: 1,\r\n * x: 0, y: 1\r\n * }\r\n */\r\nexport class GridList {\r\n items: Array<GridListItem>;\r\n grid: Array<Array<GridListItem>>;\r\n\r\n options: IGridsterOptions;\r\n\r\n constructor(items: Array<GridListItem>, options: IGridsterOptions) {\r\n this.options = options;\r\n\r\n this.items = items;\r\n\r\n this.adjustSizeOfItems();\r\n\r\n this.generateGrid();\r\n }\r\n\r\n /**\r\n * Illustrates grid as text-based table, using a number identifier for each\r\n * item. E.g.\r\n *\r\n * #| 0 1 2 3 4 5 6 7 8 9 10 11 12 13\r\n * --------------------------------------------\r\n * 0| 00 02 03 04 04 06 08 08 08 12 12 13 14 16\r\n * 1| 01 -- 03 05 05 07 09 10 11 11 -- 13 15 --\r\n *\r\n * Warn: Does not work if items don't have a width or height specified\r\n * besides their position in the grid.\r\n */\r\n toString() {\r\n const widthOfGrid = this.grid.length;\r\n let output = '\\n #|',\r\n border = '\\n --',\r\n item,\r\n i,\r\n j;\r\n\r\n // Render the table header\r\n for (i = 0; i < widthOfGrid; i++) {\r\n output += ' ' + this.padNumber(i, ' ');\r\n border += '---';\r\n }\r\n output += border;\r\n\r\n // Render table contents row by row, as we go on the y axis\r\n for (i = 0; i < this.options.lanes; i++) {\r\n output += '\\n' + this.padNumber(i, ' ') + '|';\r\n for (j = 0; j < widthOfGrid; j++) {\r\n output += ' ';\r\n item = this.grid[j][i];\r\n output += item\r\n ? this.padNumber(this.items.indexOf(item), '0')\r\n : '--';\r\n }\r\n }\r\n output += '\\n';\r\n return output;\r\n }\r\n\r\n setOption(name: string, value: any) {\r\n this.options[name] = value;\r\n }\r\n\r\n /**\r\n * Build the grid structure from scratch, with the current item positions\r\n */\r\n generateGrid() {\r\n let i;\r\n this.resetGrid();\r\n for (i = 0; i < this.items.length; i++) {\r\n this.markItemPositionToGrid(this.items[i]);\r\n }\r\n }\r\n\r\n resizeGrid(lanes: number) {\r\n let currentColumn = 0;\r\n\r\n this.options.lanes = lanes;\r\n this.adjustSizeOfItems();\r\n\r\n this.sortItemsByPosition();\r\n this.resetGrid();\r\n\r\n // The items will be sorted based on their index within the this.items array,\r\n // that is their \"1d position\"\r\n for (let i = 0; i < this.items.length; i++) {\r\n const item = this.items[i],\r\n position = this.getItemPosition(item);\r\n\r\n this.updateItemPosition(\r\n item,\r\n this.findPositionForItem(item, { x: currentColumn, y: 0 })\r\n );\r\n\r\n // New items should never be placed to the left of previous items\r\n currentColumn = Math.max(currentColumn, position.x);\r\n }\r\n\r\n this.pullItemsToLeft();\r\n }\r\n\r\n /**\r\n * This method has two options for the position we want for the item:\r\n * - Starting from a certain row/column number and only looking for\r\n * positions to its right\r\n * - Accepting positions for a certain row number only (use-case: items\r\n * being shifted to the left/right as a result of collisions)\r\n *\r\n * @param Object item\r\n * @param Object start Position from which to start\r\n * the search.\r\n * @param number [fixedRow] If provided, we're going to try to find a\r\n * position for the new item on it. If doesn't fit there, we're going\r\n * to put it on the first row.\r\n *\r\n * @returns Array x and y.\r\n */\r\n findPositionForItem(\r\n item: GridListItem,\r\n start: { x: number; y: number },\r\n fixedRow?: number\r\n ): Array<number> {\r\n let x, y, position;\r\n\r\n // Start searching for a position from the horizontal position of the\r\n // rightmost item from the grid\r\n for (x = start.x; x < this.grid.length; x++) {\r\n if (fixedRow !== undefined) {\r\n position = [x, fixedRow];\r\n\r\n if (this.itemFitsAtPosition(item, position)) {\r\n return position;\r\n }\r\n } else {\r\n for (y = start.y; y < this.options.lanes; y++) {\r\n position = [x, y];\r\n\r\n if (this.itemFitsAtPosition(item, position)) {\r\n return position;\r\n }\r\n }\r\n }\r\n }\r\n\r\n // If we've reached this point, we need to start a new column\r\n const newCol = this.grid.length;\r\n let newRow = 0;\r\n\r\n if (\r\n fixedRow !== undefined &&\r\n this.itemFitsAtPosition(item, [newCol, fixedRow])\r\n ) {\r\n newRow = fixedRow;\r\n }\r\n\r\n return [newCol, newRow];\r\n }\r\n\r\n moveAndResize(\r\n item: GridListItem,\r\n newPosition: Array<number>,\r\n size: { w: number; h: number }\r\n ) {\r\n const position = this.getItemPosition({\r\n x: newPosition[0],\r\n y: newPosition[1],\r\n w: item.w,\r\n h: item.h\r\n });\r\n const width = size.w || item.w,\r\n height = size.h || item.h;\r\n\r\n this.updateItemPosition(item, [position.x, position.y]);\r\n this.updateItemSize(item, width, height);\r\n\r\n this.resolveCollisions(item);\r\n }\r\n\r\n moveItemToPosition(item: GridListItem, newPosition: Array<number>) {\r\n const position = this.getItemPosition({\r\n x: newPosition[0],\r\n y: newPosition[1],\r\n w: item.w,\r\n h: item.h\r\n });\r\n\r\n this.updateItemPosition(item, [position.x, position.y]);\r\n this.resolveCollisions(item);\r\n }\r\n\r\n /**\r\n * Resize an item and resolve collisions.\r\n *\r\n * @param Object item A reference to an item that's part of the grid.\r\n * @param Object size\r\n * @param number [size.w=item.w] The new width.\r\n * @param number [size.h=item.h] The new height.\r\n */\r\n resizeItem(item: GridListItem, size: { w: number; h: number }) {\r\n const width = size.w || item.w,\r\n height = size.h || item.h;\r\n\r\n this.updateItemSize(item, width, height);\r\n\r\n this.pullItemsToLeft(item);\r\n }\r\n\r\n /**\r\n * Compare the current items against a previous snapshot and return only\r\n * the ones that changed their attributes in the meantime. This includes both\r\n * position (x, y) and size (w, h)\r\n *\r\n * Each item that is returned is not the GridListItem but the helper that holds GridListItem\r\n * and list of changed properties.\r\n */\r\n getChangedItems(\r\n initialItems: Array<GridListItem>,\r\n breakpoint?\r\n ): Array<{\r\n item: GridListItem;\r\n changes: Array<string>;\r\n isNew: boolean;\r\n }> {\r\n return this.items\r\n .map((item: GridListItem) => {\r\n const changes = [];\r\n const oldValues: {\r\n x?: number;\r\n y?: number;\r\n w?: number;\r\n h?: number;\r\n } = {};\r\n const initItem = initialItems.find(\r\n initItm => initItm.$element === item.$element\r\n );\r\n\r\n if (!initItem) {\r\n return { item, changes: ['x', 'y', 'w', 'h'], isNew: true };\r\n }\r\n\r\n const oldX = initItem.getValueX(breakpoint);\r\n if (item.getValueX(breakpoint) !== oldX) {\r\n changes.push('x');\r\n if (oldX || oldX === 0) {\r\n oldValues.x = oldX;\r\n }\r\n }\r\n\r\n const oldY = initItem.getValueY(breakpoint);\r\n if (item.getValueY(breakpoint) !== oldY) {\r\n changes.push('y');\r\n if (oldY || oldY === 0) {\r\n oldValues.y = oldY;\r\n }\r\n }\r\n if (\r\n item.getValueW(breakpoint) !==\r\n initItem.getValueW(breakpoint)\r\n ) {\r\n changes.push('w');\r\n oldValues.w = initItem.w;\r\n }\r\n if (\r\n item.getValueH(breakpoint) !==\r\n initItem.getValueH(breakpoint)\r\n ) {\r\n changes.push('h');\r\n oldValues.h = initItem.h;\r\n }\r\n\r\n return { item, oldValues, changes, isNew: false };\r\n })\r\n .filter(\r\n (itemChange: {\r\n item: GridListItem;\r\n changes: Array<string>;\r\n }) => {\r\n return itemChange.changes.length;\r\n }\r\n );\r\n }\r\n\r\n resolveCollisions(item: GridListItem) {\r\n if (!this.tryToResolveCollisionsLocally(item)) {\r\n this.pullItemsToLeft(item);\r\n }\r\n if (this.options.floating) {\r\n this.pullItemsToLeft();\r\n } else if (this.getItemsCollidingWithItem(item).length) {\r\n this.pullItemsToLeft();\r\n }\r\n }\r\n\r\n pushCollidingItems(fixedItem?: GridListItem) {\r\n // Start a fresh grid with the fixed item already placed inside\r\n this.sortItemsByPosition();\r\n this.resetGrid();\r\n this.generateGrid();\r\n\r\n this.items\r\n .filter(item => !this.isItemFloating(item) && item !== fixedItem)\r\n .forEach(item => {\r\n if (!this.tryToResolveCollisionsLocally(item)) {\r\n this.pullItemsToLeft(item);\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Build the grid from scratch, by using the current item positions and\r\n * pulling them as much to the left as possible, removing as space between\r\n * them as possible.\r\n *\r\n * If a \"fixed item\" is provided, its position will be kept intact and the\r\n * rest of the items will be layed around it.\r\n */\r\n pullItemsToLeft(fixedItem?) {\r\n if (this.options.direction === 'none') {\r\n return;\r\n }\r\n\r\n // Start a fresh grid with the fixed item already placed inside\r\n this.sortItemsByPosition();\r\n this.resetGrid();\r\n\r\n // Start the grid with the fixed item as the first positioned item\r\n if (fixedItem) {\r\n const fixedPosition = this.getItemPosition(fixedItem);\r\n this.updateItemPosition(fixedItem, [\r\n fixedPosition.x,\r\n fixedPosition.y\r\n ]);\r\n }\r\n\r\n this.items\r\n .filter((item: GridListItem) => {\r\n return !item.dragAndDrop && item !== fixedItem;\r\n })\r\n .forEach((item: GridListItem) => {\r\n const fixedPosition = this.getItemPosition(item);\r\n this.updateItemPosition(item, [\r\n fixedPosition.x,\r\n fixedPosition.y\r\n ]);\r\n });\r\n\r\n for (let i = 0; i < this.items.length; i++) {\r\n const item = this.items[i],\r\n position = this.getItemPosition(item);\r\n\r\n // The fixed item keeps its exact position\r\n if (\r\n (fixedItem && item === fixedItem) ||\r\n !item.dragAndDrop ||\r\n (!this.options.floating &&\r\n this.isItemFloating(item) &&\r\n !this.getItemsCollidingWithItem(item).length)\r\n ) {\r\n continue;\r\n }\r\n\r\n const x = this.findLeftMostPositionForItem(item),\r\n newPosition = this.findPositionForItem(\r\n item,\r\n { x: x, y: 0 },\r\n position.y\r\n );\r\n\r\n this.updateItemPosition(item, newPosition);\r\n }\r\n }\r\n\r\n isOverFixedArea(\r\n x: number,\r\n y: number,\r\n w: number,\r\n h: number,\r\n item: GridListItem = null\r\n ): boolean {\r\n let itemData = { x, y, w, h };\r\n\r\n if (this.options.direction !== 'horizontal') {\r\n itemData = { x: y, y: x, w: h, h: w };\r\n }\r\n\r\n for (let i = itemData.x; i < itemData.x + itemData.w; i++) {\r\n for (let j = itemData.y; j < itemData.y + itemData.h; j++) {\r\n if (\r\n this.grid[i] &&\r\n this.grid[i][j] &&\r\n this.grid[i][j] !== item &&\r\n !this.grid[i][j].dragAndDrop\r\n ) {\r\n return true;\r\n }\r\n }\r\n }\r\n return false;\r\n }\r\n\r\n checkItemAboveEmptyArea(\r\n item: GridListItem,\r\n newPosition: { x: number; y: number }\r\n ) {\r\n let itemData = {\r\n x: newPosition.x,\r\n y: newPosition.y,\r\n w: item.w,\r\n h: item.h\r\n };\r\n if (\r\n !item.itemPrototype &&\r\n item.x === newPosition.x &&\r\n item.y === newPosition.y\r\n ) {\r\n return true;\r\n }\r\n\r\n if (this.options.direction === 'horizontal') {\r\n itemData = {\r\n x: newPosition.y,\r\n y: newPosition.x,\r\n w: itemData.h,\r\n h: itemData.w\r\n };\r\n }\r\n return !this.checkItemsInArea(\r\n itemData.y,\r\n itemData.y + itemData.h - 1,\r\n itemData.x,\r\n itemData.x + itemData.w - 1,\r\n item\r\n );\r\n }\r\n\r\n fixItemsPositions(options: IGridsterOptions) {\r\n // items with x, y that fits gird with size of options.lanes\r\n const validItems = this.items\r\n .filter((item: GridListItem) => item.itemComponent)\r\n .filter((item: GridListItem) =>\r\n this.isItemValidForGrid(item, options)\r\n );\r\n // items that x, y must be generated\r\n const invalidItems = this.items\r\n .filter((item: GridListItem) => item.itemComponent)\r\n .filter(\r\n (item: GridListItem) => !this.isItemValidForGrid(item, options)\r\n );\r\n\r\n const gridList = new GridList([], options);\r\n\r\n // put items with defined positions to the grid\r\n gridList.items = validItems.map((item: GridListItem) => {\r\n return item.copyForBreakpoint(options.breakpoint);\r\n });\r\n\r\n gridList.generateGrid();\r\n\r\n invalidItems.forEach(item => {\r\n // TODO: check if this change does not broke anything\r\n // const itemCopy = item.copy();\r\n const itemCopy = item.copyForBreakpoint(options.breakpoint);\r\n const position = gridList.findPositionForItem(itemCopy, {\r\n x: 0,\r\n y: 0\r\n });\r\n\r\n gridList.items.push(itemCopy);\r\n gridList.setItemPosition(itemCopy, position);\r\n gridList.markItemPositionToGrid(itemCopy);\r\n });\r\n\r\n gridList.pullItemsToLeft();\r\n gridList.pushCollidingItems();\r\n\r\n this.items.forEach((itm: GridListItem) => {\r\n const cachedItem = gridList.items.filter(cachedItm => {\r\n return cachedItm.$element === itm.$element;\r\n })[0];\r\n\r\n itm.setValueX(cachedItem.x, options.breakpoint);\r\n itm.setValueY(cachedItem.y, options.breakpoint);\r\n itm.setValueW(cachedItem.w, options.breakpoint);\r\n itm.setValueH(cachedItem.h, options.breakpoint);\r\n itm.autoSize = cachedItem.autoSize;\r\n });\r\n }\r\n\r\n deleteItemPositionFromGrid(item: GridListItem) {\r\n const position = this.getItemPosition(item);\r\n let x, y;\r\n\r\n for (x = position.x; x < position.x + position.w; x++) {\r\n // It can happen to try to remove an item from a position not generated\r\n // in the grid, probably when loading a persisted grid of items. No need\r\n // to create a column to be able to remove something from it, though\r\n if (!this.grid[x]) {\r\n continue;\r\n }\r\n\r\n for (y = position.y; y < position.y + position.h; y++) {\r\n // Don't clear the cell if it's been occupied by a different widget in\r\n // the meantime (e.g. when an item has been moved over this one, and\r\n // thus by continuing to clear this item's previous position you would\r\n // cancel the first item's move, leaving it without any position even)\r\n if (this.grid[x][y] === item) {\r\n this.grid[x][y] = null;\r\n }\r\n }\r\n }\r\n }\r\n\r\n private isItemFloating(item) {\r\n if (item.itemComponent && item.itemComponent.isDragging) {\r\n return false;\r\n }\r\n const position = this.getItemPosition(item);\r\n\r\n if (position.x === 0) {\r\n return false;\r\n }\r\n const rowBelowItem = this.grid[position.x - 1];\r\n\r\n return (rowBelowItem || [])\r\n .slice(position.y, position.y + position.h)\r\n .reduce((isFloating, cellItem) => {\r\n return isFloating && !cellItem;\r\n }, true);\r\n }\r\n\r\n private isItemValidForGrid(item: GridListItem, options: IGridsterOptions) {\r\n const itemData =\r\n options.direction === 'horizontal'\r\n ? {\r\n x: item.getValueY(options.breakpoint),\r\n y: item.getValueX(options.breakpoint),\r\n w: item.getValueH(options.breakpoint),\r\n h: Math.min(\r\n item.getValueW(this.options.breakpoint),\r\n options.lanes\r\n )\r\n }\r\n : {\r\n x: item.getValueX(options.breakpoint),\r\n y: item.getValueY(options.breakpoint),\r\n w: Math.min(\r\n item.getValueW(this.options.breakpoint),\r\n options.lanes\r\n ),\r\n h: item.getValueH(options.breakpoint)\r\n };\r\n\r\n return (\r\n typeof itemData.x === 'number' &&\r\n typeof itemData.y === 'number' &&\r\n itemData.x + itemData.w <= options.lanes\r\n );\r\n }\r\n\r\n private findDefaultPositionHorizontal(width: number, height: number) {\r\n for (const col of this.grid) {\r\n const colIdx = this.grid.indexOf(col);\r\n let rowIdx = 0;\r\n while (rowIdx < col.length - height + 1) {\r\n if (\r\n !this.checkItemsInArea(\r\n colIdx,\r\n colIdx + width - 1,\r\n rowIdx,\r\n rowIdx + height - 1\r\n )\r\n ) {\r\n return [colIdx, rowIdx];\r\n }\r\n rowIdx++;\r\n }\r\n }\r\n return [this.grid.length, 0];\r\n }\r\n\r\n private findDefaultPositionVertical(width: number, height: number) {\r\n for (const row of this.grid) {\r\n const rowIdx = this.grid.indexOf(row);\r\n let colIdx = 0;\r\n while (colIdx < row.length - width + 1) {\r\n if (\r\n !this.checkItemsInArea(\r\n rowIdx,\r\n rowIdx + height - 1,\r\n colIdx,\r\n colIdx + width - 1\r\n )\r\n ) {\r\n return [colIdx, rowIdx];\r\n }\r\n colIdx++;\r\n }\r\n }\r\n return [0, this.grid.length];\r\n }\r\n\r\n private checkItemsInArea(\r\n rowStart: number,\r\n rowEnd: number,\r\n colStart: number,\r\n colEnd: number,\r\n item?: GridListItem\r\n ) {\r\n for (let i = rowStart; i <= rowEnd; i++) {\r\n for (let j = colStart; j <= colEnd; j++) {\r\n if (\r\n this.grid[i] &&\r\n this.grid[i][j] &&\r\n (item ? this.grid[i][j] !== item : true)\r\n ) {\r\n return true;\r\n }\r\n }\r\n }\r\n return false;\r\n }\r\n\r\n private sortItemsByPosition() {\r\n this.items.sort((item1, item2) => {\r\n const position1 = this.getItemPosition(item1),\r\n position2 = this.getItemPosition(item2);\r\n\r\n // Try to preserve columns.\r\n if (position1.x !== position2.x) {\r\n return position1.x - position2.x;\r\n }\r\n\r\n if (position1.y !== position2.y) {\r\n return position1.y - position2.y;\r\n }\r\n\r\n // The items are placed on the same position.\r\n return 0;\r\n });\r\n }\r\n\r\n /**\r\n * Some items can have 100% height or 100% width. Those dimmensions are\r\n * expressed as 0. We need to ensure a valid width and height for each of\r\n * those items as the number of items per lane.\r\n */\r\n private adjustSizeOfItems() {\r\n for (let i = 0; i < this.items.length; i++) {\r\n const item = this.items[i];\r\n\r\n // This can happen only the first time items are checked.\r\n // We need the property to have a value for all the items so that the\r\n // `cloneItems` method will merge the properties properly. If we only set\r\n // it to the items that need it then the following can happen:\r\n //\r\n // cloneItems([{id: 1, autoSize: true}, {id: 2}],\r\n // [{id: 2}, {id: 1, autoSize: true}]);\r\n //\r\n // will result in\r\n //\r\n // [{id: 1, autoSize: true}, {id: 2, autoSize: true}]\r\n if (item.autoSize === undefined) {\r\n item.autoSize = item.w === 0 || item.h === 0;\r\n }\r\n\r\n if (item.autoSize) {\r\n if (this.options.direction === 'horizontal') {\r\n item.h = this.options.lanes;\r\n } else {\r\n item.w = this.options.lanes;\r\n }\r\n }\r\n }\r\n }\r\n\r\n private resetGrid() {\r\n this.grid = [];\r\n }\r\n\r\n /**\r\n * Check that an item wouldn't overlap with another one if placed at a\r\n * certain position within the grid\r\n */\r\n private itemFitsAtPosition(item: GridListItem, newPosition) {\r\n const position = this.getItemPosition(item);\r\n let x, y;\r\n\r\n // No coordonate can be negative\r\n if (newPosition[0] < 0 || newPosition[1] < 0) {\r\n return false;\r\n }\r\n\r\n // Make sure the item isn't larger than the entire grid\r\n if (\r\n newPosition[1] + Math.min(position.h, this.options.lanes) >\r\n this.options.lanes\r\n ) {\r\n return false;\r\n }\r\n\r\n if (this.isOverFixedArea(item.x, item.y, item.w, item.h)) {\r\n return false;\r\n }\r\n\r\n // Make sure the position doesn't overlap with an already positioned\r\n // item.\r\n for (x = newPosition[0]; x < newPosition[0] + position.w; x++) {\r\n const col = this.grid[x];\r\n // Surely a column that hasn't even been created yet is available\r\n if (!col) {\r\n continue;\r\n }\r\n\r\n for (y = newPosition[1]; y < newPosition[1] + position.h; y++) {\r\n // Any space occupied by an item can continue to be occupied by the\r\n // same item.\r\n if (col[y] && col[y] !== item) {\r\n return false;\r\n }\r\n }\r\n }\r\n\r\n return true;\r\n }\r\n\r\n private updateItemPosition(item: GridListItem, position: Array<any>) {\r\n if (item.x !== null && item.y !== null) {\r\n this.deleteItemPositionFromGrid(item);\r\n }\r\n\r\n this.setItemPosition(item, position);\r\n\r\n this.markItemPositionToGrid(item);\r\n }\r\n\r\n /**\r\n * @param Object item A reference to a grid item.\r\n * @param number width The new width.\r\n * @param number height The new height.\r\n */\r\n private updateItemSize(item: GridListItem, width, height) {\r\n if (item.x !== null && item.y !== null) {\r\n this.deleteItemPositionFromGrid(item);\r\n }\r\n\r\n item.w = width;\r\n item.h = height;\r\n\r\n this.markItemPositionToGrid(item);\r\n }\r\n\r\n /**\r\n * Mark the grid cells that are occupied by an item. This prevents items\r\n * from overlapping in the grid\r\n */\r\n private markItemPositionToGrid(item: GridListItem) {\r\n const position = this.getItemPosition(item);\r\n let x, y;\r\n\r\n // Ensure that the grid has enough columns to accomodate the current item.\r\n this.ensureColumns(position.x + position.w);\r\n\r\n for (x = position.x; x < position.x + position.w; x++) {\r\n for (y = position.y; y < position.y + position.h; y++) {\r\n this.grid[x][y] = item;\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Ensure that the grid has at least N columns available.\r\n */\r\n private ensureColumns(N) {\r\n for (let i = 0; i < N; i++) {\r\n if (!this.grid[i]) {\r\n this.grid.push(new GridCol(this.options.lanes));\r\n }\r\n }\r\n }\r\n\r\n private getItemsCollidingWithItem(item: GridListItem): number[] {\r\n const collidingItems = [];\r\n for (let i = 0; i < this.items.length; i++) {\r\n if (\r\n item !== this.items[i] &&\r\n this.itemsAreColliding(item, this.items[i])\r\n ) {\r\n collidingItems.push(i);\r\n }\r\n }\r\n return collidingItems;\r\n }\r\n\r\n private itemsAreColliding(item1: GridListItem, item2: GridListItem) {\r\n const position1 = this.getItemPosition(item1),\r\n position2 = this.getItemPosition(item2);\r\n\r\n return !(\r\n position2.x >= position1.x + position1.w ||\r\n position2.x + position2.w <= position1.x ||\r\n position2.y >= position1.y + position1.h ||\r\n position2.y + position2.h <= position1.y\r\n );\r\n }\r\n\r\n /**\r\n * Attempt to resolve the collisions after moving an item over one or more\r\n * other items within the grid, by shifting the position of the colliding\r\n * items around the moving one. This might result in subsequent collisions,\r\n * in which case we will revert all position permutations. To be able to\r\n * revert to the initial item positions, we create a virtual grid in the\r\n * process\r\n */\r\n private tryToResolveCollisionsLocally(item: GridListItem) {\r\n const collidingItems = this.getItemsCollidingWithItem(item);\r\n if (!collidingItems.length) {\r\n return true;\r\n }\r\n\r\n const _gridList = new GridList(\r\n this.items.map(itm => {\r\n return itm.copy();\r\n }),\r\n this.options\r\n );\r\n\r\n let leftOfItem;\r\n let rightOfItem;\r\n let aboveOfItem;\r\n let belowOfItem;\r\n\r\n for (let i = 0; i < collidingItems.length; i++) {\r\n const collidingItem = _gridList.items[collidingItems[i]],\r\n collidingPosition = this.getItemPosition(collidingItem);\r\n\r\n // We use a simple algorithm for moving items around when collisions occur:\r\n // In this prioritized order, we try to move a colliding item around the\r\n // moving one:\r\n // 1. to its left side\r\n // 2. above it\r\n // 3. under it\r\n // 4. to its right side\r\n const position = this.getItemPosition(item);\r\n\r\n leftOfItem = [\r\n position.x - collidingPosition.w,\r\n collidingPosition.y\r\n ];\r\n rightOfItem = [position.x + position.w, collidingPosition.y];\r\n aboveOfItem = [\r\n collidingPosition.x,\r\n position.y - collidingPosition.h\r\n ];\r\n belowOfItem = [collidingPosition.x, position.y + position.h];\r\n\r\n if (_gridList.itemFitsAtPosition(collidingItem, leftOfItem)) {\r\n _gridList.updateItemPosition(collidingItem, leftOfItem);\r\n } else if (\r\n _gridList.itemFitsAtPosition(collidingItem, aboveOfItem)\r\n ) {\r\n _gridList.updateItemPosition(collidingItem, aboveOfItem);\r\n } else if (\r\n _gridList.itemFitsAtPosition(collidingItem, belowOfItem)\r\n ) {\r\n _gridList.updateItemPosition(collidingItem, belowOfItem);\r\n } else if (\r\n _gridList.itemFitsAtPosition(collidingItem, rightOfItem)\r\n ) {\r\n _gridList.updateItemPosition(collidingItem, rightOfItem);\r\n } else {\r\n // Collisions failed, we must use the pullItemsToLeft method to arrange\r\n // the other items around this item with fixed position. This is our\r\n // plan B for when local collision resolving fails.\r\n return false;\r\n }\r\n }\r\n // If we reached this point it means we managed to resolve the collisions\r\n // from one single iteration, just by moving the colliding items around. So\r\n // we accept this scenario and merge the branched-out grid instance into the\r\n // original one\r\n\r\n this.items.forEach((itm: GridListItem, idx: number) => {\r\n const cachedItem = _gridList.items.filter(cachedItm => {\r\n return cachedItm.$element === itm.$element;\r\n })[0];\r\n\r\n itm.x = cachedItem.x;\r\n itm.y = cachedItem.y;\r\n itm.w = cachedItem.w;\r\n itm.h = cachedItem.h;\r\n itm.autoSize = cachedItem.autoSize;\r\n });\r\n this.generateGrid();\r\n return true;\r\n }\r\n\r\n /**\r\n * When pulling items to the left, we need to find the leftmost position for\r\n * an item, with two considerations in mind:\r\n * - preserving its current row\r\n * - preserving the previous horizontal order between items\r\n */\r\n private findLeftMostPositionForItem(item) {\r\n let tail = 0;\r\n const position = this.getItemPosition(item);\r\n\r\n for (let i = 0; i < this.grid.length; i++) {\r\n for (let j = position.y; j < position.y + position.h; j++) {\r\n const otherItem = this.grid[i][j];\r\n\r\n if (!otherItem) {\r\n continue;\r\n }\r\n\r\n const otherPosition = this.getItemPosition(otherItem);\r\n\r\n if (this.items.indexOf(otherItem) < this.items.indexOf(item)) {\r\n tail = otherPosition.x + otherPosition.w;\r\n }\r\n }\r\n }\r\n\r\n return tail;\r\n }\r\n\r\n private findItemByPosition(x: number, y: number): GridListItem {\r\n for (let i = 0; i < this.items.length; i++) {\r\n if (this.items[i].x === x && this.items[i].y === y) {\r\n return this.items[i];\r\n }\r\n }\r\n }\r\n\r\n private getItemByAttribute(key, value) {\r\n for (let i = 0; i < this.items.length; i++) {\r\n if (this.items[i][key] === value) {\r\n return this.items[i];\r\n }\r\n }\r\n return null;\r\n }\r\n\r\n private padNumber(nr, prefix) {\r\n // Currently works for 2-digit numbers (<100)\r\n return nr >= 10 ? nr : prefix + nr;\r\n }\r\n\r\n /**\r\n * If the direction is vertical we need to rotate the grid 90 deg to the\r\n * left. Thus, we simulate the fact that items are being pulled to the top.\r\n *\r\n * Since the items have widths and heights, if we apply the classic\r\n * counter-clockwise 90 deg rotation\r\n *\r\n * [0 -1]\r\n * [1 0]\r\n *\r\n * then the top left point of an item will become the bottom left point of\r\n * the rotated item. To adjust for this, we need to subtract from the y\r\n * position the height of the original item - the width of the rotated item.\r\n *\r\n * However, if we do this then we'll reverse some actions: resizing the\r\n * width of an item will stretch the item to the left instead of to the\r\n * right; resizing an item that doesn't fit into the grid will push the\r\n * items around it instead of going on a new row, etc.\r\n *\r\n * We found it better to do a vertical flip of the grid after rotating it.\r\n * This restores the direction of the actions and greatly simplifies the\r\n * transformations.\r\n */\r\n private getItemPosition(item: any) {\r\n if (this.options.direction === 'horizontal') {\r\n return item;\r\n } else {\r\n return {\r\n x: item.y,\r\n y: item.x,\r\n w: item.h,\r\n h: item.w\r\n };\r\n }\r\n }\r\n\r\n /**\r\n * See getItemPosition.\r\n */\r\n private setItemPosition(item, position) {\r\n if (this.options.direction === 'horizontal') {\r\n item.x = position[0];\r\n item.y = position[1];\r\n } else {\r\n // We're supposed to subtract the rotated item's height which is actually\r\n // the non-rotated item's width.\r\n item.x = position[1];\r\n item.y = position[0];\r\n }\r\n }\r\n}\r\n","import { Injectable } from '@angular/core';\r\nimport { Subject } from 'rxjs';\r\nimport { debounceTime } from 'rxjs/operators';\r\n\r\nimport { GridList } from './gridList/gridList';\r\nimport { IGridsterOptions } from './IGridsterOptions';\r\nimport { IGridsterDraggableOptions } from './IGridsterDraggableOptions';\r\nimport { GridListItem } from './gridList/GridListItem';\r\nimport { GridsterComponent } from './gridster.component';\r\nimport { GridsterOptions } from './GridsterOptions';\r\n\r\n@Injectable()\r\nexport class GridsterService {\r\n $element: HTMLElement;\r\n\r\n gridList: GridList;\r\n\r\n items: Array<GridListItem> = [];\r\n _items: Array<GridListItem> = [];\r\n _itemsMap: { [breakpoint: string]: Array<GridListItem> } = {};\r\n disabledItems: Array<GridListItem> = [];\r\n\r\n options: IGridsterOptions;\r\n draggableOptions: IGridsterDraggableOptions;\r\n\r\n gridsterRect: ClientRect;\r\n gridsterScrollData: { scrollTop: number, scrollLeft: number };\r\n\r\n gridsterOptions: GridsterOptions;\r\n\r\n gridsterComponent: GridsterComponent;\r\n\r\n debounceRenderSubject = new Subject();\r\n\r\n public $positionHighlight: HTMLElement;\r\n\r\n public maxItemWidth: number;\r\n public maxItemHeight: number;\r\n\r\n public cellWidth: number;\r\n public cellHeight: number;\r\n\r\n public itemRemoveSubject: Subject<GridListItem> = new Subject();\r\n\r\n private _fontSize: number;\r\n\r\n private previousDragPosition: Array<number>;\r\n private previousDragSize: Array<number>;\r\n\r\n private currentElement: HTMLElement;\r\n\r\n private _maxGridCols: number;\r\n\r\n private isInit = false;\r\n\r\n constructor() {\r\n this.itemRemoveSubject.pipe(debounceTime(0)).subscribe(() => {\r\n this.gridList.pullItemsToLeft();\r\n this.render();\r\n this.updateCachedItems();\r\n });\r\n\r\n this.debounceRenderSubject.pipe(debounceTime(0)).subscribe(() => this.render());\r\n }\r\n\r\n isInitialized(): boolean {\r\n return this.isInit;\r\n }\r\n\r\n /**\r\n * Must be called before init\r\n * @param item\r\n */\r\n registerItem(item: GridListItem) {\r\n\r\n this.items.push(item);\r\n return item;\r\n }\r\n\r\n init(gridsterComponent: GridsterComponent) {\r\n\r\n this.gridsterComponent = gridsterComponent;\r\n\r\n this.draggableOptions = gridsterComponent.draggableOptions;\r\n\r\n this.gridsterOptions = gridsterComponent.gridsterOptions;\r\n }\r\n\r\n start() {\r\n this.updateMaxItemSize();\r\n\r\n // Used to highlight a position an element will land on upon drop\r\n if (this.$positionHighlight) {\r\n this.removePositionHighlight();\r\n }\r\n\r\n this.initGridList();\r\n\r\n this.isInit = true;\r\n\r\n setTimeout(() => {\r\n this.copyItems();\r\n this.fixItemsPositions();\r\n\r\n this.gridsterComponent.reflowGridster(true);\r\n this.gridsterComponent.setReady();\r\n });\r\n }\r\n\r\n initGridList() {\r\n // Create instance of GridList (decoupled lib for handling the grid\r\n // positioning and sorting post-drag and dropping)\r\n this.gridList = new GridList(this.items, this.options);\r\n }\r\n\r\n render() {\r\n this.updateMaxItemSize();\r\n this.gridList.generateGrid();\r\n this.applySizeToItems();\r\n this.applyPositionToItems();\r\n this.refreshLines();\r\n }\r\n\r\n reflow() {\r\n this.calculateCellSize();\r\n this.render();\r\n }\r\n\r\n fixItemsPositions() {\r\n if (this.options.responsiveSizes) {\r\n this.gridList.fixItemsPositions(this.options);\r\n } else {\r\n this.gridList.fixItemsPositions(this.gridsterOptions.basicOptions);\r\n this.gridsterOptions.responsiveOptions.forEach((options: IGridsterOptions) => {\r\n this.gridList.fixItemsPositions(options);\r\n });\r\n }\r\n\r\n this.updateCachedItems();\r\n }\r\n\r\n removeItem(item: GridListItem) {\r\n const idx = this.items.indexOf(item);\r\n\r\n if (idx >= 0) {\r\n this.items.splice(this.items.indexOf(item), 1);\r\n }\r\n\r\n this.gridList.deleteItemPositionFromGrid(item);\r\n this.removeItemFromCache(item);\r\n }\r\n\r\n onResizeStart(item: GridListItem) {\r\n this.currentElement = item.$element;\r\n\r\n this.copyItems();\r\n\r\n this._maxGridCols = this.gridList.grid.length;\r\n\r\n this.highlightPositionForItem(item);\r\n\r\n this.gridsterComponent.isResizing = true;\r\n\r\n this.refreshLines();\r\n }\r\n\r\n onResizeDrag(item: GridListItem) {\r\n const newSize = this.snapItemSizeToGrid(item);\r\n const sizeChanged = this.dragSizeChanged(newSize);\r\n const newPosition = this.snapItemPositionToGrid(item);\r\n const positionChanged = this.dragPositionChanged(newPosition);\r\n\r\n if (sizeChanged || positionChanged) {\r\n // Regenerate the grid with the positions from when the drag started\r\n this.restoreCachedItems();\r\n this.gridList.generateGrid();\r\n\r\n this.previousDragPosition = newPosition;\r\n this.previousDragSize = newSize;\r\n\r\n this.gridList.moveAndResize(item, newPosition, {w: newSize[0], h: newSize[1]});\r\n\r\n // Visually update item positions and highlight shape\r\n this.applyPositionToItems(true);\r\n this.highlightPositionForItem(item);\r\n }\r\n }\r\n\r\n onResizeStop(item: GridListItem) {\r\n this.currentElement = undefined;\r\n this.updateCachedItems();\r\n this.previousDragSize = null;\r\n\r\n this.removePositionHighlight();\r\n\r\n this.gridsterComponent.isResizing = false;\r\n\r\n this.gridList.pullItemsToLeft(item);\r\n this.debounceRenderSubject.next(null);\r\n\r\n this.fixItemsPositions();\r\n }\r\n\r\n onStart(item: GridListItem) {\r\n this.currentElement = item.$element;\r\n // itemCtrl.isDragging = true;\r\n // Create a deep copy of the items; we use them to revert the item\r\n // positions after each drag change, making an entire drag operation less\r\n // distru