├── .editorconfig ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── angular.json ├── demo-dashboard ├── README.md ├── app │ ├── app.component.ts │ ├── app.css │ ├── app.html │ └── boot.ts ├── index.html ├── package.json ├── style.css └── tsconfig.json ├── e2e ├── app.e2e-spec.ts ├── app.po.ts └── tsconfig.e2e.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── projects └── angular2-grid │ ├── karma.conf.js │ ├── ng-package.json │ ├── ng-package.prod.json │ ├── package.json │ ├── src │ ├── NgGrid.css │ ├── NgGrid_FixSmall.css │ ├── components │ │ └── NgGridPlaceholder.ts │ ├── directives │ │ ├── NgGrid.ts │ │ └── NgGridItem.ts │ ├── helpers │ │ └── NgGridHelpers.ts │ ├── interfaces │ │ └── INgGrid.ts │ ├── main.ts │ ├── modules │ │ └── NgGrid.module.ts │ ├── public_api.ts │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── tslint.json ├── protractor.conf.js ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ └── app.module.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── index.html ├── main.ts ├── polyfills.ts ├── styles.css ├── test.ts ├── tsconfig.app.json └── tsconfig.spec.json ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # IDEs and editors 11 | /.idea 12 | .project 13 | .classpath 14 | .c9/ 15 | *.launch 16 | .settings/ 17 | 18 | # IDE - VSCode 19 | .vscode/* 20 | !.vscode/settings.json 21 | !.vscode/tasks.json 22 | !.vscode/launch.json 23 | !.vscode/extensions.json 24 | 25 | # misc 26 | /.sass-cache 27 | /connect.lock 28 | /coverage/* 29 | /libpeerconnection.log 30 | npm-debug.log 31 | testem.log 32 | /typings 33 | 34 | # e2e 35 | /e2e/*.js 36 | /e2e/*.map 37 | 38 | #System Files 39 | .DS_Store 40 | Thumbs.db 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [2.3.1](https://github.com/BTMorton/angular2-grid/compare/v2.3.0...v2.3.1) - 2018-07-17 8 | ### Changed 9 | - Exposed NgConfigFixDirection interface ([#294](https://github.com/BTMorton/angular2-grid/issues/294)) 10 | - Update Angular peer dependecy to support Angular 5 ([#292](https://github.com/BTMorton/angular2-grid/issues/292)) 11 | 12 | ## [2.3.0](https://github.com/BTMorton/angular2-grid/compare/v2.2.4...v2.3.0) - 2018-07-14 13 | ### Added 14 | - Configuration of available resize directions ([#290](https://github.com/BTMorton/angular2-grid/pull/290)) 15 | - Item overlap support ([#238](https://github.com/BTMorton/angular2-grid/issues/238)) 16 | 17 | ## [2.2.4](https://github.com/BTMorton/angular2-grid/compare/v2.2.3...v2.2.4) - 2018-07-14 18 | ### Fixed 19 | - Error in fix position logic ([#289](https://github.com/BTMorton/angular2-grid/issues/289)) 20 | 21 | ## [2.2.3](https://github.com/BTMorton/angular2-grid/compare/v2.2.1...v2.2.3) - 2018-06-11 22 | (Version 2.2.2 was mispublished and did not include any changes) 23 | ### Added 24 | - Code linting 25 | 26 | ## [2.2.1](https://github.com/BTMorton/angular2-grid/compare/v2.2.0...v2.2.1) - 2018-06-10 27 | ### Fixed 28 | - Remove debug logging 29 | 30 | ## [2.2.0](https://github.com/BTMorton/angular2-grid/compare/v2.1.0...v2.2.0) - 2018-06-10 31 | ### Added 32 | - Configuration to specify directions to fix item positions 33 | - Top/left resizing for items ([#191](https://github.com/BTMorton/angular2-grid/issues/191)) 34 | 35 | ### Changed 36 | - Handling of items in the grid rework, removing the item grid and replacing it with a set of items currently in the grid ([#267](https://github.com/BTMorton/angular2-grid/pull/267)) 37 | - Overhaul algorithm for fixing item positions ([#264](https://github.com/BTMorton/angular2-grid/issues/264)) 38 | - Only listen to mouse events when configuration requires it ([#287](https://github.com/BTMorton/angular2-grid/pull/287)) 39 | 40 | ### Fixed 41 | - Change detection of user configuration 42 | - Resize detection on item extremities ([#282](https://github.com/BTMorton/angular2-grid/issues/282)) 43 | 44 | ## [2.1.0](https://github.com/BTMorton/angular2-grid/compare/v2.0.7...v2.1.0) - 2017-10-22 45 | ### Added 46 | - Option to center items when using `limit_to_screen` ([#261](https://github.com/BTMorton/angular2-grid/pull/261)) 47 | 48 | ## [2.0.7](https://github.com/BTMorton/angular2-grid/compare/v2.0.6...v2.0.7) - 2017-09-01 49 | ### Added 50 | - Config option to allow calculation max rows by element size ([#250](https://github.com/BTMorton/angular2-grid/issues/250)) 51 | 52 | ### Changed 53 | - Update usage steps in Readme ([#248](https://github.com/BTMorton/angular2-grid/pull/248)) 54 | 55 | ### Fixes 56 | - Use explicit exports for better compiler support ([#257](https://github.com/BTMorton/angular2-grid/pull/257)) 57 | - Add a check to prevent items being resized larger than the available columns ([#258](https://github.com/BTMorton/angular2-grid/pull/258)) 58 | - Improve handling of oversized items ([#260](https://github.com/BTMorton/angular2-grid/issues/260)) 59 | - Improve calcuation of drag and resize areas ([#245](https://github.com/BTMorton/angular2-grid/issues/245)) 60 | 61 | ## [2.0.6](https://github.com/BTMorton/angular2-grid/compare/v2.0.5...v2.0.6) - 2017-06-28 62 | ### Fixes 63 | - Add verification of item positions when calculating positions 64 | 65 | ## [2.0.5](https://github.com/BTMorton/angular2-grid/compare/v2.0.4...v2.0.5) - 2017-06-25 66 | ### Changed 67 | - Update peer dependencies to support Angular v4 68 | 69 | ## [2.0.4](https://github.com/BTMorton/angular2-grid/compare/v2.0.3...v2.0.4) - 2017-06-25 70 | ### Added 71 | - Add public method to trigger a resize 72 | 73 | ### Changed 74 | - Use absolute positioning instead of css transform ([#241](https://github.com/BTMorton/angular2-grid/issues/241)) 75 | 76 | ### Fixed 77 | - Revert a bug caused by a change when removing items ([#242](https://github.com/BTMorton/angular2-grid/issues/242)) 78 | - Fix Firefox dragHandle issue ([#225](https://github.com/BTMorton/angular2-grid/issues/225)) 79 | - Remove debug logging 80 | 81 | ## [2.0.3](https://github.com/BTMorton/angular2-grid/compare/v2.0.2...v2.0.3) - 2017-06-03 82 | ### Changed 83 | - Use [ngGrid] binding instead of [ng-grid] in readme ([#205](https://github.com/BTMorton/angular2-grid/pull/205)) 84 | 85 | ### Fixed 86 | - Several issues with `limit_to_screen` property 87 | 88 | ## [2.0.2](https://github.com/BTMorton/angular2-grid/compare/v2.0.1...v2.0.2) - 2017-03-05 89 | ### Added 90 | - Gitter link in README ([#198](https://github.com/BTMorton/angular2-grid/issues/198)) 91 | - Change log ([#195](https://github.com/BTMorton/angular2-grid/issues/195)) 92 | 93 | ### Fixed 94 | - Null item bug in dragStart ([#208](https://github.com/BTMorton/angular2-grid/issues/208)) 95 | - Touch events freezing on iDevices ([#190](https://github.com/BTMorton/angular2-grid/issues/190)) 96 | - `limit_to_screen` property not being honoured when adding new items ([#199](https://github.com/BTMorton/angular2-grid/issues/199)) 97 | 98 | ## [2.0.1](https://github.com/BTMorton/angular2-grid/compare/v1.1.0...v2.0.1) - 2017-01-26 99 | ### Changed 100 | - Distributed bundle files 101 | - Build process 102 | 103 | ## [1.0.1](https://github.com/BTMorton/angular2-grid/compare/v1.0.0...v1.0.1) - 2017-01-24 [YANKED] 104 | ### Added 105 | - Additional demo page ([#177](https://github.com/BTMorton/angular2-grid/pull/177)) 106 | 107 | ### Changed 108 | - Updated readme and demo 109 | - Include margin values for row height calculations ([#174](https://github.com/BTMorton/angular2-grid/pull/174)) 110 | - Make host methods public ([#176](https://github.com/BTMorton/angular2-grid/pull/176)) 111 | 112 | ### Fixed 113 | - Null item bug in resizeStart ([#185](https://github.com/BTMorton/angular2-grid/pull/185)) 114 | 115 | ## [1.0.0](https://github.com/BTMorton/angular2-grid/compare/v0.11.2...v1.0.0) - 2015-01-24 [YANKED] 116 | ### Changed 117 | - Distributed bundle files 118 | 119 | ## [0.11.2](https://github.com/BTMorton/angular2-grid/compare/v0.11.1...v0.11.2) - 2016-11-25 120 | ### Fixed 121 | - Errors caused by debugging in old commit ([#164](https://github.com/BTMorton/angular2-grid/issues/164)) 122 | 123 | ## [0.11.1](https://github.com/BTMorton/angular2-grid/compare/v0.11.0...v0.11.1) - 2016-11-22 124 | ### Changed 125 | - Item change events now also trigger on add item and remove item ([#134](https://github.com/BTMorton/angular2-grid/issues/134)) 126 | - Reduced default item drag/resize border size from 50 to 25 127 | 128 | ### Fixed 129 | - Dragging in safari ([#137](https://github.com/BTMorton/angular2-grid/issues/137)) 130 | 131 | ## [0.11.0](https://github.com/BTMorton/angular2-grid/compare/v0.10.0...v0.11.0) - 2016-11-17 132 | ### Changed 133 | - Updated to Angular version 2.0.0 134 | 135 | ## [0.10.0](https://github.com/BTMorton/angular2-grid/compare/v0.9.1...v0.10.0) - 2016-09-13 136 | 137 | ## [0.9.1](https://github.com/BTMorton/angular2-grid/compare/v0.9.0...v0.9.1) - 2016-07-30 138 | 139 | ## [0.9.0](https://github.com/BTMorton/angular2-grid/compare/v0.8.3....v0.9.0) - 2016-07-25 140 | 141 | 142 | // TODO: Complete changelog with full history 143 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ben Morton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub version](http://img.shields.io/github/release/BTMorton%2Fangular2-grid.svg)](https://github.com/BTMorton/angular2-grid) 2 | [![npm version](http://img.shields.io/npm/v/angular2-grid.svg)](https://www.npmjs.com/package/angular2-grid) 3 | [![bower version](http://img.shields.io/bower/v/angular2-grid.svg)](https://libraries.io/bower/angular2-grid) 4 | [![license](http://img.shields.io/github/license/BTMorton%2Fangular2-grid.svg)](https://github.com/BTMorton/angular2-grid/blob/master/LICENSE) 5 | [![open issues](http://img.shields.io/github/issues/BTMorton%2Fangular2-grid.svg)](https://github.com/BTMorton/angular2-grid/issues) 6 | [![Chat on Gitter](https://img.shields.io/gitter/room/angular2grid/lobby.svg)](https://gitter.im/angular2grid/Lobby) 7 | 8 | # Angular 2 Grid 9 | Angular 2 grid is a drag/drop/resize grid-based plugin directive for [Angular 2](http://angular.io). 10 | The demo included in this repo follows the [Angular 2 quick start](https://angular.io/docs/js/latest/quickstart.html) 11 | 12 | #### Setup 13 | ---------- 14 | 15 | To use the Angular 2 Grid system, simply run `npm install angular2-grid` and then include the `NgGridModule` in your project module imports (see Example for more details). 16 | 17 | If you want to help with development or try the demo, it's less simple, but not hard. First you'll need to install [Node](http://nodejs.org) and check out a copy of the repo. Then run: 18 | 19 | ```shell 20 | $ npm install 21 | $ typings install 22 | $ gulp build 23 | ``` 24 | 25 | This will give you a fully compiled version of the demo that you can run using the HTTP server of your choice. 26 | 27 | You can also use `gulp watch` to compile the demo and have gulp watch for any changes. 28 | 29 | NOTE: By default Angular 2 and System.js are not listed as actual dependencies, but as peer dependencies, so that npm doesn't install them on systems that just require the install file. If they are not installed, this could cause gulp to break. To fix this, run `npm install angular2 systemjs` and rerun the build command. 30 | 31 | #### Config 32 | ----------- 33 | 34 | To use this in your own application, all you need to do is add the `[ngGrid]` attribute to your container element and `[ngGridItem]` to each item. You can use this in conjunction with `NgFor` to create a truly dynamic angular grid. 35 | 36 | To configure the grid with your own options, it is as easy as adding them as the attribute value. The defaults for the grid are: 37 | 38 | ```javascript 39 | { 40 | 'margins': [10], // The size of the margins of each item. Supports up to four values in the same way as CSS margins. Can be updated using setMargins() 41 | 'draggable': true, // Whether the items can be dragged. Can be updated using enableDrag()/disableDrag() 42 | 'resizable': true, // Whether the items can be resized. Can be updated using enableResize()/disableResize() 43 | 'max_cols': 0, // The maximum number of columns allowed. Set to 0 for infinite. Cannot be used with max_rows 44 | 'max_rows': 0, // The maximum number of rows allowed. Set to 0 for infinite. Cannot be used with max_cols 45 | 'visible_cols': 0, // The number of columns shown on screen when auto_resize is set to true. Set to 0 to not auto_resize. Will be overriden by max_cols 46 | 'visible_rows': 0, // The number of rows shown on screen when auto_resize is set to true. Set to 0 to not auto_resize. Will be overriden by max_rows 47 | 'min_cols': 0, // The minimum number of columns allowed. Can be any number greater than or equal to 1. 48 | 'min_rows': 0, // The minimum number of rows allowed. Can be any number greater than or equal to 1. 49 | 'col_width': 250, // The width of each column 50 | 'row_height': 250, // The height of each row 51 | 'cascade': 'up', // The direction to cascade grid items ('up', 'right', 'down', 'left') 52 | 'min_width': 100, // The minimum width of an item. If greater than col_width, this will update the value of min_cols 53 | 'min_height': 100, // The minimum height of an item. If greater than row_height, this will update the value of min_rows 54 | 'fix_to_grid': false, // Fix all item movements to the grid 55 | 'auto_style': true, // Automatically add required element styles at run-time 56 | 'auto_resize': false, // Automatically set col_width/row_height so that max_cols/max_rows fills the screen. Only has effect is max_cols or max_rows is set 57 | 'maintain_ratio': false, // Attempts to maintain aspect ratio based on the colWidth/rowHeight values set in the config 58 | 'prefer_new': false, // When adding new items, will use that items position ahead of existing items 59 | 'limit_to_screen': false, // When resizing the screen, with this true and auto_resize false, the grid will re-arrange to fit the screen size. Please note, at present this only works with cascade direction up. 60 | 'center_to_screen': false, // When resizing the screen, with this true and limit_to_screen true, the grid will center itself to the screen if max columns width is smaller than the grid width. 61 | 'resize_directions': [ // The enabled resize directions in priority order 62 | "bottomright", // If two directions overlap, e.g. bottomright and right, the direction that appears first in this list will be used 63 | "bottomleft", 64 | "topright", 65 | "topleft", 66 | "right", 67 | "left", 68 | "bottom", 69 | "top" 70 | ], 71 | } 72 | ``` 73 | 74 | The defaults for the grid item are: 75 | 76 | ```javascript 77 | { 78 | 'col': 1, // The start column for the item 79 | 'row': 1, // The start row for the item 80 | 'sizex': 1, // The start width in terms of columns for the item 81 | 'sizey': 1, // The start height in terms of rows for the item 82 | 'dragHandle': null, // The selector to be used for the drag handle. If null, uses the whole item 83 | 'resizeHandle': null, // The selector to be used for the resize handle. If null, uses 'borderSize' pixels from the right for horizontal resize, 84 | // 'borderSize' pixels from the bottom for vertical, and the square in the corner bottom-right for both 85 | 'borderSize': 15, 86 | 'fixed': false, // If the grid item should be cascaded or not. If yes, manual movement is required 87 | 'draggable': true, // If the grid item can be dragged. If this or the global setting is set to false, the item cannot be dragged. 88 | 'resizable': true, // If the grid item can be resized. If this or the global setting is set to false, the item cannot be resized. 89 | 'payload': null, // An optional custom payload (string/number/object) to be used to identify the item for serialization 90 | 'maxCols': 0, // The maximum number of columns for a particular item. This value will only override the value from the grid (if set) if it is smaller 91 | 'minCols': 0, // The minimum number of columns for a particular item. This value will only override the value from the grid if larger 92 | 'maxRows': 0, // The maximum number of rows for a particular item. This value will only override the value from the grid (if set) if it is smaller 93 | 'minRows': 0, // The minimum number of rows for a particular item. This value will only override the value from the grid if larger 94 | 'minWidth': 0, // The minimum width of a particular item. This value will override the value from the grid, as well as the minimum columns if the resulting size is larger 95 | 'minHeight': 0, // The minimum height of a particular item. This value will override the value from the grid, as well as the minimum rows if the resulting size is larger 96 | 'resizeDirections': null, // The enabled resize directions in priority order. If null, uses the 'resize_directions' for the grid 97 | } 98 | ``` 99 | 100 | #### Event Handling 101 | ------------------- 102 | 103 | Both the `NgGrid` and `NgGridItem` throw events when an item is moved or resized. The grid has the following: 104 | 105 | ```javascript 106 | onDragStart(item) // When an item starts being dragged. Returns reference to corresponding NgGridItem 107 | onDrag(item) // When an item moves while dragging. Returns reference to corresponding NgGridItem 108 | onDragStop(item) // When an item stops being dragged. Returns reference to corresponding NgGridItem 109 | onResizeStart(item) // When an item starts being resized. Returns reference to corresponding NgGridItem 110 | onResize(item) // When an item is resized. Returns reference to corresponding NgGridItem 111 | onResizeStop(item) // When an item stops being resized. Returns reference to corresponding NgGridItem 112 | onItemChange(items) // When any item stops being dragged or resized. Returns an array of NgGridItemEvents in the order in which each item was added to the grid 113 | ``` 114 | 115 | The individual items will also throw the following events: 116 | 117 | ```javascript 118 | onDragStart() // When the item starts being dragged. 119 | onDrag() // When the item moves while dragging. 120 | onDragStop() // When the item stops being dragged. 121 | onDragAny() // When the item starts/stops/is being dragged. 122 | onResizeStart() // When the item starts being resized. 123 | onResize() // When the item is resized. 124 | onResizeStop() // When the item stops being resized. 125 | onResizeAny() // When the item starts/stops/is being resized. 126 | onChangeStart() // When the item starts being dragged or resized. 127 | onChange() // When the item is dragged or resized. 128 | onChangeStop() // When the item stops being dragged or resized. 129 | onChangeAny() // When the item starts/stops/is being dragged or resized. 130 | onItemChange() // When either the item's grid size or position is changed. 131 | ``` 132 | 133 | Each event will also provide the following object to any callback functions: 134 | 135 | ```javascript 136 | interface NgGridItemEvent { 137 | payload: any, // The item's optional custom payload (string/number/object) to be used to identify the item for serialization 138 | col: number, // The item's column position within the grid 139 | row: number, // The item's row position within the grid 140 | sizex: number, // The item's column size within the grid 141 | sizey: number, // The item's row size within the grid 142 | width: number, // The item's raw width value 143 | height: number, // The item's raw height value 144 | left: number, // The item's offset left value 145 | top: number // The item's offset top value 146 | } 147 | ``` 148 | 149 | #### Styling 150 | ------------ 151 | 152 | There are three elements that can be styled with angular2-grid, the grid itself `.grid`, the items `.grid-item` and the placeholder `.placeholder`. The demo includes some basic styling in NgGrid.css which you can include in your app's `styleUrls` property. It also includes some @media queries styles to handle responsiveness on smaller screens. This simple force the boxes to full width and puts them inline in their original order. This is optional functionality and does not need to be included. In order for correct functionality, the required styles are added by the classes themselves at run-time: 153 | 154 | ```css 155 | .grid { 156 | position: relative; 157 | } 158 | 159 | .grid-item { 160 | position: absolute; 161 | } 162 | 163 | .grid-item.moving { 164 | z-index: z-index + 1; 165 | } 166 | 167 | .placeholder { 168 | position: absolute; 169 | } 170 | ``` 171 | 172 | You can prevent these styles being automatically added by setting the value of `'auto_size'` to be `false`. You will then need to ensure that they are correctly incorporated into your user styles instead. 173 | 174 | NOTE: The grid system sets the values `width, height, left, top` in CSS to move and resize the elements. This cannot be disabled. 175 | 176 | #### Example 177 | ------------ 178 | 179 | The `NgGrid` and `NgGridItem` can be configured by binding directly to the directive. The `NgGridItem` supports two-way binding so you don't need to bind to any of the above events. The `NgGridItemChange` event emits under the same conditions as `onChangeStop`. The only config values that will change are `col`, `row`, `sizex` and `sizey`; the rest of your configuration will persist. You can then use these values for serialization of the grid. By binding the configuration this way, you are able to update the values on the fly. Here is an example template of the grid with two-way item bindings: 180 | 181 | ```html 182 |
183 |
184 |
{{box.title}}
185 |

{{box.text}}

186 |
187 |
188 | ``` 189 | 190 | In order to include the relevant files, you will need to import the `NgGridModule` to your app and add them to the `@NgModule` imports. This can be achieved by adding: 191 | 192 | ```typescript 193 | import { NgGridModule } from 'angular2-grid'; 194 | ``` 195 | 196 | to your typescript imports, and ensuring that your `@NgModule` annotation looks similar to the following: 197 | 198 | ```typescript 199 | @NgModule({ 200 | ..., 201 | imports: [ 202 | ..., 203 | NgGridModule, 204 | ... 205 | ], 206 | ... 207 | }) 208 | ``` 209 | 210 | As of the Angular 2 Release Candidate you will now need to have the following in your System.js configuration, assuming that you are following the same format as the [Angular 2 Quick Start](https://angular.io/docs/ts/latest/quickstart.html): 211 | 212 | ``` 213 | // latest version of angular2-grid 214 | map: { 215 | 'angular2-grid': 'node_modules/angular2-grid/bundles' 216 | } 217 | 218 | packages: { 219 | 'angular2-grid': { main: 'NgGrid.umd.js', defaultExtension: 'js' } 220 | } 221 | 222 | // earlier versions 223 | map: { 224 | 'angular2-grid': 'node_modules/angular2-grid/dist/js' 225 | } 226 | 227 | packages: { 228 | 'angular2-grid': { main: 'main.js', defaultExtension: 'js' } 229 | } 230 | ``` 231 | 232 | Alternatively, for earlier versions you can use the bundled version by setting the `map` value to `'node_modules/angular2-grid/dist/bundles'` and the `main` value within packages to `NgGrid.min.js`. 233 | 234 | To see a working typescript example project, check the [dashboard demo folder in the source](https://github.com/BTMorton/angular2-grid/tree/master/demo-dashboard) or the [main demo repository](https://github.com/BTMorton/angular2-grid-demo). 235 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular2-grid-demo": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "outputPath": "dist/angular2-grid-demo", 15 | "index": "src/index.html", 16 | "main": "src/main.ts", 17 | "tsConfig": "src/tsconfig.app.json", 18 | "polyfills": "src/polyfills.ts", 19 | "assets": [ 20 | "src/assets" 21 | ], 22 | "styles": [ 23 | "src/styles.css", 24 | "node_modules/bootstrap/dist/css/bootstrap.min.css", 25 | "dist/angular2-grid/NgGrid.css" 26 | ], 27 | "scripts": [] 28 | }, 29 | "configurations": { 30 | "production": { 31 | "optimization": true, 32 | "outputHashing": "all", 33 | "sourceMap": false, 34 | "extractCss": true, 35 | "namedChunks": false, 36 | "aot": true, 37 | "extractLicenses": true, 38 | "vendorChunk": false, 39 | "buildOptimizer": true, 40 | "fileReplacements": [ 41 | { 42 | "replace": "src/environments/environment.ts", 43 | "with": "src/environments/environment.prod.ts" 44 | } 45 | ] 46 | } 47 | } 48 | }, 49 | "serve": { 50 | "builder": "@angular-devkit/build-angular:dev-server", 51 | "options": { 52 | "browserTarget": "angular2-grid-demo:build" 53 | }, 54 | "configurations": { 55 | "production": { 56 | "browserTarget": "angular2-grid-demo:build:production" 57 | } 58 | } 59 | }, 60 | "extract-i18n": { 61 | "builder": "@angular-devkit/build-angular:extract-i18n", 62 | "options": { 63 | "browserTarget": "angular2-grid-demo:build" 64 | } 65 | }, 66 | "test": { 67 | "builder": "@angular-devkit/build-angular:karma", 68 | "options": { 69 | "main": "src/test.ts", 70 | "karmaConfig": "./karma.conf.js", 71 | "polyfills": "src/polyfills.ts", 72 | "scripts": [], 73 | "styles": [ 74 | "src/styles.scss" 75 | ], 76 | "assets": [ 77 | "src/assets" 78 | ] 79 | } 80 | }, 81 | "lint": { 82 | "builder": "@angular-devkit/build-angular:tslint", 83 | "options": { 84 | "tsConfig": [ 85 | "src/tsconfig.app.json", 86 | "src/tsconfig.spec.json" 87 | ], 88 | "exclude": [] 89 | } 90 | } 91 | } 92 | }, 93 | "angular2-grid-demo-e2e": { 94 | "root": "", 95 | "sourceRoot": "e2e", 96 | "projectType": "application", 97 | "architect": { 98 | "e2e": { 99 | "builder": "@angular-devkit/build-angular:protractor", 100 | "options": { 101 | "protractorConfig": "./protractor.conf.js", 102 | "devServerTarget": "angular2-grid-demo:serve" 103 | } 104 | }, 105 | "lint": { 106 | "builder": "@angular-devkit/build-angular:tslint", 107 | "options": { 108 | "tsConfig": [ 109 | "e2e/tsconfig.e2e.json" 110 | ], 111 | "exclude": [] 112 | } 113 | } 114 | } 115 | }, 116 | "angular2-grid": { 117 | "root": "projects/angular2-grid", 118 | "sourceRoot": "projects/angular2-grid/src", 119 | "projectType": "library", 120 | "prefix": "lib", 121 | "architect": { 122 | "build": { 123 | "builder": "@angular-devkit/build-ng-packagr:build", 124 | "options": { 125 | "tsConfig": "projects/angular2-grid/tsconfig.lib.json", 126 | "project": "projects/angular2-grid/ng-package.json" 127 | }, 128 | "configurations": { 129 | "production": { 130 | "project": "projects/angular2-grid/ng-package.prod.json" 131 | } 132 | } 133 | }, 134 | "test": { 135 | "builder": "@angular-devkit/build-angular:karma", 136 | "options": { 137 | "main": "projects/angular2-grid/src/test.ts", 138 | "tsConfig": "projects/angular2-grid/tsconfig.spec.json", 139 | "karmaConfig": "projects/angular2-grid/karma.conf.js" 140 | } 141 | }, 142 | "lint": { 143 | "builder": "@angular-devkit/build-angular:tslint", 144 | "options": { 145 | "tsConfig": [ 146 | "projects/angular2-grid/tsconfig.lib.json", 147 | "projects/angular2-grid/tsconfig.spec.json" 148 | ], 149 | "exclude": [ 150 | "**/node_modules/**" 151 | ] 152 | } 153 | } 154 | } 155 | } 156 | }, 157 | "defaultProject": "angular2-grid-demo", 158 | "schematics": { 159 | "@schematics/angular:component": { 160 | "inlineTemplate": false, 161 | "spec": true, 162 | "prefix": "app", 163 | "styleext": "css" 164 | }, 165 | "@schematics/angular:directive": { 166 | "prefix": "app" 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /demo-dashboard/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Dashboard demo of angular2-grid 3 | 4 | This is a demo of the angular2-grid project with a new coloring scheme, and the functionality of removing a widget when clicking the corresponing remove icon on widget level. 5 | 6 | #### Setup 7 | ---------- 8 | 9 | ```shell 10 | cd demo-dashboard 11 | npm install 12 | npm start 13 | ``` 14 | 15 | #### Output 16 | ---------- 17 | 18 | ![demo-dashboard](/demo-dashboard/demo-dashboard.png?raw=true "Demo Dashboard") 19 | -------------------------------------------------------------------------------- /demo-dashboard/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ViewEncapsulation} from '@angular/core'; 2 | import {NgGrid, NgGridItem, NgGridConfig, NgGridItemConfig, NgGridItemEvent} from 'angular2-grid'; 3 | 4 | interface Box { 5 | id: number; 6 | config: any; 7 | } 8 | 9 | @Component({ 10 | selector: 'my-app', 11 | templateUrl: 'app/app.html', 12 | styleUrls: ['app/app.css'], 13 | encapsulation: ViewEncapsulation.None 14 | }) 15 | export class AppComponent { 16 | private boxes: Array = []; 17 | private rgb: string = '#efefef'; 18 | private curNum; 19 | private gridConfig: NgGridConfig = { 20 | 'margins': [5], 21 | 'draggable': true, 22 | 'resizable': true, 23 | 'max_cols': 0, 24 | 'max_rows': 0, 25 | 'visible_cols': 0, 26 | 'visible_rows': 0, 27 | 'min_cols': 1, 28 | 'min_rows': 1, 29 | 'col_width': 2, 30 | 'row_height': 2, 31 | 'cascade': 'up', 32 | 'min_width': 50, 33 | 'min_height': 50, 34 | 'fix_to_grid': false, 35 | 'auto_style': true, 36 | 'auto_resize': false, 37 | 'maintain_ratio': false, 38 | 'prefer_new': false, 39 | 'zoom_on_drag': false, 40 | 'limit_to_screen': true 41 | }; 42 | private itemPositions: Array = []; 43 | 44 | constructor() { 45 | const dashconf = this._generateDefaultDashConfig(); 46 | for (var i = 0; i < dashconf.length; i++) { 47 | const conf = dashconf[i]; 48 | conf.payload = 1 + i; 49 | this.boxes[i] = { id: i + 1, config: conf }; 50 | } 51 | this.curNum = dashconf.length + 1; 52 | } 53 | 54 | addBox(): void { 55 | const conf: NgGridItemConfig = this._generateDefaultItemConfig(); 56 | conf.payload = this.curNum++; 57 | this.boxes.push({ id: conf.payload, config: conf }); 58 | } 59 | 60 | removeWidget(index: number): void { 61 | if (this.boxes[index]) { 62 | this.boxes.splice(index, 1); 63 | } 64 | } 65 | 66 | updateItem(index: number, event: NgGridItemEvent): void { 67 | // Do something here 68 | } 69 | 70 | onDrag(index: number, event: NgGridItemEvent): void { 71 | // Do something here 72 | } 73 | 74 | onResize(index: number, event: NgGridItemEvent): void { 75 | // Do something here 76 | } 77 | 78 | private _generateDefaultItemConfig(): NgGridItemConfig { 79 | return { 'dragHandle': '.handle', 'col': 1, 'row': 1, 'sizex': 1, 'sizey': 1 }; 80 | } 81 | 82 | private _generateDefaultDashConfig(): NgGridItemConfig[] { 83 | return [{ 'dragHandle': '.handle', 'col': 1, 'row': 1, 'sizex': 50, 'sizey': 40 }, 84 | { 'dragHandle': '.handle', 'col': 1, 'row': 1, 'sizex': 1, 'sizey': 1 }, 85 | { 'dragHandle': '.handle', 'col': 26, 'row': 1, 'sizex': 1, 'sizey': 1 }, 86 | { 'dragHandle': '.handle', 'col': 51, 'row': 1, 'sizex': 75, 'sizey': 1 }, 87 | { 'dragHandle': '.handle', 'col': 51, 'row': 26, 'sizex': 32, 'sizey': 40 }, 88 | { 'dragHandle': '.handle', 'col': 83, 'row': 26, 'sizex': 1, 'sizey': 1 }]; 89 | } 90 | } -------------------------------------------------------------------------------- /demo-dashboard/app/app.css: -------------------------------------------------------------------------------- 1 | .grid-item { 2 | border: solid 1px #ccc; 3 | } 4 | 5 | .handle { 6 | width: 100%; 7 | height: 50px; 8 | background: #eee; 9 | color: black; 10 | font-weight: 500; 11 | padding: 15px; 12 | box-sizing: border-box; 13 | } 14 | 15 | .btn { 16 | margin-left: 10px; 17 | } 18 | 19 | .box-header-btns { 20 | top: 15px; 21 | right: 10px; 22 | cursor: pointer; 23 | position: absolute; 24 | } 25 | 26 | .navbar-default { 27 | background: black; 28 | border: 0px; 29 | } 30 | 31 | .navbar-default .navbar-brand { 32 | color: white; 33 | } 34 | 35 | body { 36 | background-color: #f5f2f2; 37 | } -------------------------------------------------------------------------------- /demo-dashboard/app/app.html: -------------------------------------------------------------------------------- 1 | 11 |
12 | 13 |
14 |
Widget {{box.id}}
15 | 16 | 17 | 18 |
19 |
20 |
-------------------------------------------------------------------------------- /demo-dashboard/app/boot.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, enableProdMode } from '@angular/core' 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic' 3 | import { BrowserModule } from '@angular/platform-browser' 4 | import { AppComponent } from './app.component' 5 | import { NgGridModule } from 'angular2-grid'; 6 | 7 | @NgModule({ 8 | imports: [ BrowserModule, NgGridModule ], 9 | declarations: [ AppComponent ], 10 | providers: [], 11 | bootstrap: [ AppComponent ] 12 | }) 13 | class AppModule { } 14 | 15 | //enableProdMode(); 16 | const platform = platformBrowserDynamic(); 17 | platform.bootstrapModule(AppModule); -------------------------------------------------------------------------------- /demo-dashboard/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Angular 2 QuickStart 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 33 | 34 | 35 | 36 | 37 | 38 | Loading... 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /demo-dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-quickstart", 3 | "scripts": { 4 | "tsc": "tsc", 5 | "tsc:w": "tsc -w", 6 | "lite": "lite-server", 7 | "start": "concurrent \"npm run tsc:w\" \"npm run lite\" " 8 | }, 9 | "license": "ISC", 10 | "dependencies": { 11 | "@angular/common": "2.4.4", 12 | "@angular/compiler": "2.4.4", 13 | "@angular/core": "2.4.4", 14 | "@angular/platform-browser": "2.4.4", 15 | "@angular/platform-browser-dynamic": "2.4.4", 16 | "angular2-grid": "^2.0.0", 17 | "es6-shim": "^0.35.1", 18 | "reflect-metadata": "^0.1.3", 19 | "rxjs": "5.0.1", 20 | "systemjs": "0.19.29", 21 | "zone.js": "^0.7.2" 22 | }, 23 | "devDependencies": { 24 | "concurrently": "^2.1.0", 25 | "lite-server": "^2.2.0", 26 | "typescript": "^2.0.10" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /demo-dashboard/style.css: -------------------------------------------------------------------------------- 1 | .grid { 2 | background-color: #f5f2f2; 3 | width: 100%; 4 | min-height: 750px; 5 | } 6 | 7 | .grid-item { 8 | background-color: #ffffff; 9 | -webkit-transition: width 0.25s, height 0.25s, left 0.25s, top 0.25s, right 0.25s, bottom 0.25s; 10 | -moz-transition: width 0.25s, height 0.25s, left 0.25s, top 0.25s, right 0.25s, bottom 0.25s; 11 | -o-transition: width 0.25s, height 0.25s, left 0.25s, top 0.25s, right 0.25s, bottom 0.25s; 12 | transition: width 0.25s, height 0.25s, left 0.25s, top 0.25s, right 0.25s, bottom 0.25s; 13 | border: solid 1px; 14 | } 15 | 16 | .grid-item:active, .grid-item.moving { 17 | z-index: 2; 18 | -webkit-transition: none; 19 | -moz-transition: none; 20 | -o-transition: none; 21 | transition: none; 22 | } 23 | 24 | .grid-placeholder { 25 | background-color: rgba(0, 100, 200, 0.3); 26 | } 27 | 28 | @media (max-width: 767px) { 29 | .grid { 30 | width: 100% !important; 31 | height: auto !important; 32 | padding: 10px; 33 | } 34 | .grid-item { 35 | position: static !important; 36 | width: 100% !important; 37 | margin-bottom: 10px; 38 | } 39 | .grid-item:last-child { 40 | margin-bottom: 0; 41 | } 42 | } -------------------------------------------------------------------------------- /demo-dashboard/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "system", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "removeComments": false, 10 | "noImplicitAny": false, 11 | "lib": [ 12 | "es2015", 13 | "dom" 14 | ] 15 | }, 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Angular2GridDemoPage } from './app.po'; 2 | 3 | describe('angular2-grid-demo App', function() { 4 | let page: Angular2GridDemoPage; 5 | 6 | beforeEach(() => { 7 | page = new Angular2GridDemoPage(); 8 | }); 9 | 10 | it('should display message saying app works', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('app works!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | export class Angular2GridDemoPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "lib": [ 9 | "es2017" 10 | ], 11 | "outDir": "../out-tsc/e2e", 12 | "module": "commonjs", 13 | "target": "es5", 14 | "types":[ 15 | "jasmine", 16 | "node" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('~angular/cli/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | files: [ 19 | 20 | ], 21 | preprocessors: { 22 | 23 | }, 24 | mime: { 25 | 'text/x-typescript': ['ts','tsx'] 26 | }, 27 | coverageIstanbulReporter: { 28 | dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ], 29 | fixWebpackSourcePaths: true 30 | }, 31 | 32 | reporters: config.angularCli && config.angularCli.codeCoverage 33 | ? ['progress', 'coverage-istanbul'] 34 | : ['progress', 'kjhtml'], 35 | port: 9876, 36 | colors: true, 37 | logLevel: config.LOG_INFO, 38 | autoWatch: true, 39 | browsers: ['Chrome'], 40 | singleRun: false 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-grid-demo", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "angular-cli": {}, 6 | "scripts": { 7 | "ng": "ng", 8 | "start": "ng serve", 9 | "build": "ng build", 10 | "build-prod": "ng build --prod", 11 | "build-lib": "ng build --prod angular2-grid && npm run copy-lib", 12 | "copy-lib": "copyfiles -f LICENSE README.md projects/angular2-grid/src/*.css dist/angular2-grid", 13 | "test": "ng test", 14 | "lint": "ng lint", 15 | "e2e": "ng e2e" 16 | }, 17 | "private": true, 18 | "dependencies": { 19 | "@angular/common": "^9.0.1", 20 | "@angular/compiler": "^9.0.1", 21 | "@angular/core": "^9.0.1", 22 | "@angular/forms": "^9.0.1", 23 | "@angular/http": "^7.2.16", 24 | "@angular/platform-browser": "^9.0.1", 25 | "@angular/platform-browser-dynamic": "^9.0.1", 26 | "@angular/router": "^9.0.1", 27 | "bootstrap": "^3.3.7", 28 | "core-js": "^2.5.7", 29 | "rxjs": "^6.5.4", 30 | "zone.js": "^0.10.2" 31 | }, 32 | "devDependencies": { 33 | "@angular-devkit/build-angular": "~0.900.2", 34 | "@angular-devkit/build-ng-packagr": "~0.900.2", 35 | "@angular/cli": "^9.0.2", 36 | "@angular/compiler-cli": "^9.0.1", 37 | "@types/jasmine": "3.5.3", 38 | "@types/node": "~13.7.1", 39 | "codelyzer": "~5.2.1", 40 | "copyfiles": "^2.2.0", 41 | "jasmine-core": "~3.5.0", 42 | "jasmine-spec-reporter": "~4.2.1", 43 | "karma": "~4.4.1", 44 | "karma-chrome-launcher": "~3.1.0", 45 | "karma-cli": "~2.0.0", 46 | "karma-coverage-istanbul-reporter": "^2.1.1", 47 | "karma-jasmine": "~3.1.1", 48 | "karma-jasmine-html-reporter": "^1.5.2", 49 | "ng-packagr": "^9.0.0", 50 | "protractor": "~5.4.3", 51 | "ts-node": "~8.6.2", 52 | "tsickle": "0.38.0", 53 | "tslib": "^1.10.0", 54 | "tslint": "^6.0.0", 55 | "typescript": "~3.7.5" 56 | } 57 | } -------------------------------------------------------------------------------- /projects/angular2-grid/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /projects/angular2-grid/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/angular2-grid", 4 | "deleteDestPath": false, 5 | "lib": { 6 | "entryFile": "src/main.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /projects/angular2-grid/ng-package.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/angular2-grid", 4 | "lib": { 5 | "entryFile": "src/main.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/angular2-grid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular2-grid", 3 | "version": "4.0.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/BTMorton/angular2-grid.git" 7 | }, 8 | "keywords": [ 9 | "angular", 10 | "angular2", 11 | "grid", 12 | "web-components" 13 | ], 14 | "author": "Ben Morton (http://bmorton.co.uk/)", 15 | "contributors": [ 16 | "Martin Sikora (https://github.com/martinsik)", 17 | "Daniel Pastor de la Fuenta (https://github.com/aebsubis)", 18 | "Frederik Schubert (https://github.com/frederikschubert)", 19 | "Matthew de Nobrega (https://github.com/matthewdenobrega)", 20 | "Alex (https://github.com/lifecoderua)", 21 | "pocmanu (https://github.com/pocmanu)", 22 | "Chris Morabito (https://github.com/morabitowoolpert)", 23 | "Carlos Esteban Lopez Jaramillo (https://github.com/Luchillo)", 24 | "Niklas Berg (https://github.com/nkholski)", 25 | "Wouter (https://github.com/wuhkuh)", 26 | "Brian Ellis (https://github.com/OnlyAGhost)", 27 | "Janne Julkunen (https://github.com/sconix)", 28 | "Thomas Schellenberg (https://github.com/ThomasSchellenbergNextCentury)" 29 | ], 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/BTMorton/angular2-grid/issues" 33 | }, 34 | "homepage": "https://github.com/BTMorton/angular2-grid", 35 | "peerDependencies": { 36 | "@angular/common": "^8.0.0", 37 | "@angular/core": "^8.0.0" 38 | } 39 | } -------------------------------------------------------------------------------- /projects/angular2-grid/src/NgGrid.css: -------------------------------------------------------------------------------- 1 | .grid { 2 | background-color: #efefef; 3 | transform-origin: top center; 4 | transition: transform 0.5s; 5 | } 6 | 7 | .grid-item { 8 | background-color: #ffffff; 9 | -webkit-transition: width 0.25s, height 0.25s, transform 0.5s; 10 | -moz-transition: width 0.25s, height 0.25s, transform 0.5s; 11 | -o-transition: width 0.25s, height 0.25s, transform 0.5s; 12 | transition: width 0.25s, height 0.25s, transform 0.5s; 13 | overflow: hidden; 14 | } 15 | 16 | .grid-item:active, .grid-item.moving { 17 | z-index: 2; 18 | -webkit-transition: none; 19 | -moz-transition: none; 20 | -o-transition: none; 21 | transition: none; 22 | } 23 | 24 | .grid-placeholder { 25 | background-color: rgba(0, 0, 0, 0.3); 26 | } -------------------------------------------------------------------------------- /projects/angular2-grid/src/NgGrid_FixSmall.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 767px) { 2 | .grid { 3 | width: 100% !important; 4 | height: auto !important; 5 | padding: 10px; 6 | } 7 | .grid-item { 8 | position: static !important; 9 | width: 100% !important; 10 | margin-bottom: 10px; 11 | transform: none !important; 12 | } 13 | .grid-item:last-child { 14 | margin-bottom: 0; 15 | } 16 | } -------------------------------------------------------------------------------- /projects/angular2-grid/src/components/NgGridPlaceholder.ts: -------------------------------------------------------------------------------- 1 | import { NgGrid } from '../directives/NgGrid'; 2 | import { NgGridItemPosition, NgGridItemSize } from '../interfaces/INgGrid'; 3 | import { Component, Directive, ElementRef, Renderer2, EventEmitter, Host, ViewEncapsulation, Type, ComponentRef, KeyValueDiffer, KeyValueDiffers, OnInit, OnDestroy, DoCheck, ViewContainerRef, Output } from '@angular/core'; 4 | 5 | @Component({ 6 | selector: 'ng-grid-placeholder', 7 | template: '' 8 | }) 9 | export class NgGridPlaceholder implements OnInit { 10 | private _size: NgGridItemSize; 11 | private _position: NgGridItemPosition; 12 | private _ngGrid: NgGrid; 13 | private _cascadeMode: string; 14 | 15 | constructor(private _ngEl: ElementRef, private _renderer: Renderer2) { } 16 | 17 | public registerGrid(ngGrid: NgGrid) { 18 | this._ngGrid = ngGrid; 19 | } 20 | 21 | public ngOnInit(): void { 22 | this._renderer.addClass(this._ngEl.nativeElement, 'grid-placeholder'); 23 | if (this._ngGrid.autoStyle) this._renderer.setStyle(this._ngEl.nativeElement, 'position', 'absolute'); 24 | } 25 | 26 | public setSize(newSize: NgGridItemSize): void { 27 | this._size = newSize; 28 | this._recalculateDimensions(); 29 | } 30 | 31 | public setGridPosition(newPosition: NgGridItemPosition): void { 32 | this._position = newPosition; 33 | this._recalculatePosition(); 34 | } 35 | 36 | public setCascadeMode(cascade: string): void { 37 | this._cascadeMode = cascade; 38 | switch (cascade) { 39 | case 'up': 40 | case 'left': 41 | default: 42 | this._renderer.setStyle(this._ngEl.nativeElement, 'left', '0px'); 43 | this._renderer.setStyle(this._ngEl.nativeElement, 'top', '0px'); 44 | this._renderer.setStyle(this._ngEl.nativeElement, 'right', null); 45 | this._renderer.setStyle(this._ngEl.nativeElement, 'bottom', null); 46 | break; 47 | case 'right': 48 | this._renderer.setStyle(this._ngEl.nativeElement, 'right', '0px'); 49 | this._renderer.setStyle(this._ngEl.nativeElement, 'top', '0px'); 50 | this._renderer.setStyle(this._ngEl.nativeElement, 'left', null); 51 | this._renderer.setStyle(this._ngEl.nativeElement, 'bottom', null); 52 | break; 53 | case 'down': 54 | this._renderer.setStyle(this._ngEl.nativeElement, 'left', '0px'); 55 | this._renderer.setStyle(this._ngEl.nativeElement, 'bottom', '0px'); 56 | this._renderer.setStyle(this._ngEl.nativeElement, 'right', null); 57 | this._renderer.setStyle(this._ngEl.nativeElement, 'top', null); 58 | break; 59 | } 60 | } 61 | 62 | // Private methods 63 | private _setDimensions(w: number, h: number): void { 64 | this._renderer.setStyle(this._ngEl.nativeElement, 'width', w + 'px'); 65 | this._renderer.setStyle(this._ngEl.nativeElement, 'height', h + 'px'); 66 | } 67 | 68 | private _setPosition(x: number, y: number): void { 69 | switch (this._cascadeMode) { 70 | case 'up': 71 | case 'left': 72 | default: 73 | this._renderer.setStyle(this._ngEl.nativeElement, 'transform', 'translate(' + x + 'px, ' + y + 'px)'); 74 | break; 75 | case 'right': 76 | this._renderer.setStyle(this._ngEl.nativeElement, 'transform', 'translate(' + -x + 'px, ' + y + 'px)'); 77 | break; 78 | case 'down': 79 | this._renderer.setStyle(this._ngEl.nativeElement, 'transform', 'translate(' + x + 'px, ' + -y + 'px)'); 80 | break; 81 | } 82 | } 83 | 84 | private _recalculatePosition(): void { 85 | const x: number = (this._ngGrid.colWidth + this._ngGrid.marginLeft + this._ngGrid.marginRight) * (this._position.col - 1) + this._ngGrid.marginLeft + this._ngGrid.screenMargin; 86 | const y: number = (this._ngGrid.rowHeight + this._ngGrid.marginTop + this._ngGrid.marginBottom) * (this._position.row - 1) + this._ngGrid.marginTop; 87 | this._setPosition(x, y); 88 | } 89 | 90 | private _recalculateDimensions(): void { 91 | const w: number = (this._ngGrid.colWidth * this._size.x) + ((this._ngGrid.marginLeft + this._ngGrid.marginRight) * (this._size.x - 1)); 92 | const h: number = (this._ngGrid.rowHeight * this._size.y) + ((this._ngGrid.marginTop + this._ngGrid.marginBottom) * (this._size.y - 1)); 93 | this._setDimensions(w, h); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /projects/angular2-grid/src/directives/NgGrid.ts: -------------------------------------------------------------------------------- 1 | import { Component, Directive, ElementRef, Renderer2, EventEmitter, ComponentFactoryResolver, Host, ViewEncapsulation, Type, ComponentRef, KeyValueDiffer, KeyValueDiffers, OnInit, OnDestroy, DoCheck, ViewContainerRef, Output } from '@angular/core'; 2 | import { NgGridConfig, NgGridItemEvent, NgGridItemPosition, NgGridItemSize, NgGridRawPosition, NgGridItemDimensions, NgConfigFixDirection } from '../interfaces/INgGrid'; 3 | import { NgGridItem } from './NgGridItem'; 4 | import * as NgGridHelper from '../helpers/NgGridHelpers'; 5 | import { NgGridPlaceholder } from '../components/NgGridPlaceholder'; 6 | import { Subscription, Observable, fromEvent } from 'rxjs'; 7 | 8 | @Directive({ 9 | selector: '[ngGrid]', 10 | inputs: ['config: ngGrid'], 11 | host: { 12 | '(window:resize)': 'resizeEventHandler($event)', 13 | } 14 | }) 15 | export class NgGrid implements OnInit, DoCheck, OnDestroy { 16 | public static CONST_DEFAULT_RESIZE_DIRECTIONS: string[] = [ 17 | 'bottomright', 18 | 'bottomleft', 19 | 'topright', 20 | 'topleft', 21 | 'right', 22 | 'left', 23 | 'bottom', 24 | 'top', 25 | ]; 26 | 27 | // Event Emitters 28 | @Output() public onDragStart: EventEmitter = new EventEmitter(); 29 | @Output() public onDrag: EventEmitter = new EventEmitter(); 30 | @Output() public onDragStop: EventEmitter = new EventEmitter(); 31 | @Output() public onResizeStart: EventEmitter = new EventEmitter(); 32 | @Output() public onResize: EventEmitter = new EventEmitter(); 33 | @Output() public onResizeStop: EventEmitter = new EventEmitter(); 34 | @Output() public onItemChange: EventEmitter> = new EventEmitter>(); 35 | 36 | // Public variables 37 | public colWidth: number = 250; 38 | public rowHeight: number = 250; 39 | public minCols: number = 1; 40 | public minRows: number = 1; 41 | public marginTop: number = 10; 42 | public marginRight: number = 10; 43 | public marginBottom: number = 10; 44 | public marginLeft: number = 10; 45 | public screenMargin: number = 0; 46 | public isDragging: boolean = false; 47 | public isResizing: boolean = false; 48 | public autoStyle: boolean = true; 49 | public resizeEnable: boolean = true; 50 | public dragEnable: boolean = true; 51 | public cascade: string = 'up'; 52 | public minWidth: number = 100; 53 | public minHeight: number = 100; 54 | public resizeDirections: string[] = NgGrid.CONST_DEFAULT_RESIZE_DIRECTIONS; 55 | 56 | // Private variables 57 | private _items: Map = new Map(); 58 | private _draggingItem: NgGridItem = null; 59 | private _resizingItem: NgGridItem = null; 60 | private _resizeDirection: string = null; 61 | private _itemsInGrid: Set = new Set(); 62 | private _containerWidth: number; 63 | private _containerHeight: number; 64 | private _maxCols: number = 0; 65 | private _maxRows: number = 0; 66 | private _visibleCols: number = 0; 67 | private _visibleRows: number = 0; 68 | private _setWidth: number = 250; 69 | private _setHeight: number = 250; 70 | private _posOffset: NgGridRawPosition = null; 71 | private _adding: boolean = false; 72 | private _placeholderRef: ComponentRef = null; 73 | private _fixToGrid: boolean = false; 74 | private _autoResize: boolean = false; 75 | private _differ: KeyValueDiffer; 76 | private _destroyed: boolean = false; 77 | private _maintainRatio: boolean = false; 78 | private _aspectRatio: number; 79 | private _preferNew: boolean = false; 80 | private _zoomOnDrag: boolean = false; 81 | private _limitToScreen: boolean = false; 82 | private _centerToScreen: boolean = false; 83 | private _curMaxRow: number = 0; 84 | private _curMaxCol: number = 0; 85 | private _dragReady: boolean = false; 86 | private _resizeReady: boolean = false; 87 | private _elementBasedDynamicRowHeight: boolean = false; 88 | private _itemFixDirection: NgConfigFixDirection = 'cascade'; 89 | private _collisionFixDirection: NgConfigFixDirection = 'cascade'; 90 | private _allowOverlap: boolean = false; 91 | private _cascadePromise: Promise; 92 | private _lastZValue: number = 1; 93 | 94 | // Events 95 | private _documentMousemove$: Observable; 96 | private _documentMouseup$: Observable; 97 | private _mousedown$: Observable; 98 | private _mousemove$: Observable; 99 | private _mouseup$: Observable; 100 | private _touchstart$: Observable; 101 | private _touchmove$: Observable; 102 | private _touchend$: Observable; 103 | private _subscriptions: Subscription[] = []; 104 | 105 | private _enabledListener: boolean = false; 106 | 107 | // Default config 108 | private static CONST_DEFAULT_CONFIG: NgGridConfig = { 109 | margins: [10], 110 | draggable: true, 111 | resizable: true, 112 | max_cols: 0, 113 | max_rows: 0, 114 | visible_cols: 0, 115 | visible_rows: 0, 116 | col_width: 250, 117 | row_height: 250, 118 | cascade: 'up', 119 | min_width: 100, 120 | min_height: 100, 121 | fix_to_grid: false, 122 | auto_style: true, 123 | auto_resize: false, 124 | maintain_ratio: false, 125 | prefer_new: false, 126 | zoom_on_drag: false, 127 | limit_to_screen: false, 128 | center_to_screen: false, 129 | resize_directions: NgGrid.CONST_DEFAULT_RESIZE_DIRECTIONS, 130 | element_based_row_height: false, 131 | fix_item_position_direction: 'cascade', 132 | fix_collision_position_direction: 'cascade', 133 | allow_overlap: false, 134 | }; 135 | private _config = NgGrid.CONST_DEFAULT_CONFIG; 136 | 137 | // [ng-grid] attribute handler 138 | set config(v: NgGridConfig) { 139 | if (v == null || typeof v !== 'object') { 140 | return; 141 | } 142 | 143 | this.setConfig(v); 144 | 145 | if (this._differ == null && v != null) { 146 | this._differ = this._differs.find(this._config).create(); 147 | } 148 | 149 | this._differ.diff(this._config); 150 | } 151 | 152 | // Constructor 153 | constructor( 154 | private _differs: KeyValueDiffers, 155 | private _ngEl: ElementRef, 156 | private _renderer: Renderer2, 157 | private componentFactoryResolver: ComponentFactoryResolver, 158 | ) { 159 | this._defineListeners(); 160 | } 161 | 162 | // Public methods 163 | public ngOnInit(): void { 164 | this._renderer.addClass(this._ngEl.nativeElement, 'grid'); 165 | if (this.autoStyle) this._renderer.setStyle(this._ngEl.nativeElement, 'position', 'relative'); 166 | this.setConfig(this._config); 167 | } 168 | 169 | public ngOnDestroy(): void { 170 | this._destroyed = true; 171 | this._disableListeners(); 172 | } 173 | 174 | public generateItemUid(): string { 175 | const uid: string = NgGridHelper.generateUuid(); 176 | 177 | if (this._items.has(uid)) { 178 | return this.generateItemUid(); 179 | } 180 | 181 | return uid; 182 | } 183 | 184 | public setConfig(config: NgGridConfig): void { 185 | this._config = config; 186 | 187 | var maxColRowChanged = false; 188 | for (var x in config) { 189 | var val = config[x]; 190 | var intVal = !val ? 0 : parseInt(val); 191 | 192 | switch (x) { 193 | case 'margins': 194 | this.setMargins(val); 195 | break; 196 | case 'col_width': 197 | this.colWidth = Math.max(intVal, 1); 198 | break; 199 | case 'row_height': 200 | this.rowHeight = Math.max(intVal, 1); 201 | break; 202 | case 'auto_style': 203 | this.autoStyle = val ? true : false; 204 | break; 205 | case 'auto_resize': 206 | this._autoResize = val ? true : false; 207 | break; 208 | case 'draggable': 209 | this.dragEnable = val ? true : false; 210 | break; 211 | case 'resizable': 212 | this.resizeEnable = val ? true : false; 213 | break; 214 | case 'max_rows': 215 | maxColRowChanged = maxColRowChanged || this._maxRows != intVal; 216 | this._maxRows = intVal < 0 ? 0 : intVal; 217 | break; 218 | case 'max_cols': 219 | maxColRowChanged = maxColRowChanged || this._maxCols != intVal; 220 | this._maxCols = intVal < 0 ? 0 : intVal; 221 | break; 222 | case 'visible_rows': 223 | this._visibleRows = Math.max(intVal, 0); 224 | break; 225 | case 'visible_cols': 226 | this._visibleCols = Math.max(intVal, 0); 227 | break; 228 | case 'min_rows': 229 | this.minRows = Math.max(intVal, 1); 230 | break; 231 | case 'min_cols': 232 | this.minCols = Math.max(intVal, 1); 233 | break; 234 | case 'min_height': 235 | this.minHeight = Math.max(intVal, 1); 236 | break; 237 | case 'min_width': 238 | this.minWidth = Math.max(intVal, 1); 239 | break; 240 | case 'zoom_on_drag': 241 | this._zoomOnDrag = val ? true : false; 242 | break; 243 | case 'cascade': 244 | if (this.cascade != val) { 245 | this.cascade = val; 246 | this._cascadeGrid(); 247 | } 248 | break; 249 | case 'fix_to_grid': 250 | this._fixToGrid = val ? true : false; 251 | break; 252 | case 'maintain_ratio': 253 | this._maintainRatio = val ? true : false; 254 | break; 255 | case 'prefer_new': 256 | this._preferNew = val ? true : false; 257 | break; 258 | case 'limit_to_screen': 259 | this._limitToScreen = !this._autoResize && !!val; 260 | break; 261 | case 'center_to_screen': 262 | this._centerToScreen = val ? true : false; 263 | break; 264 | case 'resize_directions': 265 | this.resizeDirections = val || ['bottomright', 'bottomleft', 'topright', 'topleft', 'right', 'left', 'bottom', 'top']; 266 | break; 267 | case 'element_based_row_height': 268 | this._elementBasedDynamicRowHeight = !!val; 269 | break; 270 | case 'fix_item_position_direction': 271 | this._itemFixDirection = val; 272 | break; 273 | case 'fix_collision_position_direction': 274 | this._collisionFixDirection = val; 275 | break; 276 | case 'allow_overlap': 277 | this._allowOverlap = !!val; 278 | break; 279 | } 280 | } 281 | 282 | if (this._allowOverlap && this.cascade !== 'off' && this.cascade !== '') { 283 | console.warn('Unable to overlap items when a cascade direction is set.'); 284 | this._allowOverlap = false; 285 | } 286 | 287 | if (this.dragEnable || this.resizeEnable) { 288 | this._enableListeners(); 289 | } else { 290 | this._disableListeners(); 291 | } 292 | 293 | if (this._itemFixDirection === 'cascade') { 294 | this._itemFixDirection = this._getFixDirectionFromCascade(); 295 | } 296 | 297 | if (this._collisionFixDirection === 'cascade') { 298 | this._collisionFixDirection = this._getFixDirectionFromCascade(); 299 | } 300 | 301 | if (this._limitToScreen) { 302 | const newMaxCols = this._getContainerColumns(); 303 | 304 | if (this._maxCols != newMaxCols) { 305 | this._maxCols = newMaxCols; 306 | maxColRowChanged = true; 307 | } 308 | } 309 | 310 | if (this._limitToScreen && this._centerToScreen) { 311 | this.screenMargin = this._getScreenMargin(); 312 | } else { 313 | this.screenMargin = 0; 314 | } 315 | 316 | if (this._maintainRatio) { 317 | if (this.colWidth && this.rowHeight) { 318 | this._aspectRatio = this.colWidth / this.rowHeight; 319 | } else { 320 | this._maintainRatio = false; 321 | } 322 | } 323 | 324 | if (maxColRowChanged) { 325 | if (this._maxCols > 0 && this._maxRows > 0) { // Can't have both, prioritise on cascade 326 | switch (this.cascade) { 327 | case 'left': 328 | case 'right': 329 | this._maxCols = 0; 330 | break; 331 | case 'up': 332 | case 'down': 333 | default: 334 | this._maxRows = 0; 335 | break; 336 | } 337 | } 338 | 339 | this._updatePositionsAfterMaxChange(); 340 | } 341 | 342 | this._calculateColWidth(); 343 | this._calculateRowHeight(); 344 | 345 | var maxWidth = this._maxCols * this.colWidth; 346 | var maxHeight = this._maxRows * this.rowHeight; 347 | 348 | if (maxWidth > 0 && this.minWidth > maxWidth) this.minWidth = 0.75 * this.colWidth; 349 | if (maxHeight > 0 && this.minHeight > maxHeight) this.minHeight = 0.75 * this.rowHeight; 350 | 351 | if (this.minWidth > this.colWidth) this.minCols = Math.max(this.minCols, Math.ceil(this.minWidth / this.colWidth)); 352 | if (this.minHeight > this.rowHeight) this.minRows = Math.max(this.minRows, Math.ceil(this.minHeight / this.rowHeight)); 353 | 354 | if (this._maxCols > 0 && this.minCols > this._maxCols) this.minCols = 1; 355 | if (this._maxRows > 0 && this.minRows > this._maxRows) this.minRows = 1; 356 | 357 | this._updateRatio(); 358 | 359 | this._items.forEach((item: NgGridItem) => { 360 | this._removeFromGrid(item); 361 | item.setCascadeMode(this.cascade); 362 | }); 363 | 364 | this._items.forEach((item: NgGridItem) => { 365 | item.recalculateSelf(); 366 | this._addToGrid(item); 367 | }); 368 | 369 | this._cascadeGrid(); 370 | this._updateSize(); 371 | } 372 | 373 | public getItemPosition(itemId: string): NgGridItemPosition { 374 | return this._items.has(itemId) ? this._items.get(itemId).getGridPosition() : null; 375 | } 376 | 377 | public getItemSize(itemId: string): NgGridItemSize { 378 | return this._items.has(itemId) ? this._items.get(itemId).getSize() : null; 379 | } 380 | 381 | public ngDoCheck(): boolean { 382 | if (this._differ != null) { 383 | var changes = this._differ.diff(this._config); 384 | 385 | if (changes != null) { 386 | this._applyChanges(changes); 387 | 388 | return true; 389 | } 390 | } 391 | 392 | return false; 393 | } 394 | 395 | public setMargins(margins: Array): void { 396 | this.marginTop = Math.max(parseInt(margins[0]), 0); 397 | this.marginRight = margins.length >= 2 ? Math.max(parseInt(margins[1]), 0) : this.marginTop; 398 | this.marginBottom = margins.length >= 3 ? Math.max(parseInt(margins[2]), 0) : this.marginTop; 399 | this.marginLeft = margins.length >= 4 ? Math.max(parseInt(margins[3]), 0) : this.marginRight; 400 | } 401 | 402 | public enableDrag(): void { 403 | this.dragEnable = true; 404 | } 405 | 406 | public disableDrag(): void { 407 | this.dragEnable = false; 408 | } 409 | 410 | public enableResize(): void { 411 | this.resizeEnable = true; 412 | } 413 | 414 | public disableResize(): void { 415 | this.resizeEnable = false; 416 | } 417 | 418 | public addItem(ngItem: NgGridItem): void { 419 | ngItem.setCascadeMode(this.cascade); 420 | 421 | if (!this._preferNew) { 422 | var newPos = this._fixGridPosition(ngItem.getGridPosition(), ngItem.getSize()); 423 | ngItem.setGridPosition(newPos); 424 | } 425 | 426 | if (ngItem.uid === null || this._items.has(ngItem.uid)) { 427 | ngItem.uid = this.generateItemUid(); 428 | } 429 | 430 | this._items.set(ngItem.uid, ngItem); 431 | this._addToGrid(ngItem); 432 | 433 | this._updateSize(); 434 | 435 | this.triggerCascade().then(() => { 436 | ngItem.recalculateSelf(); 437 | ngItem.onCascadeEvent(); 438 | 439 | this._emitOnItemChange(); 440 | }); 441 | 442 | } 443 | 444 | public removeItem(ngItem: NgGridItem): void { 445 | this._removeFromGrid(ngItem); 446 | 447 | this._items.delete(ngItem.uid); 448 | 449 | if (this._destroyed) return; 450 | 451 | this.triggerCascade().then(() => { 452 | this._updateSize(); 453 | this._items.forEach((item: NgGridItem) => item.recalculateSelf()); 454 | this._emitOnItemChange(); 455 | }); 456 | } 457 | 458 | public updateItem(ngItem: NgGridItem): void { 459 | this._removeFromGrid(ngItem); 460 | this._addToGrid(ngItem); 461 | 462 | this.triggerCascade().then(() => { 463 | this._updateSize(); 464 | ngItem.onCascadeEvent(); 465 | }); 466 | } 467 | 468 | public triggerCascade(): Promise { 469 | if (!this._cascadePromise) { 470 | this._cascadePromise = new Promise((resolve: () => void) => { 471 | setTimeout(() => { 472 | this._cascadePromise = null; 473 | this._cascadeGrid(null, null); 474 | resolve(); 475 | }, 0); 476 | }); 477 | } 478 | 479 | return this._cascadePromise; 480 | } 481 | 482 | public triggerResize(): void { 483 | this.resizeEventHandler(null); 484 | } 485 | 486 | public resizeEventHandler(e: any): void { 487 | this._calculateColWidth(); 488 | this._calculateRowHeight(); 489 | 490 | this._updateRatio(); 491 | 492 | if (this._limitToScreen) { 493 | const newMaxColumns = this._getContainerColumns(); 494 | if (this._maxCols !== newMaxColumns) { 495 | this._maxCols = newMaxColumns; 496 | this._updatePositionsAfterMaxChange(); 497 | this._cascadeGrid(); 498 | } 499 | 500 | if (this._centerToScreen) { 501 | this.screenMargin = this._getScreenMargin(); 502 | 503 | this._items.forEach((item: NgGridItem) => { 504 | item.recalculateSelf(); 505 | }); 506 | } 507 | } else if (this._autoResize) { 508 | this._items.forEach((item: NgGridItem) => { 509 | item.recalculateSelf(); 510 | }); 511 | } 512 | 513 | this._updateSize(); 514 | } 515 | 516 | public mouseDownEventHandler(e: MouseEvent | TouchEvent): void { 517 | var mousePos = this._getMousePosition(e); 518 | var item = this._getItemFromPosition(mousePos); 519 | 520 | if (item == null) return; 521 | 522 | const resizeDirection: string = item.canResize(e); 523 | 524 | if (this.resizeEnable && resizeDirection) { 525 | this._resizeReady = true; 526 | this._resizingItem = item; 527 | this._resizeDirection = resizeDirection; 528 | 529 | e.preventDefault(); 530 | } else if (this.dragEnable && item.canDrag(e)) { 531 | this._dragReady = true; 532 | this._draggingItem = item; 533 | 534 | const itemPos = item.getPosition(); 535 | this._posOffset = { 'left': (mousePos.left - itemPos.left), 'top': (mousePos.top - itemPos.top) } 536 | 537 | e.preventDefault(); 538 | } 539 | } 540 | 541 | public mouseUpEventHandler(e: MouseEvent | TouchEvent): void { 542 | if (this.isDragging) { 543 | this._dragStop(e); 544 | } else if (this.isResizing) { 545 | this._resizeStop(e); 546 | } else if (this._dragReady || this._resizeReady) { 547 | this._cleanDrag(); 548 | this._cleanResize(); 549 | } 550 | } 551 | 552 | public mouseMoveEventHandler(e: MouseEvent | TouchEvent): void { 553 | if (this._resizeReady) { 554 | this._resizeStart(e); 555 | e.preventDefault(); 556 | return; 557 | } else if (this._dragReady) { 558 | this._dragStart(e); 559 | e.preventDefault(); 560 | return; 561 | } 562 | 563 | if (this.isDragging) { 564 | this._drag(e); 565 | } else if (this.isResizing) { 566 | this._resize(e); 567 | } else { 568 | var mousePos = this._getMousePosition(e); 569 | var item = this._getItemFromPosition(mousePos); 570 | 571 | if (item) { 572 | item.onMouseMove(e); 573 | } 574 | } 575 | } 576 | 577 | // Private methods 578 | private _getFixDirectionFromCascade(): NgConfigFixDirection { 579 | switch (this.cascade) { 580 | case 'up': 581 | case 'down': 582 | default: 583 | return 'vertical'; 584 | case 'left': 585 | case 'right': 586 | return 'horizontal'; 587 | } 588 | } 589 | private _updatePositionsAfterMaxChange(): void { 590 | this._items.forEach((item: NgGridItem) => { 591 | var pos = item.getGridPosition(); 592 | var dims = item.getSize(); 593 | 594 | if (!this._hasGridCollision(pos, dims) && this._isWithinBounds(pos, dims) && dims.x <= this._maxCols && dims.y <= this._maxRows) { 595 | return; 596 | } 597 | 598 | this._removeFromGrid(item); 599 | 600 | if (this._maxCols > 0 && dims.x > this._maxCols) { 601 | dims.x = this._maxCols; 602 | item.setSize(dims); 603 | } else if (this._maxRows > 0 && dims.y > this._maxRows) { 604 | dims.y = this._maxRows; 605 | item.setSize(dims); 606 | } 607 | 608 | if (this._hasGridCollision(pos, dims) || !this._isWithinBounds(pos, dims, true)) { 609 | var newPosition = this._fixGridPosition(pos, dims); 610 | item.setGridPosition(newPosition); 611 | } 612 | 613 | this._addToGrid(item); 614 | }); 615 | } 616 | 617 | private _calculateColWidth(): void { 618 | if (this._autoResize) { 619 | if (this._maxCols > 0 || this._visibleCols > 0) { 620 | var maxCols = this._maxCols > 0 ? this._maxCols : this._visibleCols; 621 | var maxWidth: number = this._ngEl.nativeElement.getBoundingClientRect().width; 622 | 623 | var colWidth: number = Math.floor(maxWidth / maxCols); 624 | colWidth -= (this.marginLeft + this.marginRight); 625 | if (colWidth > 0) this.colWidth = colWidth; 626 | 627 | } 628 | } 629 | 630 | if (this.colWidth < this.minWidth || this.minCols > this._config.min_cols) { 631 | this.minCols = Math.max(this._config.min_cols, Math.ceil(this.minWidth / this.colWidth)); 632 | } 633 | } 634 | 635 | private _calculateRowHeight(): void { 636 | if (this._autoResize) { 637 | if (this._maxRows > 0 || this._visibleRows > 0) { 638 | var maxRows = this._maxRows > 0 ? this._maxRows : this._visibleRows; 639 | let maxHeight: number; 640 | 641 | if (this._elementBasedDynamicRowHeight) { 642 | maxHeight = this._ngEl.nativeElement.getBoundingClientRect().height; 643 | } else { 644 | maxHeight = window.innerHeight - this.marginTop - this.marginBottom; 645 | } 646 | 647 | var rowHeight: number = Math.max(Math.floor(maxHeight / maxRows), this.minHeight); 648 | rowHeight -= (this.marginTop + this.marginBottom); 649 | if (rowHeight > 0) this.rowHeight = rowHeight; 650 | 651 | } 652 | } 653 | 654 | if (this.rowHeight < this.minHeight || this.minRows > this._config.min_rows) { 655 | this.minRows = Math.max(this._config.min_rows, Math.ceil(this.minHeight / this.rowHeight)); 656 | } 657 | } 658 | 659 | private _updateRatio(): void { 660 | if (!this._autoResize || !this._maintainRatio) return; 661 | 662 | if (this._maxCols > 0 && this._visibleRows <= 0) { 663 | this.rowHeight = this.colWidth / this._aspectRatio; 664 | } else if (this._maxRows > 0 && this._visibleCols <= 0) { 665 | this.colWidth = this._aspectRatio * this.rowHeight; 666 | } else if (this._maxCols == 0 && this._maxRows == 0) { 667 | if (this._visibleCols > 0) { 668 | this.rowHeight = this.colWidth / this._aspectRatio; 669 | } else if (this._visibleRows > 0) { 670 | this.colWidth = this._aspectRatio * this.rowHeight; 671 | } 672 | } 673 | } 674 | 675 | private _applyChanges(changes: any): void { 676 | changes.forEachAddedItem((record: any) => { this._config[record.key] = record.currentValue; }); 677 | changes.forEachChangedItem((record: any) => { this._config[record.key] = record.currentValue; }); 678 | changes.forEachRemovedItem((record: any) => { delete this._config[record.key]; }); 679 | 680 | this.setConfig(this._config); 681 | } 682 | 683 | private _resizeStart(e: any): void { 684 | if (!this.resizeEnable || !this._resizingItem) return; 685 | 686 | // Setup 687 | this._resizingItem.startMoving(); 688 | this._removeFromGrid(this._resizingItem); 689 | this._createPlaceholder(this._resizingItem); 690 | 691 | if (this._allowOverlap) { 692 | this._resizingItem.zIndex = this._lastZValue++; 693 | } 694 | 695 | // Status Flags 696 | this.isResizing = true; 697 | this._resizeReady = false; 698 | 699 | // Events 700 | this.onResizeStart.emit(this._resizingItem); 701 | this._resizingItem.onResizeStartEvent(); 702 | } 703 | 704 | private _dragStart(e: any): void { 705 | if (!this.dragEnable || !this._draggingItem) return; 706 | 707 | // Start dragging 708 | this._draggingItem.startMoving(); 709 | this._removeFromGrid(this._draggingItem); 710 | this._createPlaceholder(this._draggingItem); 711 | 712 | if (this._allowOverlap) { 713 | this._draggingItem.zIndex = this._lastZValue++; 714 | } 715 | 716 | // Status Flags 717 | this.isDragging = true; 718 | this._dragReady = false; 719 | 720 | // Events 721 | this.onDragStart.emit(this._draggingItem); 722 | this._draggingItem.onDragStartEvent(); 723 | 724 | // Zoom 725 | if (this._zoomOnDrag) { 726 | this._zoomOut(); 727 | } 728 | } 729 | 730 | private _zoomOut(): void { 731 | this._renderer.setStyle(this._ngEl.nativeElement, 'transform', 'scale(0.5, 0.5)'); 732 | } 733 | 734 | private _resetZoom(): void { 735 | this._renderer.setStyle(this._ngEl.nativeElement, 'transform', ''); 736 | } 737 | 738 | private _drag(e: any): void { 739 | if (!this.isDragging) return; 740 | 741 | if (window.getSelection) { 742 | if (window.getSelection().empty) { 743 | window.getSelection().empty(); 744 | } else if (window.getSelection().removeAllRanges) { 745 | window.getSelection().removeAllRanges(); 746 | } 747 | } else if ((document).selection) { 748 | (document).selection.empty(); 749 | } 750 | 751 | var mousePos = this._getMousePosition(e); 752 | var newL = (mousePos.left - this._posOffset.left); 753 | var newT = (mousePos.top - this._posOffset.top); 754 | 755 | var itemPos = this._draggingItem.getGridPosition(); 756 | var gridPos = this._calculateGridPosition(newL, newT); 757 | var dims = this._draggingItem.getSize(); 758 | 759 | gridPos = this._fixPosToBoundsX(gridPos, dims); 760 | 761 | if (!this._isWithinBoundsY(gridPos, dims)) { 762 | gridPos = this._fixPosToBoundsY(gridPos, dims); 763 | } 764 | 765 | if (gridPos.col != itemPos.col || gridPos.row != itemPos.row) { 766 | this._draggingItem.setGridPosition(gridPos, this._fixToGrid); 767 | this._placeholderRef.instance.setGridPosition(gridPos); 768 | 769 | if (['up', 'down', 'left', 'right'].indexOf(this.cascade) >= 0) { 770 | this._fixGridCollisions(gridPos, dims); 771 | this._cascadeGrid(gridPos, dims); 772 | } 773 | } 774 | 775 | if (!this._fixToGrid) { 776 | this._draggingItem.setPosition(newL, newT); 777 | } 778 | 779 | this.onDrag.emit(this._draggingItem); 780 | this._draggingItem.onDragEvent(); 781 | } 782 | 783 | private _resize(e: any): void { 784 | if (!this.isResizing) { return; } 785 | 786 | if (window.getSelection) { 787 | if (window.getSelection().empty) { 788 | window.getSelection().empty(); 789 | } else if (window.getSelection().removeAllRanges) { 790 | window.getSelection().removeAllRanges(); 791 | } 792 | } else if ((document).selection) { 793 | (document).selection.empty(); 794 | } 795 | 796 | const mousePos = this._getMousePosition(e); 797 | const itemPos = this._resizingItem.getPosition(); 798 | const itemDims = this._resizingItem.getDimensions(); 799 | const endCorner = { 800 | left: itemPos.left + itemDims.width, 801 | top: itemPos.top + itemDims.height, 802 | } 803 | 804 | const resizeTop = this._resizeDirection.includes('top'); 805 | const resizeBottom = this._resizeDirection.includes('bottom'); 806 | const resizeLeft = this._resizeDirection.includes('left') 807 | const resizeRight = this._resizeDirection.includes('right'); 808 | 809 | // Calculate new width and height based upon resize direction 810 | let newW = resizeRight 811 | ? (mousePos.left - itemPos.left + 1) 812 | : resizeLeft 813 | ? (endCorner.left - mousePos.left + 1) 814 | : itemDims.width; 815 | let newH = resizeBottom 816 | ? (mousePos.top - itemPos.top + 1) 817 | : resizeTop 818 | ? (endCorner.top - mousePos.top + 1) 819 | : itemDims.height; 820 | 821 | if (newW < this.minWidth) 822 | newW = this.minWidth; 823 | if (newH < this.minHeight) 824 | newH = this.minHeight; 825 | if (newW < this._resizingItem.minWidth) 826 | newW = this._resizingItem.minWidth; 827 | if (newH < this._resizingItem.minHeight) 828 | newH = this._resizingItem.minHeight; 829 | 830 | let newX = itemPos.left; 831 | let newY = itemPos.top; 832 | 833 | if (resizeLeft) 834 | newX = endCorner.left - newW; 835 | if (resizeTop) 836 | newY = endCorner.top - newH; 837 | 838 | let calcSize = this._calculateGridSize(newW, newH); 839 | const itemSize = this._resizingItem.getSize(); 840 | const iGridPos = this._resizingItem.getGridPosition(); 841 | const bottomRightCorner = { 842 | col: iGridPos.col + itemSize.x, 843 | row: iGridPos.row + itemSize.y, 844 | }; 845 | const targetPos: NgGridItemPosition = Object.assign({}, iGridPos); 846 | 847 | if (this._resizeDirection.includes('top')) 848 | targetPos.row = bottomRightCorner.row - calcSize.y; 849 | if (this._resizeDirection.includes('left')) 850 | targetPos.col = bottomRightCorner.col - calcSize.x; 851 | 852 | if (!this._isWithinBoundsX(targetPos, calcSize)) 853 | calcSize = this._fixSizeToBoundsX(targetPos, calcSize); 854 | 855 | if (!this._isWithinBoundsY(targetPos, calcSize)) 856 | calcSize = this._fixSizeToBoundsY(targetPos, calcSize); 857 | 858 | calcSize = this._resizingItem.fixResize(calcSize); 859 | 860 | if (calcSize.x != itemSize.x || calcSize.y != itemSize.y) { 861 | this._resizingItem.setGridPosition(targetPos, this._fixToGrid); 862 | this._placeholderRef.instance.setGridPosition(targetPos); 863 | this._resizingItem.setSize(calcSize, this._fixToGrid); 864 | this._placeholderRef.instance.setSize(calcSize); 865 | 866 | if (['up', 'down', 'left', 'right'].indexOf(this.cascade) >= 0) { 867 | this._fixGridCollisions(targetPos, calcSize); 868 | this._cascadeGrid(targetPos, calcSize); 869 | } 870 | } 871 | 872 | if (!this._fixToGrid) { 873 | this._resizingItem.setDimensions(newW, newH); 874 | this._resizingItem.setPosition(newX, newY); 875 | } 876 | 877 | this.onResize.emit(this._resizingItem); 878 | this._resizingItem.onResizeEvent(); 879 | } 880 | 881 | private _dragStop(e: any): void { 882 | if (!this.isDragging) return; 883 | 884 | this.isDragging = false; 885 | 886 | var itemPos = this._draggingItem.getGridPosition(); 887 | 888 | this._draggingItem.setGridPosition(itemPos); 889 | this._addToGrid(this._draggingItem); 890 | 891 | this._cascadeGrid(); 892 | this._updateSize(); 893 | 894 | this._draggingItem.stopMoving(); 895 | this._draggingItem.onDragStopEvent(); 896 | this.onDragStop.emit(this._draggingItem); 897 | 898 | this._cleanDrag(); 899 | this._placeholderRef.destroy(); 900 | 901 | this._emitOnItemChange(); 902 | 903 | if (this._zoomOnDrag) { 904 | this._resetZoom(); 905 | } 906 | } 907 | 908 | private _resizeStop(e: any): void { 909 | if (!this.isResizing) return; 910 | 911 | this.isResizing = false; 912 | 913 | const itemDims = this._resizingItem.getSize(); 914 | this._resizingItem.setSize(itemDims); 915 | 916 | const itemPos = this._resizingItem.getGridPosition(); 917 | this._resizingItem.setGridPosition(itemPos); 918 | 919 | this._addToGrid(this._resizingItem); 920 | 921 | this._cascadeGrid(); 922 | this._updateSize(); 923 | 924 | this._resizingItem.stopMoving(); 925 | this._resizingItem.onResizeStopEvent(); 926 | this.onResizeStop.emit(this._resizingItem); 927 | 928 | this._cleanResize(); 929 | this._placeholderRef.destroy(); 930 | 931 | this._emitOnItemChange(); 932 | } 933 | 934 | private _cleanDrag(): void { 935 | this._draggingItem = null; 936 | this._posOffset = null; 937 | this.isDragging = false; 938 | this._dragReady = false; 939 | } 940 | 941 | private _cleanResize(): void { 942 | this._resizingItem = null; 943 | this._resizeDirection = null; 944 | this.isResizing = false; 945 | this._resizeReady = false; 946 | } 947 | 948 | private _calculateGridSize(width: number, height: number): NgGridItemSize { 949 | width += this.marginLeft + this.marginRight; 950 | height += this.marginTop + this.marginBottom; 951 | 952 | var sizex = Math.max(this.minCols, Math.round(width / (this.colWidth + this.marginLeft + this.marginRight))); 953 | var sizey = Math.max(this.minRows, Math.round(height / (this.rowHeight + this.marginTop + this.marginBottom))); 954 | 955 | if (!this._isWithinBoundsX({ col: 1, row: 1 }, { x: sizex, y: sizey })) sizex = this._maxCols; 956 | if (!this._isWithinBoundsY({ col: 1, row: 1 }, { x: sizex, y: sizey })) sizey = this._maxRows; 957 | 958 | return { 'x': sizex, 'y': sizey }; 959 | } 960 | 961 | private _calculateGridPosition(left: number, top: number): NgGridItemPosition { 962 | var col = Math.max(1, Math.round(left / (this.colWidth + this.marginLeft + this.marginRight)) + 1); 963 | var row = Math.max(1, Math.round(top / (this.rowHeight + this.marginTop + this.marginBottom)) + 1); 964 | 965 | if (!this._isWithinBoundsX({ col: col, row: row }, { x: 1, y: 1 })) col = this._maxCols; 966 | if (!this._isWithinBoundsY({ col: col, row: row }, { x: 1, y: 1 })) row = this._maxRows; 967 | 968 | return { 'col': col, 'row': row }; 969 | } 970 | 971 | private _hasGridCollision(pos: NgGridItemPosition, dims: NgGridItemSize): boolean { 972 | var positions = this._getCollisions(pos, dims); 973 | 974 | if (positions == null || positions.length == 0) return false; 975 | 976 | return positions.some((v: NgGridItem) => { 977 | return !(v === null); 978 | }); 979 | } 980 | 981 | private _getCollisions(pos: NgGridItemPosition, dims: NgGridItemSize): Array { 982 | if (this._allowOverlap) return []; 983 | 984 | const returns: Array = []; 985 | 986 | if (!pos.col) { pos.col = 1; } 987 | if (!pos.row) { pos.row = 1; } 988 | 989 | const leftCol = pos.col; 990 | const rightCol = pos.col + dims.x; 991 | const topRow = pos.row; 992 | const bottomRow = pos.row + dims.y; 993 | 994 | this._itemsInGrid.forEach((itemId: string) => { 995 | const item: NgGridItem = this._items.get(itemId); 996 | 997 | if (!item) { 998 | this._itemsInGrid.delete(itemId); 999 | return; 1000 | } 1001 | 1002 | const itemLeftCol = item.col; 1003 | const itemRightCol = item.col + item.sizex; 1004 | const itemTopRow = item.row; 1005 | const itemBottomRow = item.row + item.sizey; 1006 | 1007 | const withinColumns = leftCol < itemRightCol && itemLeftCol < rightCol; 1008 | const withinRows = topRow < itemBottomRow && itemTopRow < bottomRow; 1009 | 1010 | if (withinColumns && withinRows) { 1011 | returns.push(item); 1012 | } 1013 | }); 1014 | 1015 | return returns; 1016 | } 1017 | 1018 | private _fixGridCollisions(pos: NgGridItemPosition, dims: NgGridItemSize): void { 1019 | const collisions: Array = this._getCollisions(pos, dims); 1020 | if (collisions.length === 0) { return; } 1021 | 1022 | for (let collision of collisions) { 1023 | this._removeFromGrid(collision); 1024 | 1025 | const itemDims: NgGridItemSize = collision.getSize(); 1026 | const itemPos: NgGridItemPosition = collision.getGridPosition(); 1027 | let newItemPos: NgGridItemPosition = { col: itemPos.col, row: itemPos.row }; 1028 | 1029 | if (this._collisionFixDirection === 'vertical') { 1030 | newItemPos.row = pos.row + dims.y; 1031 | 1032 | if (!this._isWithinBoundsY(newItemPos, itemDims)) { 1033 | newItemPos.col = pos.col + dims.x; 1034 | newItemPos.row = 1; 1035 | } 1036 | } else if (this._collisionFixDirection === 'horizontal') { 1037 | newItemPos.col = pos.col + dims.x; 1038 | 1039 | if (!this._isWithinBoundsX(newItemPos, itemDims)) { 1040 | newItemPos.col = 1; 1041 | newItemPos.row = pos.row + dims.y; 1042 | } 1043 | } 1044 | 1045 | collision.setGridPosition(newItemPos); 1046 | 1047 | this._fixGridCollisions(newItemPos, itemDims); 1048 | this._addToGrid(collision); 1049 | collision.onCascadeEvent(); 1050 | } 1051 | 1052 | this._fixGridCollisions(pos, dims); 1053 | } 1054 | 1055 | private _cascadeGrid(pos?: NgGridItemPosition, dims?: NgGridItemSize): void { 1056 | if (this._destroyed) return; 1057 | if (this._allowOverlap) return; 1058 | if (!pos !== !dims) throw new Error('Cannot cascade with only position and not dimensions'); 1059 | 1060 | if (this.isDragging && this._draggingItem && !pos && !dims) { 1061 | pos = this._draggingItem.getGridPosition(); 1062 | dims = this._draggingItem.getSize(); 1063 | } else if (this.isResizing && this._resizingItem && !pos && !dims) { 1064 | pos = this._resizingItem.getGridPosition(); 1065 | dims = this._resizingItem.getSize(); 1066 | } 1067 | 1068 | let itemsInGrid: NgGridItem[] = Array.from(this._itemsInGrid, (itemId: string) => this._items.get(itemId)); 1069 | 1070 | switch (this.cascade) { 1071 | case 'up': 1072 | case 'down': 1073 | itemsInGrid = itemsInGrid.sort(NgGridHelper.sortItemsByPositionVertical); 1074 | const lowestRowPerColumn: Map = new Map(); 1075 | 1076 | for (let item of itemsInGrid) { 1077 | if (item.isFixed) continue; 1078 | 1079 | const itemDims: NgGridItemSize = item.getSize(); 1080 | const itemPos: NgGridItemPosition = item.getGridPosition(); 1081 | 1082 | let lowestRowForItem: number = lowestRowPerColumn.get(itemPos.col) || 1; 1083 | 1084 | for (let i: number = 1; i < itemDims.x; i++) { 1085 | const lowestRowForColumn = lowestRowPerColumn.get(itemPos.col + i) || 1; 1086 | lowestRowForItem = Math.max(lowestRowForColumn, lowestRowForItem); 1087 | } 1088 | 1089 | const leftCol = itemPos.col; 1090 | const rightCol = itemPos.col + itemDims.x; 1091 | 1092 | if (pos && dims) { 1093 | const withinColumns = rightCol > pos.col && leftCol < (pos.col + dims.x); 1094 | 1095 | if (withinColumns) { // If our element is in one of the item's columns 1096 | const roomAboveItem = itemDims.y <= (pos.row - lowestRowForItem); 1097 | 1098 | if (!roomAboveItem) { // Item can't fit above our element 1099 | lowestRowForItem = Math.max(lowestRowForItem, pos.row + dims.y); // Set the lowest row to be below it 1100 | } 1101 | } 1102 | } 1103 | 1104 | const newPos: NgGridItemPosition = { col: itemPos.col, row: lowestRowForItem }; 1105 | 1106 | // What if it's not within bounds Y? 1107 | if (lowestRowForItem != itemPos.row && this._isWithinBoundsY(newPos, itemDims)) { // If the item is not already on this row move it up 1108 | this._removeFromGrid(item); 1109 | 1110 | item.setGridPosition(newPos); 1111 | 1112 | item.onCascadeEvent(); 1113 | this._addToGrid(item); 1114 | } 1115 | 1116 | for (let i: number = 0; i < itemDims.x; i++) { 1117 | lowestRowPerColumn.set(itemPos.col + i, lowestRowForItem + itemDims.y); // Update the lowest row to be below the item 1118 | } 1119 | } 1120 | break; 1121 | case 'left': 1122 | case 'right': 1123 | itemsInGrid = itemsInGrid.sort(NgGridHelper.sortItemsByPositionHorizontal); 1124 | const lowestColumnPerRow: Map = new Map(); 1125 | 1126 | for (let item of itemsInGrid) { 1127 | const itemDims: NgGridItemSize = item.getSize(); 1128 | const itemPos: NgGridItemPosition = item.getGridPosition(); 1129 | 1130 | let lowestColumnForItem: number = lowestColumnPerRow.get(itemPos.row) || 1; 1131 | 1132 | for (let i: number = 1; i < itemDims.y; i++) { 1133 | let lowestOffsetColumn: number = lowestColumnPerRow.get(itemPos.row + i) || 1; 1134 | lowestColumnForItem = Math.max(lowestOffsetColumn, lowestColumnForItem); 1135 | } 1136 | 1137 | const topRow = itemPos.row; 1138 | const bottomRow = itemPos.row + itemDims.y; 1139 | 1140 | if (pos && dims) { 1141 | const withinRows = bottomRow > pos.col && topRow < (pos.col + dims.x); 1142 | 1143 | if (withinRows) { // If our element is in one of the item's rows 1144 | const roomNextToItem = itemDims.x <= (pos.col - lowestColumnForItem); 1145 | 1146 | if (!roomNextToItem) { // Item can't fit next to our element 1147 | lowestColumnForItem = Math.max(lowestColumnForItem, pos.col + dims.x); // Set the lowest col to be the other side of it 1148 | } 1149 | } 1150 | } 1151 | 1152 | const newPos: NgGridItemPosition = { col: lowestColumnForItem, row: itemPos.row }; 1153 | 1154 | if (lowestColumnForItem != itemPos.col && this._isWithinBoundsX(newPos, itemDims)) { // If the item is not already on this col move it up 1155 | this._removeFromGrid(item); 1156 | 1157 | item.setGridPosition(newPos); 1158 | 1159 | item.onCascadeEvent(); 1160 | this._addToGrid(item); 1161 | } 1162 | 1163 | for (let i: number = 0; i < itemDims.y; i++) { 1164 | lowestColumnPerRow.set(itemPos.row + i, lowestColumnForItem + itemDims.x); // Update the lowest col to be below the item 1165 | } 1166 | } 1167 | break; 1168 | default: 1169 | break; 1170 | } 1171 | } 1172 | 1173 | private _fixGridPosition(pos: NgGridItemPosition, dims: NgGridItemSize): NgGridItemPosition { 1174 | if (!this._hasGridCollision(pos, dims)) return pos; 1175 | 1176 | const maxRow = this._maxRows === 0 ? this._getMaxRow() : this._maxRows; 1177 | const maxCol = this._maxCols === 0 ? this._getMaxCol() : this._maxCols; 1178 | const newPos = { 1179 | col: pos.col, 1180 | row: pos.row, 1181 | }; 1182 | 1183 | if (this._itemFixDirection === 'vertical') { 1184 | fixLoop: 1185 | for (; newPos.col <= maxRow;) { 1186 | const itemsInPath = this._getItemsInVerticalPath(newPos, dims, newPos.row); 1187 | let nextRow = newPos.row; 1188 | 1189 | for (let item of itemsInPath) { 1190 | if (item.row - nextRow >= dims.y) { 1191 | newPos.row = nextRow; 1192 | break fixLoop; 1193 | } 1194 | 1195 | nextRow = item.row + item.sizey; 1196 | } 1197 | 1198 | if (maxRow - nextRow >= dims.y) { 1199 | newPos.row = nextRow; 1200 | break fixLoop; 1201 | } 1202 | 1203 | newPos.col = Math.max(newPos.col + 1, Math.min.apply(Math, itemsInPath.map((item) => item.col + dims.x))); 1204 | newPos.row = 1; 1205 | } 1206 | } else if (this._itemFixDirection === 'horizontal') { 1207 | fixLoop: 1208 | for (; newPos.row <= maxRow;) { 1209 | const itemsInPath = this._getItemsInHorizontalPath(newPos, dims, newPos.col); 1210 | let nextCol = newPos.col; 1211 | 1212 | for (let item of itemsInPath) { 1213 | if (item.col - nextCol >= dims.x) { 1214 | newPos.col = nextCol; 1215 | break fixLoop; 1216 | } 1217 | 1218 | nextCol = item.col + item.sizex; 1219 | } 1220 | 1221 | if (maxCol - nextCol >= dims.x) { 1222 | newPos.col = nextCol; 1223 | break fixLoop; 1224 | } 1225 | 1226 | newPos.row = Math.max(newPos.row + 1, Math.min.apply(Math, itemsInPath.map((item) => item.row + dims.y))); 1227 | newPos.col = 1; 1228 | } 1229 | } 1230 | 1231 | return newPos; 1232 | } 1233 | 1234 | private _getItemsInHorizontalPath(pos: NgGridItemPosition, dims: NgGridItemSize, startColumn: number = 0): NgGridItem[] { 1235 | const itemsInPath: NgGridItem[] = []; 1236 | const topRow: number = pos.row + dims.y - 1; 1237 | 1238 | this._itemsInGrid.forEach((itemId: string) => { 1239 | const item = this._items.get(itemId); 1240 | if (item.col + item.sizex - 1 < startColumn) { return; } // Item falls after start column 1241 | if (item.row > topRow) { return; } // Item falls above path 1242 | if (item.row + item.sizey - 1 < pos.row) { return; } // Item falls below path 1243 | itemsInPath.push(item); 1244 | }); 1245 | 1246 | return itemsInPath; 1247 | } 1248 | 1249 | private _getItemsInVerticalPath(pos: NgGridItemPosition, dims: NgGridItemSize, startRow: number = 0): NgGridItem[] { 1250 | const itemsInPath: NgGridItem[] = []; 1251 | const rightCol: number = pos.col + dims.x - 1; 1252 | 1253 | this._itemsInGrid.forEach((itemId: string) => { 1254 | const item = this._items.get(itemId); 1255 | if (item.row + item.sizey - 1 < startRow) { return; } // Item falls above start row 1256 | if (item.col > rightCol) { return; } // Item falls after path 1257 | if (item.col + item.sizex - 1 < pos.col) { return; } // Item falls before path 1258 | itemsInPath.push(item); 1259 | }); 1260 | 1261 | return itemsInPath; 1262 | } 1263 | 1264 | private _isWithinBoundsX(pos: NgGridItemPosition, dims: NgGridItemSize, allowExcessiveItems: boolean = false) { 1265 | return this._maxCols == 0 || (allowExcessiveItems && pos.col == 1) || (pos.col + dims.x - 1) <= this._maxCols; 1266 | } 1267 | 1268 | private _fixPosToBoundsX(pos: NgGridItemPosition, dims: NgGridItemSize): NgGridItemPosition { 1269 | if (!this._isWithinBoundsX(pos, dims)) { 1270 | pos.col = Math.max(this._maxCols - (dims.x - 1), 1); 1271 | pos.row ++; 1272 | } 1273 | return pos; 1274 | } 1275 | 1276 | private _fixSizeToBoundsX(pos: NgGridItemPosition, dims: NgGridItemSize): NgGridItemSize { 1277 | if (!this._isWithinBoundsX(pos, dims)) { 1278 | dims.x = Math.max(this._maxCols - (pos.col - 1), 1); 1279 | dims.y++; 1280 | } 1281 | return dims; 1282 | } 1283 | 1284 | private _isWithinBoundsY(pos: NgGridItemPosition, dims: NgGridItemSize, allowExcessiveItems: boolean = false) { 1285 | return this._maxRows == 0 || (allowExcessiveItems && pos.row == 1) || (pos.row + dims.y - 1) <= this._maxRows; 1286 | } 1287 | 1288 | private _fixPosToBoundsY(pos: NgGridItemPosition, dims: NgGridItemSize): NgGridItemPosition { 1289 | if (!this._isWithinBoundsY(pos, dims)) { 1290 | pos.row = Math.max(this._maxRows - (dims.y - 1), 1); 1291 | pos.col++; 1292 | } 1293 | return pos; 1294 | } 1295 | 1296 | private _fixSizeToBoundsY(pos: NgGridItemPosition, dims: NgGridItemSize): NgGridItemSize { 1297 | if (!this._isWithinBoundsY(pos, dims)) { 1298 | dims.y = Math.max(this._maxRows - (pos.row - 1), 1); 1299 | dims.x++; 1300 | } 1301 | return dims; 1302 | } 1303 | 1304 | private _isWithinBounds(pos: NgGridItemPosition, dims: NgGridItemSize, allowExcessiveItems: boolean = false) { 1305 | return this._isWithinBoundsX(pos, dims, allowExcessiveItems) && this._isWithinBoundsY(pos, dims, allowExcessiveItems); 1306 | } 1307 | 1308 | private _fixPosToBounds(pos: NgGridItemPosition, dims: NgGridItemSize): NgGridItemPosition { 1309 | return this._fixPosToBoundsX(this._fixPosToBoundsY(pos, dims), dims); 1310 | } 1311 | 1312 | private _fixSizeToBounds(pos: NgGridItemPosition, dims: NgGridItemSize): NgGridItemSize { 1313 | return this._fixSizeToBoundsX(pos, this._fixSizeToBoundsY(pos, dims)); 1314 | } 1315 | 1316 | private _addToGrid(item: NgGridItem): void { 1317 | let pos: NgGridItemPosition = item.getGridPosition(); 1318 | const dims: NgGridItemSize = item.getSize(); 1319 | 1320 | if (this._hasGridCollision(pos, dims)) { 1321 | this._fixGridCollisions(pos, dims); 1322 | pos = item.getGridPosition(); 1323 | } 1324 | 1325 | if (this._allowOverlap) { 1326 | item.zIndex = this._lastZValue++; 1327 | } 1328 | 1329 | this._itemsInGrid.add(item.uid); 1330 | } 1331 | 1332 | private _removeFromGrid(item: NgGridItem): void { 1333 | this._itemsInGrid.delete(item.uid); 1334 | } 1335 | 1336 | private _updateSize(): void { 1337 | if (this._destroyed) return; 1338 | let maxCol: number = this._getMaxCol(); 1339 | let maxRow: number = this._getMaxRow(); 1340 | 1341 | if (maxCol != this._curMaxCol || maxRow != this._curMaxRow) { 1342 | this._curMaxCol = maxCol; 1343 | this._curMaxRow = maxRow; 1344 | } 1345 | 1346 | this._renderer.setStyle(this._ngEl.nativeElement, 'width', '100%');//(maxCol * (this.colWidth + this.marginLeft + this.marginRight))+'px'); 1347 | if (!this._elementBasedDynamicRowHeight) { 1348 | this._renderer.setStyle(this._ngEl.nativeElement, 'height', (maxRow * (this.rowHeight + this.marginTop + this.marginBottom)) + 'px'); 1349 | } 1350 | } 1351 | 1352 | private _getMaxRow(): number { 1353 | const itemsRows: number[] = Array.from(this._itemsInGrid, (itemId: string) => { 1354 | const item = this._items.get(itemId); 1355 | if (!item) return 0; 1356 | return item.row + item.sizey - 1; 1357 | }); 1358 | 1359 | return Math.max.apply(null, itemsRows); 1360 | } 1361 | 1362 | private _getMaxCol(): number { 1363 | const itemsCols: number[] = Array.from(this._itemsInGrid, (itemId: string) => { 1364 | const item = this._items.get(itemId); 1365 | if (!item) return 0; 1366 | return item.col + item.sizex - 1; 1367 | }); 1368 | 1369 | return Math.max.apply(null, itemsCols); 1370 | } 1371 | 1372 | private _getMousePosition(e: any): NgGridRawPosition { 1373 | if (((window).TouchEvent && e instanceof TouchEvent) || (e.touches || e.changedTouches)) { 1374 | e = e.touches.length > 0 ? e.touches[0] : e.changedTouches[0]; 1375 | } 1376 | 1377 | const refPos: any = this._ngEl.nativeElement.getBoundingClientRect(); 1378 | 1379 | let left: number = e.clientX - refPos.left; 1380 | let top: number = e.clientY - refPos.top; 1381 | 1382 | if (this.cascade == 'down') top = refPos.top + refPos.height - e.clientY; 1383 | if (this.cascade == 'right') left = refPos.left + refPos.width - e.clientX; 1384 | 1385 | if (this.isDragging && this._zoomOnDrag) { 1386 | left *= 2; 1387 | top *= 2; 1388 | } 1389 | 1390 | return { 1391 | left: left, 1392 | top: top 1393 | }; 1394 | } 1395 | 1396 | private _getAbsoluteMousePosition(e: any): NgGridRawPosition { 1397 | if (((window).TouchEvent && e instanceof TouchEvent) || (e.touches || e.changedTouches)) { 1398 | e = e.touches.length > 0 ? e.touches[0] : e.changedTouches[0]; 1399 | } 1400 | 1401 | return { 1402 | left: e.clientX, 1403 | top: e.clientY 1404 | }; 1405 | } 1406 | 1407 | private _getContainerColumns(): number { 1408 | const maxWidth: number = this._ngEl.nativeElement.getBoundingClientRect().width; 1409 | const itemWidth: number = this.colWidth + this.marginLeft + this.marginRight; 1410 | return Math.floor(maxWidth / itemWidth); 1411 | } 1412 | 1413 | private _getContainerRows(): number { 1414 | const maxHeight: number = window.innerHeight - this.marginTop - this.marginBottom; 1415 | return Math.floor(maxHeight / (this.rowHeight + this.marginTop + this.marginBottom)); 1416 | } 1417 | 1418 | private _getScreenMargin(): number { 1419 | const maxWidth: number = this._ngEl.nativeElement.getBoundingClientRect().width; 1420 | const itemWidth: number = this.colWidth + this.marginLeft + this.marginRight; 1421 | return Math.floor((maxWidth - (this._maxCols * itemWidth)) / 2);; 1422 | } 1423 | 1424 | private _getItemFromPosition(position: NgGridRawPosition): NgGridItem { 1425 | return Array.from(this._itemsInGrid, (itemId: string) => this._items.get(itemId)).find((item: NgGridItem) => { 1426 | if (!item) return false; 1427 | 1428 | const size: NgGridItemDimensions = item.getDimensions(); 1429 | const pos: NgGridRawPosition = item.getPosition(); 1430 | 1431 | return position.left >= pos.left && position.left < (pos.left + size.width) && 1432 | position.top >= pos.top && position.top < (pos.top + size.height); 1433 | }); 1434 | } 1435 | 1436 | private _createPlaceholder(item: NgGridItem): void { 1437 | const pos: NgGridItemPosition = item.getGridPosition(); 1438 | const dims: NgGridItemSize = item.getSize(); 1439 | 1440 | const factory = this.componentFactoryResolver.resolveComponentFactory(NgGridPlaceholder); 1441 | var componentRef: ComponentRef = item.containerRef.createComponent(factory); 1442 | this._placeholderRef = componentRef; 1443 | const placeholder: NgGridPlaceholder = componentRef.instance; 1444 | placeholder.registerGrid(this); 1445 | placeholder.setCascadeMode(this.cascade); 1446 | placeholder.setGridPosition({ col: pos.col, row: pos.row }); 1447 | placeholder.setSize({ x: dims.x, y: dims.y }); 1448 | } 1449 | 1450 | private _emitOnItemChange() { 1451 | const itemOutput: any[] = Array.from(this._itemsInGrid) 1452 | .map((itemId: string) => this._items.get(itemId)) 1453 | .filter((item: NgGridItem) => !!item) 1454 | .map((item: NgGridItem) => item.getEventOutput()); 1455 | 1456 | this.onItemChange.emit(itemOutput); 1457 | } 1458 | 1459 | private _defineListeners(): void { 1460 | const element = this._ngEl.nativeElement; 1461 | 1462 | this._documentMousemove$ = fromEvent(document, 'mousemove'); 1463 | this._documentMouseup$ = fromEvent(document, 'mouseup'); 1464 | this._mousedown$ = fromEvent(element, 'mousedown'); 1465 | this._mousemove$ = fromEvent(element, 'mousemove'); 1466 | this._mouseup$ = fromEvent(element, 'mouseup'); 1467 | this._touchstart$ = fromEvent(element, 'touchstart'); 1468 | this._touchmove$ = fromEvent(element, 'touchmove'); 1469 | this._touchend$ = fromEvent(element, 'touchend'); 1470 | } 1471 | 1472 | private _enableListeners(): void { 1473 | if (this._enabledListener) { 1474 | return; 1475 | } 1476 | 1477 | this._enableMouseListeners(); 1478 | 1479 | if (this._isTouchDevice()) { 1480 | this._enableTouchListeners(); 1481 | } 1482 | 1483 | this._enabledListener = true; 1484 | } 1485 | 1486 | private _disableListeners(): void { 1487 | this._subscriptions.forEach((subs: Subscription) => subs.unsubscribe()); 1488 | this._enabledListener = false; 1489 | } 1490 | 1491 | private _isTouchDevice(): boolean { 1492 | return 'ontouchstart' in window || navigator.maxTouchPoints > 0; 1493 | }; 1494 | 1495 | private _enableTouchListeners(): void { 1496 | const touchstartSubs = this._touchstart$.subscribe((e: TouchEvent) => this.mouseDownEventHandler(e)); 1497 | const touchmoveSubs = this._touchmove$.subscribe((e: TouchEvent) => this.mouseMoveEventHandler(e)); 1498 | const touchendSubs = this._touchend$.subscribe((e: TouchEvent) => this.mouseUpEventHandler(e)); 1499 | 1500 | this._subscriptions.push( 1501 | touchstartSubs, 1502 | touchmoveSubs, 1503 | touchendSubs 1504 | ); 1505 | } 1506 | 1507 | private _enableMouseListeners(): void { 1508 | const documentMousemoveSubs = this._documentMousemove$.subscribe((e: MouseEvent) => this.mouseMoveEventHandler(e)); 1509 | const documentMouseupSubs = this._documentMouseup$.subscribe((e: MouseEvent) => this.mouseUpEventHandler(e)); 1510 | const mousedownSubs = this._mousedown$.subscribe((e: MouseEvent) => this.mouseDownEventHandler(e)); 1511 | const mousemoveSubs = this._mousemove$.subscribe((e: MouseEvent) => this.mouseMoveEventHandler(e)); 1512 | const mouseupSubs = this._mouseup$.subscribe((e: MouseEvent) => this.mouseUpEventHandler(e)); 1513 | 1514 | this._subscriptions.push( 1515 | documentMousemoveSubs, 1516 | documentMouseupSubs, 1517 | mousedownSubs, 1518 | mousemoveSubs, 1519 | mouseupSubs 1520 | ); 1521 | } 1522 | } 1523 | -------------------------------------------------------------------------------- /projects/angular2-grid/src/directives/NgGridItem.ts: -------------------------------------------------------------------------------- 1 | import { NgGrid } from './NgGrid'; 2 | import { NgGridItemConfig, NgGridItemEvent, NgGridItemPosition, NgGridItemSize, NgGridRawPosition, NgGridItemDimensions, ResizeHandle } from '../interfaces/INgGrid'; 3 | import { Directive, ElementRef, Renderer2, EventEmitter, KeyValueDiffer, KeyValueDiffers, OnInit, OnDestroy, ViewContainerRef, Output, DoCheck } from '@angular/core'; 4 | 5 | @Directive({ 6 | selector: '[ngGridItem]', 7 | inputs: ['config: ngGridItem'] 8 | }) 9 | export class NgGridItem implements OnInit, OnDestroy, DoCheck { 10 | // Event Emitters 11 | @Output() public onItemChange: EventEmitter = new EventEmitter(false); 12 | @Output() public onDragStart: EventEmitter = new EventEmitter(); 13 | @Output() public onDrag: EventEmitter = new EventEmitter(); 14 | @Output() public onDragStop: EventEmitter = new EventEmitter(); 15 | @Output() public onDragAny: EventEmitter = new EventEmitter(); 16 | @Output() public onResizeStart: EventEmitter = new EventEmitter(); 17 | @Output() public onResize: EventEmitter = new EventEmitter(); 18 | @Output() public onResizeStop: EventEmitter = new EventEmitter(); 19 | @Output() public onResizeAny: EventEmitter = new EventEmitter(); 20 | @Output() public onChangeStart: EventEmitter = new EventEmitter(); 21 | @Output() public onChange: EventEmitter = new EventEmitter(); 22 | @Output() public onChangeStop: EventEmitter = new EventEmitter(); 23 | @Output() public onChangeAny: EventEmitter = new EventEmitter(); 24 | @Output() public ngGridItemChange: EventEmitter = new EventEmitter(); 25 | 26 | // Default config 27 | private static CONST_DEFAULT_CONFIG: NgGridItemConfig = { 28 | uid: null, 29 | col: 1, 30 | row: 1, 31 | sizex: 1, 32 | sizey: 1, 33 | dragHandle: null, 34 | resizeHandle: null, 35 | fixed: false, 36 | draggable: true, 37 | resizable: true, 38 | borderSize: 25, 39 | resizeDirections: null, 40 | }; 41 | 42 | public isFixed: boolean = false; 43 | public isDraggable: boolean = true; 44 | public isResizable: boolean = true; 45 | public minWidth: number = 0; 46 | public minHeight: number = 0; 47 | public uid: string = null; 48 | 49 | // Private variables 50 | private _payload: any; 51 | private _currentPosition: NgGridItemPosition = { col: 1, row: 1 }; 52 | private _size: NgGridItemSize = { x: 1, y: 1 }; 53 | private _config = NgGridItem.CONST_DEFAULT_CONFIG; 54 | private _userConfig = null; 55 | private _dragHandle: string; 56 | private _resizeHandle: ResizeHandle; 57 | private _borderSize: number; 58 | private _elemWidth: number; 59 | private _elemHeight: number; 60 | private _elemLeft: number; 61 | private _elemTop: number; 62 | private _added: boolean = false; 63 | private _differ: KeyValueDiffer; 64 | private _cascadeMode: string; 65 | private _maxCols: number = 0; 66 | private _minCols: number = 0; 67 | private _maxRows: number = 0; 68 | private _minRows: number = 0; 69 | private _resizeDirections: string[] = []; 70 | private _zIndex: number = 0; 71 | 72 | set zIndex(zIndex: number) { 73 | this._renderer.setStyle(this._ngEl.nativeElement, 'z-index', zIndex.toString()); 74 | this._zIndex = zIndex; 75 | } 76 | 77 | get zIndex(): number { 78 | return this._zIndex; 79 | } 80 | 81 | // [ng-grid-item] handler 82 | set config(v: NgGridItemConfig) { 83 | this._userConfig = v; 84 | 85 | const configObject = Object.assign({}, NgGridItem.CONST_DEFAULT_CONFIG, v); 86 | for (let x in NgGridItem.CONST_DEFAULT_CONFIG) 87 | if (configObject[x] == null) 88 | configObject[x] = NgGridItem.CONST_DEFAULT_CONFIG[x]; 89 | 90 | this.setConfig(configObject); 91 | 92 | if (this._userConfig != null) { 93 | if (this._differ == null) { 94 | this._differ = this._differs.find(this._userConfig).create(); 95 | } 96 | 97 | this._differ.diff(this._userConfig); 98 | } 99 | 100 | if (!this._added) { 101 | this._added = true; 102 | this._ngGrid.addItem(this); 103 | } 104 | 105 | this._recalculateDimensions(); 106 | this._recalculatePosition(); 107 | } 108 | 109 | get sizex(): number { 110 | return this._size.x; 111 | } 112 | 113 | get sizey(): number { 114 | return this._size.y; 115 | } 116 | 117 | get col(): number { 118 | return this._currentPosition.col; 119 | } 120 | 121 | get row(): number { 122 | return this._currentPosition.row; 123 | } 124 | 125 | get currentCol(): number { 126 | return this._currentPosition.col; 127 | } 128 | 129 | get currentRow(): number { 130 | return this._currentPosition.row; 131 | } 132 | 133 | // Constructor 134 | constructor( 135 | private _differs: KeyValueDiffers, 136 | private _ngEl: ElementRef, 137 | private _renderer: Renderer2, 138 | private _ngGrid: NgGrid, 139 | public containerRef: ViewContainerRef, 140 | ) { } 141 | 142 | public onResizeStartEvent(): void { 143 | const event: NgGridItemEvent = this.getEventOutput(); 144 | this.onResizeStart.emit(event); 145 | this.onResizeAny.emit(event); 146 | this.onChangeStart.emit(event); 147 | this.onChangeAny.emit(event); 148 | } 149 | public onResizeEvent(): void { 150 | const event: NgGridItemEvent = this.getEventOutput(); 151 | this.onResize.emit(event); 152 | this.onResizeAny.emit(event); 153 | this.onChange.emit(event); 154 | this.onChangeAny.emit(event); 155 | } 156 | public onResizeStopEvent(): void { 157 | const event: NgGridItemEvent = this.getEventOutput(); 158 | this.onResizeStop.emit(event); 159 | this.onResizeAny.emit(event); 160 | this.onChangeStop.emit(event); 161 | this.onChangeAny.emit(event); 162 | 163 | this.onConfigChangeEvent(); 164 | } 165 | public onDragStartEvent(): void { 166 | const event: NgGridItemEvent = this.getEventOutput(); 167 | this.onDragStart.emit(event); 168 | this.onDragAny.emit(event); 169 | this.onChangeStart.emit(event); 170 | this.onChangeAny.emit(event); 171 | } 172 | public onDragEvent(): void { 173 | const event: NgGridItemEvent = this.getEventOutput(); 174 | this.onDrag.emit(event); 175 | this.onDragAny.emit(event); 176 | this.onChange.emit(event); 177 | this.onChangeAny.emit(event); 178 | } 179 | public onDragStopEvent(): void { 180 | const event: NgGridItemEvent = this.getEventOutput(); 181 | this.onDragStop.emit(event); 182 | this.onDragAny.emit(event); 183 | this.onChangeStop.emit(event); 184 | this.onChangeAny.emit(event); 185 | 186 | this.onConfigChangeEvent(); 187 | } 188 | public onCascadeEvent(): void { 189 | this.onConfigChangeEvent(); 190 | } 191 | 192 | public ngOnInit(): void { 193 | this._renderer.addClass(this._ngEl.nativeElement, 'grid-item'); 194 | if (this._ngGrid.autoStyle) this._renderer.setStyle(this._ngEl.nativeElement, 'position', 'absolute'); 195 | this._recalculateDimensions(); 196 | this._recalculatePosition(); 197 | 198 | // Force a config update in case there is no config assigned 199 | this.config = this._userConfig; 200 | } 201 | 202 | // Public methods 203 | public canDrag(e: any): boolean { 204 | if (!this.isDraggable) return false; 205 | 206 | if (this._dragHandle) { 207 | return this.findHandle(this._dragHandle, e.target); 208 | } 209 | 210 | return true; 211 | } 212 | 213 | public findHandle(handleSelector: string, startElement: HTMLElement): boolean { 214 | try { 215 | let targetElem: any = startElement; 216 | 217 | while (targetElem && targetElem != this._ngEl.nativeElement) { 218 | if (this.elementMatches(targetElem, handleSelector)) return true; 219 | 220 | targetElem = targetElem.parentElement; 221 | } 222 | } catch (err) {} 223 | 224 | return false; 225 | } 226 | 227 | public canResize(e: any): string { 228 | if (!this.isResizable) return null; 229 | 230 | if (this._resizeHandle) { 231 | if (typeof this._resizeHandle === 'string') { 232 | return this.findHandle(this._resizeHandle, e.target) ? 'bottomright' : null; 233 | } 234 | 235 | if (typeof this._resizeHandle !== 'object') return null; 236 | 237 | const resizeDirections = [ 'bottomright', 'bottomleft', 'topright', 'topleft', 'right', 'left', 'bottom', 'top' ]; 238 | for (let direction of resizeDirections) { 239 | if (direction in this._resizeHandle) { 240 | if (this.findHandle(this._resizeHandle[direction], e.target)) { 241 | return direction; 242 | } 243 | } 244 | } 245 | 246 | return null; 247 | } 248 | 249 | if (this._borderSize <= 0) return null; 250 | 251 | const mousePos: NgGridRawPosition = this._getMousePosition(e); 252 | 253 | for (let direction of this._resizeDirections) { 254 | if (this.canResizeInDirection(direction, mousePos)) { 255 | return direction; 256 | } 257 | } 258 | 259 | return null; 260 | } 261 | 262 | public onMouseMove(e: any): void { 263 | if (this._ngGrid.autoStyle) { 264 | if (this._ngGrid.resizeEnable) { 265 | const resizeDirection = this.canResize(e); 266 | 267 | let cursor: string = 'default'; 268 | switch (resizeDirection) { 269 | case 'bottomright': 270 | case 'topleft': 271 | cursor = 'nwse-resize'; 272 | break; 273 | case 'topright': 274 | case 'bottomleft': 275 | cursor = 'nesw-resize'; 276 | break; 277 | case 'top': 278 | case 'bottom': 279 | cursor = 'ns-resize'; 280 | break; 281 | case 'left': 282 | case 'right': 283 | cursor = 'ew-resize'; 284 | break; 285 | default: 286 | if (this._ngGrid.dragEnable && this.canDrag(e)) { 287 | cursor = 'move'; 288 | } 289 | break; 290 | } 291 | 292 | this._renderer.setStyle(this._ngEl.nativeElement, 'cursor', cursor); 293 | } else if (this._ngGrid.dragEnable && this.canDrag(e)) { 294 | this._renderer.setStyle(this._ngEl.nativeElement, 'cursor', 'move'); 295 | } else { 296 | this._renderer.setStyle(this._ngEl.nativeElement, 'cursor', 'default'); 297 | } 298 | } 299 | } 300 | 301 | public ngOnDestroy(): void { 302 | if (this._added) this._ngGrid.removeItem(this); 303 | } 304 | 305 | // Getters 306 | public getElement(): ElementRef { 307 | return this._ngEl; 308 | } 309 | 310 | public getDragHandle(): string { 311 | return this._dragHandle; 312 | } 313 | 314 | public getResizeHandle(): ResizeHandle { 315 | return this._resizeHandle; 316 | } 317 | 318 | public getDimensions(): NgGridItemDimensions { 319 | return { 'width': this._elemWidth, 'height': this._elemHeight }; 320 | } 321 | 322 | public getSize(): NgGridItemSize { 323 | return this._size; 324 | } 325 | 326 | public getPosition(): NgGridRawPosition { 327 | return { 'left': this._elemLeft, 'top': this._elemTop }; 328 | } 329 | 330 | public getGridPosition(): NgGridItemPosition { 331 | return this._currentPosition; 332 | } 333 | 334 | // Setters 335 | public setConfig(config: NgGridItemConfig): void { 336 | this._config = config; 337 | 338 | this._payload = config.payload; 339 | this._currentPosition.col = config.col ? config.col : NgGridItem.CONST_DEFAULT_CONFIG.col; 340 | this._currentPosition.row = config.row ? config.row : NgGridItem.CONST_DEFAULT_CONFIG.row; 341 | this._size.x = config.sizex ? config.sizex : NgGridItem.CONST_DEFAULT_CONFIG.sizex; 342 | this._size.y = config.sizey ? config.sizey : NgGridItem.CONST_DEFAULT_CONFIG.sizey; 343 | this._dragHandle = config.dragHandle; 344 | this._resizeHandle = config.resizeHandle; 345 | this._borderSize = config.borderSize; 346 | this.isDraggable = config.draggable ? true : false; 347 | this.isResizable = config.resizable ? true : false; 348 | this.isFixed = config.fixed ? true : false; 349 | this._resizeDirections = config.resizeDirections || this._ngGrid.resizeDirections; 350 | 351 | this._maxCols = !isNaN(config.maxCols) && isFinite(config.maxCols) ? config.maxCols : 0; 352 | this._minCols = !isNaN(config.minCols) && isFinite(config.minCols) ? config.minCols : 0; 353 | this._maxRows = !isNaN(config.maxRows) && isFinite(config.maxRows) ? config.maxRows : 0; 354 | this._minRows = !isNaN(config.minRows) && isFinite(config.minRows) ? config.minRows : 0; 355 | 356 | this.minWidth = !isNaN(config.minWidth) && isFinite(config.minWidth) ? config.minWidth : 0; 357 | this.minHeight = !isNaN(config.minHeight) && isFinite(config.minHeight) ? config.minHeight : 0; 358 | 359 | if (this._minCols > 0 && this._maxCols > 0 && this._minCols > this._maxCols) this._minCols = 0; 360 | if (this._minRows > 0 && this._maxRows > 0 && this._minRows > this._maxRows) this._minRows = 0; 361 | 362 | if (this._added) { 363 | this._ngGrid.updateItem(this); 364 | } 365 | 366 | this._size = this.fixResize(this._size); 367 | 368 | this._recalculatePosition(); 369 | this._recalculateDimensions(); 370 | } 371 | 372 | public ngDoCheck(): boolean { 373 | if (this._differ != null) { 374 | const changes: any = this._differ.diff(this._userConfig); 375 | 376 | if (changes != null) { 377 | return this._applyChanges(changes); 378 | } 379 | } 380 | 381 | return false; 382 | } 383 | 384 | public setSize(newSize: NgGridItemSize, update: boolean = true): void { 385 | newSize = this.fixResize(newSize); 386 | this._size = newSize; 387 | if (update) this._recalculateDimensions(); 388 | 389 | this.onItemChange.emit(this.getEventOutput()); 390 | } 391 | 392 | public setGridPosition(gridPosition: NgGridItemPosition, update: boolean = true): void { 393 | this._currentPosition = gridPosition; 394 | if (update) this._recalculatePosition(); 395 | 396 | this.onItemChange.emit(this.getEventOutput()); 397 | } 398 | 399 | public getEventOutput(): NgGridItemEvent { 400 | return { 401 | uid: this.uid, 402 | payload: this._payload, 403 | col: this._currentPosition.col, 404 | row: this._currentPosition.row, 405 | sizex: this._size.x, 406 | sizey: this._size.y, 407 | width: this._elemWidth, 408 | height: this._elemHeight, 409 | left: this._elemLeft, 410 | top: this._elemTop 411 | }; 412 | } 413 | 414 | public setPosition(x: number, y: number): void { 415 | switch (this._cascadeMode) { 416 | case 'up': 417 | case 'left': 418 | default: 419 | this._renderer.setStyle(this._ngEl.nativeElement, 'left', x + 'px'); 420 | this._renderer.setStyle(this._ngEl.nativeElement, 'top', y + 'px'); 421 | break; 422 | case 'right': 423 | this._renderer.setStyle(this._ngEl.nativeElement, 'right', x + 'px'); 424 | this._renderer.setStyle(this._ngEl.nativeElement, 'top', y + 'px'); 425 | break; 426 | case 'down': 427 | this._renderer.setStyle(this._ngEl.nativeElement, 'left', x + 'px'); 428 | this._renderer.setStyle(this._ngEl.nativeElement, 'bottom', y + 'px'); 429 | break; 430 | } 431 | 432 | this._elemLeft = x; 433 | this._elemTop = y; 434 | } 435 | 436 | public setCascadeMode(cascade: string): void { 437 | this._cascadeMode = cascade; 438 | switch (cascade) { 439 | case 'up': 440 | case 'left': 441 | default: 442 | this._renderer.setStyle(this._ngEl.nativeElement, 'left', this._elemLeft + 'px'); 443 | this._renderer.setStyle(this._ngEl.nativeElement, 'top', this._elemTop + 'px'); 444 | this._renderer.setStyle(this._ngEl.nativeElement, 'right', null); 445 | this._renderer.setStyle(this._ngEl.nativeElement, 'bottom', null); 446 | break; 447 | case 'right': 448 | this._renderer.setStyle(this._ngEl.nativeElement, 'right', this._elemLeft + 'px'); 449 | this._renderer.setStyle(this._ngEl.nativeElement, 'top', this._elemTop + 'px'); 450 | this._renderer.setStyle(this._ngEl.nativeElement, 'left', null); 451 | this._renderer.setStyle(this._ngEl.nativeElement, 'bottom', null); 452 | break; 453 | case 'down': 454 | this._renderer.setStyle(this._ngEl.nativeElement, 'left', this._elemLeft + 'px'); 455 | this._renderer.setStyle(this._ngEl.nativeElement, 'bottom', this._elemTop + 'px'); 456 | this._renderer.setStyle(this._ngEl.nativeElement, 'right', null); 457 | this._renderer.setStyle(this._ngEl.nativeElement, 'top', null); 458 | break; 459 | } 460 | } 461 | 462 | public setDimensions(w: number, h: number): void { 463 | if (w < this.minWidth) w = this.minWidth; 464 | if (h < this.minHeight) h = this.minHeight; 465 | 466 | this._renderer.setStyle(this._ngEl.nativeElement, 'width', w + 'px'); 467 | this._renderer.setStyle(this._ngEl.nativeElement, 'height', h + 'px'); 468 | 469 | this._elemWidth = w; 470 | this._elemHeight = h; 471 | } 472 | 473 | public startMoving(): void { 474 | this._renderer.addClass(this._ngEl.nativeElement, 'moving'); 475 | const style: any = window.getComputedStyle(this._ngEl.nativeElement); 476 | if (this._ngGrid.autoStyle) this._renderer.setStyle(this._ngEl.nativeElement, 'z-index', (parseInt(style.getPropertyValue('z-index')) + 1).toString()); 477 | } 478 | 479 | public stopMoving(): void { 480 | this._renderer.removeClass(this._ngEl.nativeElement, 'moving'); 481 | const style: any = window.getComputedStyle(this._ngEl.nativeElement); 482 | if (this._ngGrid.autoStyle) this._renderer.setStyle(this._ngEl.nativeElement, 'z-index', (parseInt(style.getPropertyValue('z-index')) - 1).toString()); 483 | } 484 | 485 | public recalculateSelf(): void { 486 | this._recalculatePosition(); 487 | this._recalculateDimensions(); 488 | } 489 | 490 | public fixResize(newSize: NgGridItemSize): NgGridItemSize { 491 | if (this._maxCols > 0 && newSize.x > this._maxCols) newSize.x = this._maxCols; 492 | if (this._maxRows > 0 && newSize.y > this._maxRows) newSize.y = this._maxRows; 493 | 494 | if (this._minCols > 0 && newSize.x < this._minCols) newSize.x = this._minCols; 495 | if (this._minRows > 0 && newSize.y < this._minRows) newSize.y = this._minRows; 496 | 497 | const itemWidth = (newSize.x * this._ngGrid.colWidth) + ((this._ngGrid.marginLeft + this._ngGrid.marginRight) * (newSize.x - 1)); 498 | if (itemWidth < this.minWidth) newSize.x = Math.ceil((this.minWidth + this._ngGrid.marginRight + this._ngGrid.marginLeft) / (this._ngGrid.colWidth + this._ngGrid.marginRight + this._ngGrid.marginLeft)); 499 | 500 | const itemHeight = (newSize.y * this._ngGrid.rowHeight) + ((this._ngGrid.marginTop + this._ngGrid.marginBottom) * (newSize.y - 1)); 501 | if (itemHeight < this.minHeight) newSize.y = Math.ceil((this.minHeight + this._ngGrid.marginBottom + this._ngGrid.marginTop) / (this._ngGrid.rowHeight + this._ngGrid.marginBottom + this._ngGrid.marginTop)); 502 | 503 | return newSize; 504 | } 505 | 506 | // Private methods 507 | private elementMatches(element: any, selector: string): boolean { 508 | if (!element) return false; 509 | if (element.matches) return element.matches(selector); 510 | if (element.oMatchesSelector) return element.oMatchesSelector(selector); 511 | if (element.msMatchesSelector) return element.msMatchesSelector(selector); 512 | if (element.mozMatchesSelector) return element.mozMatchesSelector(selector); 513 | if (element.webkitMatchesSelector) return element.webkitMatchesSelector(selector); 514 | 515 | if (!element.document || !element.ownerDocument) return false; 516 | 517 | const matches: any = (element.document || element.ownerDocument).querySelectorAll(selector); 518 | let i: number = matches.length; 519 | while (--i >= 0 && matches.item(i) !== element) { } 520 | return i > -1; 521 | } 522 | 523 | private _recalculatePosition(): void { 524 | const x: number = (this._ngGrid.colWidth + this._ngGrid.marginLeft + this._ngGrid.marginRight) * (this._currentPosition.col - 1) + this._ngGrid.marginLeft + this._ngGrid.screenMargin; 525 | const y: number = (this._ngGrid.rowHeight + this._ngGrid.marginTop + this._ngGrid.marginBottom) * (this._currentPosition.row - 1) + this._ngGrid.marginTop; 526 | 527 | this.setPosition(x, y); 528 | } 529 | 530 | private _recalculateDimensions(): void { 531 | if (this._size.x < this._ngGrid.minCols) this._size.x = this._ngGrid.minCols; 532 | if (this._size.y < this._ngGrid.minRows) this._size.y = this._ngGrid.minRows; 533 | 534 | const newWidth: number = (this._ngGrid.colWidth * this._size.x) + ((this._ngGrid.marginLeft + this._ngGrid.marginRight) * (this._size.x - 1)); 535 | const newHeight: number = (this._ngGrid.rowHeight * this._size.y) + ((this._ngGrid.marginTop + this._ngGrid.marginBottom) * (this._size.y - 1)); 536 | 537 | const w: number = Math.max(this.minWidth, this._ngGrid.minWidth, newWidth); 538 | const h: number = Math.max(this.minHeight, this._ngGrid.minHeight, newHeight); 539 | 540 | this.setDimensions(w, h); 541 | } 542 | 543 | private _getMousePosition(e: any): NgGridRawPosition { 544 | if (e.originalEvent && e.originalEvent.touches) { 545 | const oe: any = e.originalEvent; 546 | e = oe.touches.length ? oe.touches[0] : (oe.changedTouches.length ? oe.changedTouches[0] : e); 547 | } else if (e.touches) { 548 | e = e.touches.length ? e.touches[0] : (e.changedTouches.length ? e.changedTouches[0] : e); 549 | } 550 | 551 | 552 | const refPos: NgGridRawPosition = this._ngEl.nativeElement.getBoundingClientRect(); 553 | 554 | return { 555 | left: e.clientX - refPos.left, 556 | top: e.clientY - refPos.top 557 | }; 558 | } 559 | 560 | private _applyChanges(changes: any): boolean { 561 | let changed: boolean = false; 562 | const changeCheck = (record: any) => { 563 | if (this._config[record.key] !== record.currentValue) { 564 | this._config[record.key] = record.currentValue; 565 | changed = true; 566 | } 567 | }; 568 | changes.forEachAddedItem(changeCheck); 569 | changes.forEachChangedItem(changeCheck); 570 | changes.forEachRemovedItem((record: any) => { 571 | changed = true; 572 | delete this._config[record.key]; 573 | }); 574 | 575 | if (changed) { 576 | this.setConfig(this._config); 577 | } 578 | 579 | return changed; 580 | } 581 | 582 | private onConfigChangeEvent() { 583 | if (this._userConfig === null) return; 584 | 585 | this._config.sizex = this._userConfig.sizex = this._size.x; 586 | this._config.sizey = this._userConfig.sizey = this._size.y; 587 | this._config.col = this._userConfig.col = this._currentPosition.col; 588 | this._config.row = this._userConfig.row = this._currentPosition.row; 589 | this.ngGridItemChange.emit(this._userConfig); 590 | } 591 | 592 | private canResizeInDirection(direction: string, mousePos: NgGridRawPosition): boolean { 593 | switch (direction) { 594 | case 'bottomright': 595 | return mousePos.left < this._elemWidth && mousePos.left > this._elemWidth - this._borderSize 596 | && mousePos.top < this._elemHeight && mousePos.top > this._elemHeight - this._borderSize; // tslint:disable-line:indent 597 | case 'bottomleft': 598 | return mousePos.left < this._borderSize && mousePos.top < this._elemHeight 599 | && mousePos.top > this._elemHeight - this._borderSize; // tslint:disable-line:indent 600 | case 'topright': 601 | return mousePos.left < this._elemWidth && mousePos.left > this._elemWidth - this._borderSize 602 | && mousePos.top < this._borderSize; // tslint:disable-line:indent 603 | case 'topleft': 604 | return mousePos.left < this._borderSize && mousePos.top < this._borderSize; 605 | case 'right': 606 | return mousePos.left < this._elemWidth && mousePos.left > this._elemWidth - this._borderSize; 607 | case 'left': 608 | return mousePos.left < this._borderSize; 609 | case 'bottom': 610 | return mousePos.top < this._elemHeight && mousePos.top > this._elemHeight - this._borderSize; 611 | case 'top': 612 | return mousePos.top < this._borderSize; 613 | default: 614 | return false; 615 | } 616 | } 617 | } 618 | -------------------------------------------------------------------------------- /projects/angular2-grid/src/helpers/NgGridHelpers.ts: -------------------------------------------------------------------------------- 1 | import { NgGridItem } from "../directives/NgGridItem"; 2 | 3 | export function generateUuid(): string { 4 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { 5 | let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); 6 | return v.toString(16); 7 | }); 8 | } 9 | 10 | export function sortItemsByPositionHorizontal(a: NgGridItem, b: NgGridItem): number { 11 | if (a.col === b.col) { return a.row - b.row; } 12 | return a.col - b.col; 13 | } 14 | 15 | export function sortItemsByPositionVertical(a: NgGridItem, b: NgGridItem): number { 16 | if (a.row === b.row) { return a.col - b.col; } 17 | return a.row - b.row; 18 | } 19 | -------------------------------------------------------------------------------- /projects/angular2-grid/src/interfaces/INgGrid.ts: -------------------------------------------------------------------------------- 1 | export type NgConfigFixDirection = 'vertical' | 'horizontal' | 'cascade'; 2 | 3 | export interface NgGridConfig { 4 | margins?: number[]; 5 | draggable?: boolean; 6 | resizable?: boolean; 7 | max_cols?: number; 8 | max_rows?: number; 9 | visible_cols?: number; 10 | visible_rows?: number; 11 | min_cols?: number; 12 | min_rows?: number; 13 | col_width?: number; 14 | row_height?: number; 15 | cascade?: string; 16 | min_width?: number; 17 | min_height?: number; 18 | fix_to_grid?: boolean; 19 | auto_style?: boolean; 20 | auto_resize?: boolean; 21 | maintain_ratio?: boolean; 22 | prefer_new?: boolean; 23 | zoom_on_drag?: boolean; 24 | limit_to_screen?: boolean; 25 | center_to_screen?: boolean; 26 | resize_directions?: string[]; 27 | element_based_row_height?: boolean; 28 | fix_item_position_direction?: NgConfigFixDirection; 29 | fix_collision_position_direction?: NgConfigFixDirection; 30 | allow_overlap?: boolean; 31 | } 32 | 33 | export interface NgGridItemConfig { 34 | uid?: string; 35 | payload?: any; 36 | col?: number; 37 | row?: number; 38 | sizex?: number; 39 | sizey?: number; 40 | dragHandle?: string; 41 | resizeHandle?: ResizeHandle; 42 | fixed?: boolean; 43 | draggable?: boolean; 44 | resizable?: boolean; 45 | borderSize?: number; 46 | maxCols?: number; 47 | minCols?: number; 48 | maxRows?: number; 49 | minRows?: number; 50 | minWidth?: number; 51 | minHeight?: number; 52 | resizeDirections?: string[]; 53 | } 54 | 55 | export interface NgGridItemEvent { 56 | uid: string; 57 | payload: any; 58 | col: number; 59 | row: number; 60 | sizex: number; 61 | sizey: number; 62 | width: number; 63 | height: number; 64 | left: number; 65 | top: number; 66 | } 67 | 68 | export interface NgGridItemSize { 69 | x: number; 70 | y: number; 71 | } 72 | 73 | export interface NgGridItemPosition { 74 | col: number; 75 | row: number; 76 | } 77 | 78 | export interface NgGridRawPosition { 79 | left: number; 80 | top: number; 81 | } 82 | 83 | export interface NgGridItemDimensions { 84 | width: number; 85 | height: number; 86 | } 87 | 88 | export type ResizeHandle = string | { 89 | bottomright?: string; 90 | bottomleft?: string; 91 | topright?: string; 92 | topleft?: string; 93 | right?: string; 94 | left?: string; 95 | bottom?: string; 96 | top?: string; 97 | }; 98 | -------------------------------------------------------------------------------- /projects/angular2-grid/src/main.ts: -------------------------------------------------------------------------------- 1 | export { NgGrid } from './directives/NgGrid'; 2 | export { NgGridItem } from './directives/NgGridItem'; 3 | export { NgGridPlaceholder } from './components/NgGridPlaceholder'; 4 | export { NgGridConfig, NgGridItemConfig, NgGridItemEvent, NgGridItemSize, NgGridItemPosition, NgGridRawPosition, NgGridItemDimensions } from './interfaces/INgGrid'; 5 | export { NgGridModule } from './modules/NgGrid.module'; 6 | -------------------------------------------------------------------------------- /projects/angular2-grid/src/modules/NgGrid.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { NgGrid } from '../directives/NgGrid'; 3 | import { NgGridItem } from '../directives/NgGridItem'; 4 | import { NgGridPlaceholder } from '../components/NgGridPlaceholder'; 5 | 6 | @NgModule({ 7 | declarations: [ NgGrid, NgGridItem, NgGridPlaceholder ], 8 | entryComponents: [ NgGridPlaceholder ], 9 | exports: [ NgGrid, NgGridItem ] 10 | }) 11 | export class NgGridModule {} 12 | -------------------------------------------------------------------------------- /projects/angular2-grid/src/public_api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of angular2-grid 3 | */ 4 | 5 | export * from './lib/angular2-grid.service'; 6 | export * from './lib/angular2-grid.component'; 7 | export * from './lib/angular2-grid.module'; 8 | -------------------------------------------------------------------------------- /projects/angular2-grid/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'core-js/es7/reflect'; 4 | import 'zone.js/dist/zone'; 5 | import 'zone.js/dist/zone-testing'; 6 | import { getTestBed } from '@angular/core/testing'; 7 | import { 8 | BrowserDynamicTestingModule, 9 | platformBrowserDynamicTesting 10 | } from '@angular/platform-browser-dynamic/testing'; 11 | 12 | declare const require: any; 13 | 14 | // First, initialize the Angular testing environment. 15 | getTestBed().initTestEnvironment( 16 | BrowserDynamicTestingModule, 17 | platformBrowserDynamicTesting() 18 | ); 19 | // Then we find all the tests. 20 | const context = require.context('./', true, /\.spec\.ts$/); 21 | // And load the modules. 22 | context.keys().map(context); 23 | -------------------------------------------------------------------------------- /projects/angular2-grid/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/lib", 5 | "target": "es2015", 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "declaration": true, 9 | "sourceMap": true, 10 | "inlineSources": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "importHelpers": true, 14 | "types": [], 15 | "lib": [ 16 | "dom", 17 | "es2015" 18 | ] 19 | }, 20 | "angularCompilerOptions": { 21 | "annotateForClosureCompiler": true, 22 | "skipTemplateCodegen": true, 23 | "strictMetadataEmit": true, 24 | "fullTemplateTypeCheck": true, 25 | "strictInjectionParameters": true, 26 | "flatModuleId": "AUTOGENERATED", 27 | "flatModuleOutFile": "AUTOGENERATED", 28 | "enableIvy": false 29 | }, 30 | "exclude": [ 31 | "src/test.ts", 32 | "**/*.spec.ts" 33 | ] 34 | } -------------------------------------------------------------------------------- /projects/angular2-grid/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/angular2-grid/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "lib", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "lib", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | /*global jasmine */ 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | exports.config = { 8 | allScriptsTimeout: 11000, 9 | specs: [ 10 | './e2e/**/*.e2e-spec.ts' 11 | ], 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | directConnect: true, 16 | baseUrl: 'http://localhost:4200/', 17 | framework: 'jasmine', 18 | jasmineNodeOpts: { 19 | showColors: true, 20 | defaultTimeoutInterval: 30000, 21 | print: function() {} 22 | }, 23 | beforeLaunch: function() { 24 | require('ts-node').register({ 25 | project: 'e2e/tsconfig.e2e.json' 26 | }); 27 | }, 28 | onPrepare: function() { 29 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .grid-item { 2 | border: solid 1px; 3 | } 4 | 5 | .handle { 6 | width: 100%; 7 | height: 50px; 8 | background: blue; 9 | color: white; 10 | padding: 15px; 11 | box-sizing: border-box; 12 | } 13 | 14 | .btn { 15 | margin-left: 10px; 16 | } -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Angular 2 Grid Demo

4 |

This is a simple demo used for creation/testing of angular2-grid, a grid-based drag/drop/resize plugin directive for Angular 2. It makes use of angular directives so that in order to include it, all you need to do is add an "[ngGrid]" attribute to your container and an "[ngGridItem]" to each of your grid items. It even works with the ngFor directive to add boxes on the fly!

5 |
6 |
7 |
8 |
9 | 12 | 13 |
14 |
15 | 18 | 19 |
20 |
21 | 24 | 25 |
26 |
27 | 30 | 31 |
32 |
33 | 36 | 37 |
38 |
39 | 42 | 49 |
50 |
51 |
52 |
53 | 57 |
58 |
59 | 63 |
64 |
65 | 69 |
70 |
71 |
72 |
73 | 77 |
78 |
79 | 83 |
84 |
85 |
86 |
87 | 91 |
92 |
93 | 97 |
98 |
99 |
100 |
101 | 105 |
106 |
107 | 111 |
112 |
113 |
114 |
115 | 118 | 119 |
120 |
121 | 124 | 125 |
126 |
127 | 130 | 131 |
132 |
133 | 136 | 137 |
138 |
139 | 142 | 143 |
144 |
145 |
146 |
147 | 151 |
152 |
153 | 157 |
158 |
159 |
160 |
161 | 169 |
170 |
171 | 179 |
180 |
181 |
182 | 188 | 189 | Col: {{ curItem.col }}, Row: {{ curItem.row }}, Size X: {{ curItem.sizex }}, Size Y: {{ curItem.sizey }} 190 | Remove Box 191 |
192 |
193 | 194 | Randomise Boxes 195 | Add Box 196 |
197 |
198 |
199 | 226 |
227 |
228 | {{box.id}} 229 |
230 | 246 |
247 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ViewChild, AfterViewInit } from '@angular/core'; 2 | import { NgGridConfig, NgGridItemConfig, NgGridItemEvent, NgGrid } from 'angular2-grid'; 3 | 4 | interface Box { 5 | id: number; 6 | config: NgGridItemConfig; 7 | } 8 | 9 | @Component({ 10 | selector: 'app-root', 11 | templateUrl: './app.component.html', 12 | styleUrls: ['./app.component.css'] 13 | }) 14 | export class AppComponent implements AfterViewInit { 15 | @ViewChild(NgGrid) 16 | private grid: NgGrid; 17 | public curNum = 10; 18 | public boxes: Array = []; 19 | public gridConfig: NgGridConfig = { 20 | 'margins': [5], 21 | 'draggable': true, 22 | 'resizable': true, 23 | 'max_cols': 0, 24 | 'max_rows': 0, 25 | 'visible_cols': 0, 26 | 'visible_rows': 0, 27 | 'min_cols': 1, 28 | 'min_rows': 1, 29 | 'col_width': 2, 30 | 'row_height': 2, 31 | 'cascade': 'up', 32 | 'min_width': 50, 33 | 'min_height': 50, 34 | 'fix_to_grid': false, 35 | 'auto_style': true, 36 | 'auto_resize': false, 37 | 'maintain_ratio': false, 38 | 'prefer_new': false, 39 | 'zoom_on_drag': false, 40 | 'limit_to_screen': true, 41 | 'element_based_row_height': false, 42 | 'center_to_screen': false, 43 | 'fix_item_position_direction': 'horizontal', 44 | 'fix_collision_position_direction': 'horizontal', 45 | }; 46 | private rgb = '#efefef'; 47 | private curItemCheck = 0; 48 | private itemPositions: Array = []; 49 | 50 | constructor() { 51 | for (let i = 1; i < this.curNum; i++) { 52 | const conf = this._generateDefaultItemConfig(); 53 | conf.payload = i; 54 | this.boxes[i - 1] = { id: i, config: conf }; 55 | } 56 | } 57 | 58 | get ratioDisabled(): boolean { 59 | return (this.gridConfig.max_rows > 0 && this.gridConfig.visible_cols > 0) || 60 | (this.gridConfig.max_cols > 0 && this.gridConfig.visible_rows > 0) || 61 | (this.gridConfig.visible_cols > 0 && this.gridConfig.visible_rows > 0); 62 | } 63 | 64 | get itemCheck(): number { 65 | return this.curItemCheck; 66 | } 67 | 68 | set itemCheck(v: number) { 69 | this.curItemCheck = v; 70 | } 71 | 72 | get curItem(): NgGridItemConfig { 73 | return this.boxes[this.curItemCheck] ? this.boxes[this.curItemCheck].config : {}; 74 | } 75 | 76 | ngAfterViewInit(): void { 77 | // Do something with NgGrid instance here 78 | } 79 | 80 | setMargin(marginSize: string): void { 81 | this.gridConfig.margins = [ parseInt(marginSize, 10) ]; 82 | } 83 | 84 | addBox(): void { 85 | const conf: NgGridItemConfig = this._generateDefaultItemConfig(); 86 | conf.payload = this.curNum++; 87 | this.boxes.push({ id: conf.payload, config: conf }); 88 | } 89 | 90 | removeBox(): void { 91 | if (this.boxes[this.curItemCheck]) { 92 | this.boxes.splice(this.curItemCheck, 1); 93 | } 94 | } 95 | 96 | updateItem(index: number, event: NgGridItemEvent): void { 97 | // Do something here 98 | } 99 | 100 | onDrag(index: number, event: NgGridItemEvent): void { 101 | // Do something here 102 | } 103 | 104 | onResize(index: number, event: NgGridItemEvent): void { 105 | // Do something here 106 | } 107 | 108 | public randomise(): void { 109 | for (const box of this.boxes) { 110 | box.config.col = Math.floor(Math.random() * 6) + 1; 111 | box.config.row = 1; 112 | } 113 | } 114 | 115 | private _generateDefaultItemConfig(): NgGridItemConfig { 116 | return { 'dragHandle': '.handle', 'col': 1, 'row': 1, 'sizex': 1, 'sizey': 1 }; 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { HttpModule } from '@angular/http'; 5 | import { NgGridModule } from "angular2-grid"; 6 | 7 | import { AppComponent } from './app.component'; 8 | 9 | @NgModule({ 10 | declarations: [ 11 | AppComponent 12 | ], 13 | imports: [ 14 | BrowserModule, 15 | FormsModule, 16 | HttpModule, 17 | NgGridModule 18 | ], 19 | providers: [], 20 | bootstrap: [AppComponent] 21 | }) 22 | export class AppModule { } 23 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BTMorton/angular2-grid/3ba01d30ba1ffdc19798af498daff59339d3a251/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular 2 Grid Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | Loading... 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | import { enableProdMode } from '@angular/core'; 3 | import { environment } from './environments/environment'; 4 | import { AppModule } from './app/app.module'; 5 | 6 | if (environment.production) { 7 | enableProdMode(); 8 | } 9 | 10 | platformBrowserDynamic().bootstrapModule(AppModule); 11 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | // This file includes polyfills needed by Angular and is loaded before 2 | // the app. You can add your own extra polyfills to this file. 3 | import 'core-js/es6/symbol'; 4 | import 'core-js/es6/object'; 5 | import 'core-js/es6/function'; 6 | import 'core-js/es6/parse-int'; 7 | import 'core-js/es6/parse-float'; 8 | import 'core-js/es6/number'; 9 | import 'core-js/es6/math'; 10 | import 'core-js/es6/string'; 11 | import 'core-js/es6/date'; 12 | import 'core-js/es6/array'; 13 | import 'core-js/es6/regexp'; 14 | import 'core-js/es6/map'; 15 | import 'core-js/es6/set'; 16 | import 'core-js/es6/reflect'; 17 | 18 | import 'core-js/es7/reflect'; 19 | import 'zone.js/dist/zone'; 20 | -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/long-stack-trace-zone'; 4 | import 'zone.js/dist/proxy.js'; 5 | import 'zone.js/dist/sync-test'; 6 | import 'zone.js/dist/jasmine-patch'; 7 | import 'zone.js/dist/async-test'; 8 | import 'zone.js/dist/fake-async-test'; 9 | import { getTestBed } from '@angular/core/testing'; 10 | import { 11 | BrowserDynamicTestingModule, 12 | platformBrowserDynamicTesting 13 | } from '@angular/platform-browser-dynamic/testing'; 14 | 15 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. 16 | declare var __karma__: any; 17 | declare var require: any; 18 | 19 | // Prevent Karma from running prematurely. 20 | __karma__.loaded = function () {}; 21 | 22 | // First, initialize the Angular testing environment. 23 | getTestBed().initTestEnvironment( 24 | BrowserDynamicTestingModule, 25 | platformBrowserDynamicTesting() 26 | ); 27 | // Then we find all the tests. 28 | const context = require.context('./', true, /\.spec\.ts$/); 29 | // And load the modules. 30 | context.keys().map(context); 31 | // Finally, start Karma to run the tests. 32 | __karma__.start(); 33 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es5", 9 | "lib": [ 10 | "es2017", 11 | "dom" 12 | ], 13 | "outDir": "../out-tsc/app", 14 | "module": "es2015", 15 | "baseUrl": "", 16 | "types": [], 17 | "paths": { 18 | "angular2-grid": [ 19 | "../dist/angular2-grid" 20 | ] 21 | } 22 | }, 23 | "exclude": [ 24 | "test.ts", 25 | "**/*.spec.ts" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "lib": [ 9 | "es2017", 10 | "dom" 11 | ], 12 | "outDir": "../out-tsc/spec", 13 | "module": "commonjs", 14 | "target": "es5", 15 | "baseUrl": "", 16 | "types": [ 17 | "jasmine", 18 | "node" 19 | ], 20 | "paths": { 21 | "angular2-grid": [ 22 | "../dist/angular2-grid" 23 | ] 24 | } 25 | }, 26 | "files": [ 27 | "test.ts", 28 | "polyfills.ts" 29 | ], 30 | "include": [ 31 | "**/*.spec.ts", 32 | "**/*.d.ts" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "baseUrl": "src", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2017", 17 | "dom" 18 | ], 19 | "paths": { 20 | "angular2-grid": [ 21 | "dist/angular2-grid" 22 | ] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "callable-types": true, 7 | "class-name": true, 8 | "comment-format": [ 9 | true, 10 | "check-space" 11 | ], 12 | "curly": true, 13 | "eofline": true, 14 | "forin": true, 15 | "import-blacklist": [true], 16 | "import-spacing": true, 17 | "indent": [ 18 | true, 19 | "spaces" 20 | ], 21 | "interface-over-type-literal": true, 22 | "label-position": true, 23 | "max-line-length": [ 24 | true, 25 | 140 26 | ], 27 | "member-access": false, 28 | "member-ordering": [ 29 | true, 30 | "static-before-instance", 31 | "variables-before-functions" 32 | ], 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-console": [ 36 | true, 37 | "debug", 38 | "info", 39 | "time", 40 | "timeEnd", 41 | "trace" 42 | ], 43 | "no-construct": true, 44 | "no-debugger": true, 45 | "no-duplicate-variable": true, 46 | "no-empty": false, 47 | "no-empty-interface": true, 48 | "no-eval": true, 49 | "no-inferrable-types": [true, "ignore-params"], 50 | "no-shadowed-variable": true, 51 | "no-string-literal": false, 52 | "no-string-throw": true, 53 | "no-switch-case-fall-through": true, 54 | "no-trailing-whitespace": true, 55 | "no-unused-expression": true, 56 | "no-use-before-declare": true, 57 | "no-var-keyword": true, 58 | "object-literal-sort-keys": false, 59 | "one-line": [ 60 | true, 61 | "check-open-brace", 62 | "check-catch", 63 | "check-else", 64 | "check-whitespace" 65 | ], 66 | "prefer-const": true, 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "radix": true, 72 | "semicolon": [ 73 | "always" 74 | ], 75 | "triple-equals": [ 76 | true, 77 | "allow-null-check" 78 | ], 79 | "typedef-whitespace": [ 80 | true, 81 | { 82 | "call-signature": "nospace", 83 | "index-signature": "nospace", 84 | "parameter": "nospace", 85 | "property-declaration": "nospace", 86 | "variable-declaration": "nospace" 87 | } 88 | ], 89 | "typeof-compare": true, 90 | "unified-signatures": true, 91 | "variable-name": false, 92 | "whitespace": [ 93 | true, 94 | "check-branch", 95 | "check-decl", 96 | "check-operator", 97 | "check-separator", 98 | "check-type" 99 | ], 100 | 101 | "directive-selector": [true, "attribute", "app", "camelCase"], 102 | "component-selector": [true, "element", "app", "kebab-case"], 103 | "use-input-property-decorator": true, 104 | "use-output-property-decorator": true, 105 | "use-host-property-decorator": true, 106 | "no-input-rename": true, 107 | "no-output-rename": true, 108 | "use-life-cycle-interface": true, 109 | "use-pipe-transform-interface": true, 110 | "component-class-suffix": true, 111 | "directive-class-suffix": true, 112 | "no-access-missing-member": true, 113 | "templates-use-public": true, 114 | "invoke-injectable": true 115 | } 116 | } 117 | --------------------------------------------------------------------------------