diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000000..c2cdfb8ada --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 2 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.travis.yml b/.travis.yml index 04be86e827..97c86ac117 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ node_js: - '0.10' notifications: - irc: "chat.freenode.net#ui-grid" webhooks: urls: - https://webhooks.gitter.im/e/c9dc628573cc153706fa diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a162c3356..69202ff08c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,64 @@ + +### v3.0.0-rc.21 (2015-04-28) + + +#### Bug Fixes + +* **Expandable:** Run with lower priority than ngIf ([949013c3](http://github.com/angular-ui/ng-grid/commit/949013c332c5af1b3e37b1d3fa515dfd96c8acb2), closes [#2804](http://github.com/angular-ui/ng-grid/issues/2804)) +* **RTL:** + * Use Math.abs for normalizing negatives ([4acbdc1a](http://github.com/angular-ui/ng-grid/commit/4acbdc1a58d8043d60e3a62d1126b0f69bc6ee86)) + * Use feature detection to determine RTL ([fbb36319](http://github.com/angular-ui/ng-grid/commit/fbb363197ab3975411589dfa0904495f861795c0), closes [#1689](http://github.com/angular-ui/ng-grid/issues/1689)) +* **cellNav:** fix null ref issue in navigate event for oldRowColumn scrollTo should not setF ([02b05cae](http://github.com/angular-ui/ng-grid/commit/02b05cae6d5385e01d00f812662f16009130c647)) +* **pinning:** restore correct width state ([4ffaaf26](http://github.com/angular-ui/ng-grid/commit/4ffaaf26774bae7f52bf4956f45243f6c7dd53a3)) +* **scrolling:** Fix for #3260 atTop/Bottom/Left/Right needed tweaking ([89461bcb](http://github.com/angular-ui/ng-grid/commit/89461bcbcfdfc527655c398df19555738fa9bd63)) +* **selection:** + * allow rowSelection to be navigable if using cellNav; allow rowSelection via the ([95ce7b1b](http://github.com/angular-ui/ng-grid/commit/95ce7b1b694b23f1a7506cf4f6a32d0ae384697c)) + * allow rowSelection to be navigable if using cellNav; allow rowSelection via the ([3d5d6031](http://github.com/angular-ui/ng-grid/commit/3d5d603178f0fcb4cc2abab6ce637c1dd6face8d)) +* **uiGrid:** + * Use margins rather than floats for pinning ([1373b99e](http://github.com/angular-ui/ng-grid/commit/1373b99e1e1680184270d61bca88124efd7a4c14), closes [#2997](http://github.com/angular-ui/ng-grid/issues/2997), [#NaN](http://github.com/angular-ui/ng-grid/issues/NaN)) + * Wait for grid to get dimensions ([e7dfb8c2](http://github.com/angular-ui/ng-grid/commit/e7dfb8c2dfac69bb3a38f7253062367671fec56d)) +* **uiGridColumnMenu:** Position relatively ([9d918052](http://github.com/angular-ui/ng-grid/commit/9d9180520d8d6fd16b897ba4b9fbfc4bb4860ea9), closes [#2319](http://github.com/angular-ui/ng-grid/issues/2319)) +* **uiGridFooter:** Watch for col change ([1f9100de](http://github.com/angular-ui/ng-grid/commit/1f9100defb1489bed46515fb859aed9c9a090e73), closes [#2686](http://github.com/angular-ui/ng-grid/issues/2686)) +* **uiGridHeader:** + * Use parseInt on header heights ([98ed0104](http://github.com/angular-ui/ng-grid/commit/98ed01049015b22caddb651b1884f6e383fc58aa)) + * Allow header to shrink in size ([7c5cdca1](http://github.com/angular-ui/ng-grid/commit/7c5cdca1f471a0a3c1ef340fe65af268df68cae3), closes [#3138](http://github.com/angular-ui/ng-grid/issues/3138)) + + +#### Features + +* **saveState:** add pinning to save state ([b0d943a8](http://github.com/angular-ui/ng-grid/commit/b0d943a82a1d5c64808b759c8b96833e66380b02)) + + +#### Breaking Changes + +* gridUtil will no longer calculate dimensions of hidden +elements + ([e7dfb8c2](http://github.com/angular-ui/ng-grid/commit/e7dfb8c2dfac69bb3a38f7253062367671fec56d)) +* Two events are now emitted on scroll: + + grid.api.core.ScrollBegin + grid.api.core.ScrollEnd + + Before: + grid.api.core.ScrollEvent + After: +grid.api.core.ScrollBegin + +ScrollToIfNecessary and ScrollTo moved from cellNav to core and grid removed from arguments +Before: +grid.api.cellNav.ScrollToIfNecessary(grid, gridRow, gridCol) +grid.api.cellNav.ScrollTo(grid, rowEntity, colDef) + +After: +grid.api.core.ScrollToIfNecessary(gridRow, gridCol) +grid.api.core.ScrollTo(rowEntity, colDef) + +GridEdit/cellNav +When using cellNav, a cell no longer receives focus. Instead the viewport always receives focus. This eliminated many bugs associated with scrolling and focus. + +If you have a custom editor, you will no longer receive keyDown/Up events from the readonly cell. Use the cellNav api viewPortKeyDown to capture any needed keydown events. see GridEdit.js for an example + ([052c2321](http://github.com/angular-ui/ng-grid/commit/052c2321f97b37f860c769dcbd2e8d9094cf2bbf)) + ### v3.0.0-rc.20 (2015-02-24) diff --git a/bower.json b/bower.json index 185d4c0afb..8ee4fd2a0b 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "angular-ui-grid", - "version": "3.0.0-rc.20", + "version": "3.0.0-rc.21", "homepage": "http://ui-grid.info", "repository": { "type": "git", diff --git a/misc/api/design-rendering-cycle.ngdoc b/misc/api/design-rendering-cycle.ngdoc new file mode 100644 index 0000000000..a0a6feb4df --- /dev/null +++ b/misc/api/design-rendering-cycle.ngdoc @@ -0,0 +1,73 @@ +@ngdoc overview +@name Rendering Cycle +@module +@description + +The core grid rendering cycle is executed whenever the grid needs re-rendering. There are a number of core methods, +with some of those methods able to be called individually. + +### Current +The key method is `grid.refresh`. This method updates the rows and columns in the grid, then redraws and resizes the grid. + + - grid.refresh + - rowsProcessors + - setVisibleRows + - columnsProcessors + - setVisibleColumns + - redrawInPlace + - refreshCanvas + +By preference grid.refresh is called through a debounce function - grid.queueGridRefresh. If you use this method you are +telling the grid that you want a refresh, but you're allowing the grid to consolidate all refreshes from the current digest cycle +and process just once. + +A similar method, `grid.refreshRows` also exists, this is the same as grid.refresh except that it doesn't run `columnsProcessors` +or `setVisibleColumns`. + +The rows and columns processors are focused on ordering and determining the visibility of columns and rows. They include functions +such as sorting and filtering (impacting order and visibility of rows), grouping rows (which adds extra rows, and changes the ordering +and widths of columns), and pinning, which changes which render container particular columns are in. + +`redrawInPlace` determines the correct scroll percentage in the grid, and therefore which of the rows and columns should currently +be visible in the viewport. + +`refreshCanvas` is a complicated method that determines the sizing of each of the grid elements. In some cases it is currently iterative, +for example it determines header height by rendering each of the column headers, and determining the maximum rendered height. This largely +appears to be to accomodate filters. + + - refreshCanvas + - buildStyles + - $timeout - calcHeaders (this is inline - should it be a style computation? It isn't a promise, and doesn't wait on the buildStyles + promise, but it does run in a timeout. Conversely, it creates a promise that it resolves - but it doesn't wait for the header calc to + complete before resolving the promise) + - may call buildStyles again if it decides headerHeight has changed + +The style builders include: + +- `GridRenderContainer.updateColumnWidths`, which calculates column widths based on the defined settings, including resolving * and ** etc. No rendering + is involved - all based on the availableWidth. This may be the source of some of the iteration - because availableWidth must in some way be + based on columnWidth - the canvas doesn't really have an available width. I also have question on why we calculate widths on the grid + and not on the renderContainer, that may be another source of iteration. Having said that, things like % and * probably apply to the + full grid width, not to just a container, and we wouldn't expect a column to change width when it changed container (e.g. when we pinned it) +- `uiGridRenderContainer.update()`, which is called for each renderContainer. It determines the +width of each column in the render container, and the width of the overall render container. +- `Grid.prototype.getFooterStyles()`, sets the columnFooterHeight and the gridFooterHeight based on fixed values declared in the options +- when there are multiple renderContainers (e.g. a left container), the non-body render containers appear to execute first +- `ui-pinned-container.updateContainerDimensions()`: sets the width of a pinned container. How does this interact with render container width? + +### Vision +The vision is to make the style calculations more deterministic, and remove any iteration or other dependencies. A single pass through +refreshCanvas should return a correctly sized grid. + +To achieve this, we really need to: + +- calculate all sizing in code, without reference to the sizing of rendered grid elements. We already do most of this for rows + and columns, the main gap seems to be grid header +- we could reference rendered size to determine the grid's available size (if we want to), which could allow the grid to be more + responsive. Probably we already do this. +- calculate the column widths and element heights +- layout all the columns - what render container they're in etc, then size the render containers +- calculate the overall grid sizing based on all the elements (headerHeight, footerHeight, container widths etc) +- render + +Thinking only at this stage!! diff --git a/misc/demo/grid-save.html b/misc/demo/grid-save.html new file mode 100644 index 0000000000..8a19c65df6 --- /dev/null +++ b/misc/demo/grid-save.html @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + +

Grid

+
+ + + +
+
+ + + + + \ No newline at end of file diff --git a/misc/demo/modal.html b/misc/demo/modal.html new file mode 100644 index 0000000000..60ebcc34ad --- /dev/null +++ b/misc/demo/modal.html @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + diff --git a/misc/demo/pinning.html b/misc/demo/pinning.html new file mode 100644 index 0000000000..f1db42c7e9 --- /dev/null +++ b/misc/demo/pinning.html @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + +

Grid

+
+ + + +
+
+ + + + + diff --git a/misc/site/data/100_ASC.json b/misc/site/data/100_ASC.json index 2d61b62597..023e4eae99 100644 --- a/misc/site/data/100_ASC.json +++ b/misc/site/data/100_ASC.json @@ -1,348 +1,353 @@ [ { - "name": "Beryl Rice", + "name": "Alexander Foley", + "gender": "male", + "company": "Geekosis" + }, + { + "name": "Alisha Myers", "gender": "female", - "company": "Velity" + "company": "Intradisk" }, { - "name": "Bruce Strong", + "name": "Anthony Joyner", "gender": "male", - "company": "Xyqag" + "company": "Senmei" }, { - "name": "Carroll Buchanan", + "name": "Atkins Dunlap", "gender": "male", - "company": "Ecosys" + "company": "Comveyor" }, { - "name": "Claudine Neal", - "gender": "female", - "company": "Sealoud" + "name": "Ayers Hood", + "gender": "male", + "company": "Accuprint" }, { - "name": "Dawson Barber", + "name": "Baird Ryan", "gender": "male", - "company": "Dymi" + "company": "Aquasseur" }, { - "name": "Ethel Price", - "gender": "female", - "company": "Enersol" + "name": "Barnett Case", + "gender": "male", + "company": "Norali" }, { - "name": "Evans Hickman", + "name": "Barr Page", "gender": "male", - "company": "Parleynet" + "company": "Apex" }, { - "name": "Georgina Schultz", - "gender": "female", - "company": "Suretech" + "name": "Bean Donovan", + "gender": "male", + "company": "Mantro" }, { - "name": "Jackson Macias", - "gender": "male", - "company": "Aquamate" + "name": "Beryl Rice", + "gender": "female", + "company": "Velity" }, { - "name": "Lynda Mendoza", + "name": "Betsy Horton", "gender": "female", - "company": "Dogspa" + "company": "Zilla" }, { - "name": "Nellie Whitfield", + "name": "Billie Rowe", "gender": "female", - "company": "Exospace" + "company": "Cemention" }, { - "name": "Robles Boyle", + "name": "Blackburn Drake", "gender": "male", - "company": "Comtract" + "company": "Frenex" }, { - "name": "Sarah Massey", + "name": "Blanche Conley", "gender": "female", - "company": "Bisba" + "company": "Imkan" }, { - "name": "Schroeder Mathews", + "name": "Bruce Strong", "gender": "male", - "company": "Polarium" + "company": "Xyqag" }, { - "name": "Valarie Atkinson", - "gender": "female", - "company": "Hopeli" + "name": "Carroll Buchanan", + "gender": "male", + "company": "Ecosys" }, { - "name": "Wilder Gonzales", + "name": "Chaney Roach", "gender": "male", - "company": "Geekko" + "company": "Qualitern" }, { - "name": "Pena Pena", - "gender": "male", - "company": "Quarx" + "name": "Christine Compton", + "gender": "female", + "company": "Bleeko" }, { - "name": "Lelia Gates", + "name": "Claudine Neal", "gender": "female", - "company": "Proxsoft" + "company": "Sealoud" }, { - "name": "Letitia Vasquez", + "name": "Consuelo Dickson", "gender": "female", - "company": "Slumberia" + "company": "Poshome" }, { - "name": "Trevino Moreno", - "gender": "male", - "company": "Conjurica" + "name": "Cora Chase", + "gender": "female", + "company": "Isonus" }, { - "name": "Barr Page", - "gender": "male", - "company": "Apex" + "name": "Dale Byrd", + "gender": "female", + "company": "Kneedles" }, { - "name": "Kirkland Merrill", + "name": "Dawson Barber", "gender": "male", - "company": "Utara" + "company": "Dymi" }, { - "name": "Blanche Conley", + "name": "Deann Bridges", "gender": "female", - "company": "Imkan" + "company": "Equitox" }, { - "name": "Atkins Dunlap", - "gender": "male", - "company": "Comveyor" + "name": "Ericka Alvarado", + "gender": "female", + "company": "Lyrichord" }, { - "name": "Everett Foreman", + "name": "Ethel Price", + "gender": "female", + "company": "Enersol" + }, + { + "name": "Evans Hickman", "gender": "male", - "company": "Maineland" + "company": "Parleynet" }, { - "name": "Gould Randolph", + "name": "Everett Foreman", "gender": "male", - "company": "Intergeek" + "company": "Maineland" }, { - "name": "Kelli Leon", + "name": "Felecia Smith", "gender": "female", - "company": "Verbus" + "company": "Futurity" }, { - "name": "Freda Mason", + "name": "Francesca Elliott", "gender": "female", - "company": "Accidency" + "company": "Nspire" }, { - "name": "Tucker Maxwell", + "name": "Franco Hunter", "gender": "male", - "company": "Lumbrex" + "company": "Rockabye" }, { - "name": "Yvonne Parsons", + "name": "Freda Mason", "gender": "female", - "company": "Zolar" + "company": "Accidency" }, { - "name": "Woods Key", + "name": "Frye Sharpe", "gender": "male", - "company": "Bedder" + "company": "Eplode" }, { - "name": "Stephens Reilly", + "name": "Gaines Beck", "gender": "male", - "company": "Acusage" + "company": "Sequitur" }, { - "name": "Mcfarland Sparks", + "name": "Garrett Brennan", "gender": "male", - "company": "Comvey" + "company": "Bluegrain" }, { - "name": "Jocelyn Sawyer", + "name": "Georgia Mercer", "gender": "female", - "company": "Fortean" + "company": "Skyplex" }, { - "name": "Renee Barr", + "name": "Georgina Schultz", "gender": "female", - "company": "Kiggle" + "company": "Suretech" }, { - "name": "Gaines Beck", + "name": "Gould Randolph", "gender": "male", - "company": "Sequitur" + "company": "Intergeek" }, { - "name": "Luisa Farrell", - "gender": "female", - "company": "Cinesanct" + "name": "Graham Marsh", + "gender": "male", + "company": "Medifax" }, { - "name": "Robyn Strickland", - "gender": "female", - "company": "Obones" + "name": "Hale Boone", + "gender": "male", + "company": "Digial" }, { - "name": "Roseann Jarvis", + "name": "Hattie Mullen", "gender": "female", - "company": "Aquazure" + "company": "Zilencio" }, { - "name": "Johnston Park", + "name": "Herring Pierce", "gender": "male", - "company": "Netur" + "company": "Geeketron" }, { - "name": "Wong Craft", - "gender": "male", - "company": "Opticall" + "name": "Hilda Crane", + "gender": "female", + "company": "Jumpstack" }, { - "name": "Merritt Cole", + "name": "Humphrey Curtis", "gender": "male", - "company": "Techtrix" + "company": "Corepan" }, { - "name": "Dale Byrd", - "gender": "female", - "company": "Kneedles" + "name": "Jackson Macias", + "gender": "male", + "company": "Aquamate" }, { - "name": "Sara Delgado", + "name": "Jeanne Lindsay", "gender": "female", - "company": "Netagy" + "company": "Genesynk" }, { - "name": "Alisha Myers", + "name": "Jerri King", "gender": "female", - "company": "Intradisk" + "company": "Eventex" }, { - "name": "Felecia Smith", + "name": "Jocelyn Sawyer", "gender": "female", - "company": "Futurity" + "company": "Fortean" }, { - "name": "Neal Harvey", + "name": "Johnston Park", "gender": "male", - "company": "Pyramax" + "company": "Netur" }, { - "name": "Nola Miles", + "name": "Kelli Leon", "gender": "female", - "company": "Sonique" + "company": "Verbus" }, { - "name": "Herring Pierce", + "name": "Kirk Cross", "gender": "male", - "company": "Geeketron" + "company": "Portico" }, { - "name": "Shelley Rodriquez", + "name": "Kirkland Merrill", + "gender": "male", + "company": "Utara" + }, + { + "name": "Kristi Brewer", "gender": "female", - "company": "Bostonic" + "company": "Oronoko" }, { - "name": "Cora Chase", + "name": "Lakisha Huber", "gender": "female", - "company": "Isonus" + "company": "Insource" }, { - "name": "Mckay Santos", + "name": "Lancaster Patel", "gender": "male", - "company": "Amtas" + "company": "Krog" }, { - "name": "Hilda Crane", + "name": "Lelia Gates", "gender": "female", - "company": "Jumpstack" + "company": "Proxsoft" }, { - "name": "Jeanne Lindsay", + "name": "Letitia Vasquez", "gender": "female", - "company": "Genesynk" - }, - { - "name": "Frye Sharpe", - "gender": "male", - "company": "Eplode" + "company": "Slumberia" }, { - "name": "Velma Fry", + "name": "Lindsay Avery", "gender": "female", - "company": "Ronelon" + "company": "Unq" }, { - "name": "Reyna Espinoza", + "name": "Luisa Farrell", "gender": "female", - "company": "Prismatic" - }, - { - "name": "Spencer Sloan", - "gender": "male", - "company": "Comverges" + "company": "Cinesanct" }, { - "name": "Graham Marsh", - "gender": "male", - "company": "Medifax" + "name": "Lynda Mendoza", + "gender": "female", + "company": "Dogspa" }, { - "name": "Hale Boone", - "gender": "male", - "company": "Digial" + "name": "Lynette Stein", + "gender": "female", + "company": "Macronaut" }, { - "name": "Wiley Hubbard", + "name": "Lyons Peters", "gender": "male", - "company": "Zensus" + "company": "Quinex" }, { - "name": "Blackburn Drake", - "gender": "male", - "company": "Frenex" + "name": "Marcy Green", + "gender": "female", + "company": "Pharmex" }, { - "name": "Franco Hunter", + "name": "Mcfarland Sparks", "gender": "male", - "company": "Rockabye" + "company": "Comvey" }, { - "name": "Barnett Case", + "name": "Mckay Santos", "gender": "male", - "company": "Norali" + "company": "Amtas" }, { - "name": "Alexander Foley", + "name": "Merritt Cole", "gender": "male", - "company": "Geekosis" + "company": "Techtrix" }, { - "name": "Lynette Stein", + "name": "Milagros Finch", "gender": "female", - "company": "Macronaut" + "company": "Handshake" }, { - "name": "Anthony Joyner", + "name": "Neal Harvey", "gender": "male", - "company": "Senmei" + "company": "Pyramax" }, { - "name": "Garrett Brennan", - "gender": "male", - "company": "Bluegrain" + "name": "Nellie Whitfield", + "gender": "female", + "company": "Exospace" }, { - "name": "Betsy Horton", + "name": "Nola Miles", "gender": "female", - "company": "Zilla" + "company": "Sonique" }, { "name": "Patton Small", @@ -350,153 +355,148 @@ "company": "Genmex" }, { - "name": "Lakisha Huber", - "gender": "female", - "company": "Insource" + "name": "Pena Pena", + "gender": "male", + "company": "Quarx" }, { - "name": "Lindsay Avery", + "name": "Renee Barr", "gender": "female", - "company": "Unq" + "company": "Kiggle" }, { - "name": "Ayers Hood", - "gender": "male", - "company": "Accuprint" + "name": "Reyna Espinoza", + "gender": "female", + "company": "Prismatic" }, { - "name": "Torres Durham", + "name": "Robles Boyle", "gender": "male", - "company": "Uplinx" + "company": "Comtract" }, { - "name": "Vincent Hernandez", - "gender": "male", - "company": "Talendula" + "name": "Robyn Strickland", + "gender": "female", + "company": "Obones" }, { - "name": "Baird Ryan", + "name": "Rocha Meadows", "gender": "male", - "company": "Aquasseur" + "company": "Goko" }, { - "name": "Georgia Mercer", + "name": "Rosa Dyer", "gender": "female", - "company": "Skyplex" + "company": "Netility" }, { - "name": "Francesca Elliott", + "name": "Roseann Jarvis", "gender": "female", - "company": "Nspire" - }, - { - "name": "Lyons Peters", - "gender": "male", - "company": "Quinex" + "company": "Aquazure" }, { - "name": "Kristi Brewer", + "name": "Sara Delgado", "gender": "female", - "company": "Oronoko" + "company": "Netagy" }, { - "name": "Tonya Bray", + "name": "Sarah Massey", "gender": "female", - "company": "Insuron" + "company": "Bisba" }, { - "name": "Valenzuela Huff", + "name": "Schroeder Mathews", "gender": "male", - "company": "Applideck" + "company": "Polarium" }, { - "name": "Tiffany Anderson", + "name": "Shelley Rodriquez", "gender": "female", - "company": "Zanymax" + "company": "Bostonic" }, { - "name": "Jerri King", - "gender": "female", - "company": "Eventex" + "name": "Spencer Sloan", + "gender": "male", + "company": "Comverges" }, { - "name": "Rocha Meadows", + "name": "Stephens Reilly", "gender": "male", - "company": "Goko" + "company": "Acusage" }, { - "name": "Marcy Green", + "name": "Sylvia Sosa", "gender": "female", - "company": "Pharmex" - }, - { - "name": "Kirk Cross", - "gender": "male", - "company": "Portico" + "company": "Circum" }, { - "name": "Hattie Mullen", + "name": "Tiffany Anderson", "gender": "female", - "company": "Zilencio" + "company": "Zanymax" }, { - "name": "Deann Bridges", + "name": "Tonya Bray", "gender": "female", - "company": "Equitox" + "company": "Insuron" }, { - "name": "Chaney Roach", + "name": "Torres Durham", "gender": "male", - "company": "Qualitern" + "company": "Uplinx" }, { - "name": "Consuelo Dickson", - "gender": "female", - "company": "Poshome" + "name": "Trevino Moreno", + "gender": "male", + "company": "Conjurica" }, { - "name": "Billie Rowe", - "gender": "female", - "company": "Cemention" + "name": "Tucker Maxwell", + "gender": "male", + "company": "Lumbrex" }, { - "name": "Bean Donovan", - "gender": "male", - "company": "Mantro" + "name": "Valarie Atkinson", + "gender": "female", + "company": "Hopeli" }, { - "name": "Lancaster Patel", + "name": "Valenzuela Huff", "gender": "male", - "company": "Krog" + "company": "Applideck" }, { - "name": "Rosa Dyer", + "name": "Velma Fry", "gender": "female", - "company": "Netility" + "company": "Ronelon" }, { - "name": "Christine Compton", - "gender": "female", - "company": "Bleeko" + "name": "Vincent Hernandez", + "gender": "male", + "company": "Talendula" }, { - "name": "Milagros Finch", - "gender": "female", - "company": "Handshake" + "name": "Wilder Gonzales", + "gender": "male", + "company": "Geekko" }, { - "name": "Ericka Alvarado", - "gender": "female", - "company": "Lyrichord" + "name": "Wiley Hubbard", + "gender": "male", + "company": "Zensus" }, { - "name": "Sylvia Sosa", - "gender": "female", - "company": "Circum" + "name": "Wong Craft", + "gender": "male", + "company": "Opticall" }, { - "name": "Humphrey Curtis", + "name": "Woods Key", "gender": "male", - "company": "Corepan" + "company": "Bedder" + }, + { + "name": "Yvonne Parsons", + "gender": "female", + "company": "Zolar" } -] \ No newline at end of file +] diff --git a/misc/site/data/100_DESC.json b/misc/site/data/100_DESC.json index 159ea94821..dbd1e0fe7a 100644 --- a/misc/site/data/100_DESC.json +++ b/misc/site/data/100_DESC.json @@ -1,163 +1,183 @@ [ { - "name": "Wilder Gonzales", + "name": "Yvonne Parsons", + "gender": "female", + "company": "Zolar" + }, + { + "name": "Woods Key", "gender": "male", - "company": "Geekko" + "company": "Bedder" }, { - "name": "Valarie Atkinson", - "gender": "female", - "company": "Hopeli" + "name": "Wong Craft", + "gender": "male", + "company": "Opticall" }, { - "name": "Schroeder Mathews", + "name": "Wiley Hubbard", "gender": "male", - "company": "Polarium" + "company": "Zensus" }, { - "name": "Sarah Massey", - "gender": "female", - "company": "Bisba" + "name": "Wilder Gonzales", + "gender": "male", + "company": "Geekko" }, { - "name": "Robles Boyle", + "name": "Vincent Hernandez", "gender": "male", - "company": "Comtract" + "company": "Talendula" }, { - "name": "Nellie Whitfield", + "name": "Velma Fry", "gender": "female", - "company": "Exospace" + "company": "Ronelon" }, { - "name": "Lynda Mendoza", + "name": "Valenzuela Huff", + "gender": "male", + "company": "Applideck" + }, + { + "name": "Valarie Atkinson", "gender": "female", - "company": "Dogspa" + "company": "Hopeli" }, { - "name": "Jackson Macias", + "name": "Tucker Maxwell", "gender": "male", - "company": "Aquamate" + "company": "Lumbrex" }, { - "name": "Georgina Schultz", - "gender": "female", - "company": "Suretech" + "name": "Trevino Moreno", + "gender": "male", + "company": "Conjurica" }, { - "name": "Evans Hickman", + "name": "Torres Durham", "gender": "male", - "company": "Parleynet" + "company": "Uplinx" }, { - "name": "Ethel Price", + "name": "Tonya Bray", "gender": "female", - "company": "Enersol" + "company": "Insuron" }, { - "name": "Dawson Barber", - "gender": "male", - "company": "Dymi" + "name": "Tiffany Anderson", + "gender": "female", + "company": "Zanymax" }, { - "name": "Claudine Neal", + "name": "Sylvia Sosa", "gender": "female", - "company": "Sealoud" + "company": "Circum" }, { - "name": "Carroll Buchanan", + "name": "Stephens Reilly", "gender": "male", - "company": "Ecosys" + "company": "Acusage" }, { - "name": "Bruce Strong", + "name": "Spencer Sloan", "gender": "male", - "company": "Xyqag" + "company": "Comverges" }, { - "name": "Beryl Rice", + "name": "Shelley Rodriquez", "gender": "female", - "company": "Velity" + "company": "Bostonic" }, { - "name": "Pena Pena", + "name": "Schroeder Mathews", "gender": "male", - "company": "Quarx" + "company": "Polarium" }, { - "name": "Lelia Gates", + "name": "Sarah Massey", "gender": "female", - "company": "Proxsoft" + "company": "Bisba" }, { - "name": "Letitia Vasquez", + "name": "Sara Delgado", "gender": "female", - "company": "Slumberia" + "company": "Netagy" }, { - "name": "Trevino Moreno", - "gender": "male", - "company": "Conjurica" + "name": "Roseann Jarvis", + "gender": "female", + "company": "Aquazure" }, { - "name": "Barr Page", - "gender": "male", - "company": "Apex" + "name": "Rosa Dyer", + "gender": "female", + "company": "Netility" }, { - "name": "Kirkland Merrill", + "name": "Rocha Meadows", "gender": "male", - "company": "Utara" + "company": "Goko" }, { - "name": "Blanche Conley", + "name": "Robyn Strickland", "gender": "female", - "company": "Imkan" + "company": "Obones" }, { - "name": "Atkins Dunlap", + "name": "Robles Boyle", "gender": "male", - "company": "Comveyor" + "company": "Comtract" }, { - "name": "Everett Foreman", + "name": "Reyna Espinoza", + "gender": "female", + "company": "Prismatic" + }, + { + "name": "Renee Barr", + "gender": "female", + "company": "Kiggle" + }, + { + "name": "Pena Pena", "gender": "male", - "company": "Maineland" + "company": "Quarx" }, { - "name": "Gould Randolph", + "name": "Patton Small", "gender": "male", - "company": "Intergeek" + "company": "Genmex" }, { - "name": "Kelli Leon", + "name": "Nola Miles", "gender": "female", - "company": "Verbus" + "company": "Sonique" }, { - "name": "Freda Mason", + "name": "Nellie Whitfield", "gender": "female", - "company": "Accidency" + "company": "Exospace" }, { - "name": "Tucker Maxwell", + "name": "Neal Harvey", "gender": "male", - "company": "Lumbrex" + "company": "Pyramax" }, { - "name": "Yvonne Parsons", + "name": "Milagros Finch", "gender": "female", - "company": "Zolar" + "company": "Handshake" }, { - "name": "Woods Key", + "name": "Merritt Cole", "gender": "male", - "company": "Bedder" + "company": "Techtrix" }, { - "name": "Stephens Reilly", + "name": "Mckay Santos", "gender": "male", - "company": "Acusage" + "company": "Amtas" }, { "name": "Mcfarland Sparks", @@ -165,19 +185,24 @@ "company": "Comvey" }, { - "name": "Jocelyn Sawyer", + "name": "Marcy Green", "gender": "female", - "company": "Fortean" + "company": "Pharmex" }, { - "name": "Renee Barr", + "name": "Lyons Peters", + "gender": "male", + "company": "Quinex" + }, + { + "name": "Lynette Stein", "gender": "female", - "company": "Kiggle" + "company": "Macronaut" }, { - "name": "Gaines Beck", - "gender": "male", - "company": "Sequitur" + "name": "Lynda Mendoza", + "gender": "female", + "company": "Dogspa" }, { "name": "Luisa Farrell", @@ -185,84 +210,64 @@ "company": "Cinesanct" }, { - "name": "Robyn Strickland", + "name": "Lindsay Avery", "gender": "female", - "company": "Obones" + "company": "Unq" }, { - "name": "Roseann Jarvis", + "name": "Letitia Vasquez", "gender": "female", - "company": "Aquazure" - }, - { - "name": "Johnston Park", - "gender": "male", - "company": "Netur" + "company": "Slumberia" }, { - "name": "Wong Craft", - "gender": "male", - "company": "Opticall" + "name": "Lelia Gates", + "gender": "female", + "company": "Proxsoft" }, { - "name": "Merritt Cole", + "name": "Lancaster Patel", "gender": "male", - "company": "Techtrix" - }, - { - "name": "Dale Byrd", - "gender": "female", - "company": "Kneedles" + "company": "Krog" }, { - "name": "Sara Delgado", + "name": "Lakisha Huber", "gender": "female", - "company": "Netagy" + "company": "Insource" }, { - "name": "Alisha Myers", + "name": "Kristi Brewer", "gender": "female", - "company": "Intradisk" + "company": "Oronoko" }, { - "name": "Felecia Smith", - "gender": "female", - "company": "Futurity" + "name": "Kirkland Merrill", + "gender": "male", + "company": "Utara" }, { - "name": "Neal Harvey", + "name": "Kirk Cross", "gender": "male", - "company": "Pyramax" + "company": "Portico" }, { - "name": "Nola Miles", + "name": "Kelli Leon", "gender": "female", - "company": "Sonique" + "company": "Verbus" }, { - "name": "Herring Pierce", + "name": "Johnston Park", "gender": "male", - "company": "Geeketron" - }, - { - "name": "Shelley Rodriquez", - "gender": "female", - "company": "Bostonic" + "company": "Netur" }, { - "name": "Cora Chase", + "name": "Jocelyn Sawyer", "gender": "female", - "company": "Isonus" - }, - { - "name": "Mckay Santos", - "gender": "male", - "company": "Amtas" + "company": "Fortean" }, { - "name": "Hilda Crane", + "name": "Jerri King", "gender": "female", - "company": "Jumpstack" + "company": "Eventex" }, { "name": "Jeanne Lindsay", @@ -270,29 +275,29 @@ "company": "Genesynk" }, { - "name": "Frye Sharpe", + "name": "Jackson Macias", "gender": "male", - "company": "Eplode" + "company": "Aquamate" }, { - "name": "Velma Fry", - "gender": "female", - "company": "Ronelon" + "name": "Humphrey Curtis", + "gender": "male", + "company": "Corepan" }, { - "name": "Reyna Espinoza", + "name": "Hilda Crane", "gender": "female", - "company": "Prismatic" + "company": "Jumpstack" }, { - "name": "Spencer Sloan", + "name": "Herring Pierce", "gender": "male", - "company": "Comverges" + "company": "Geeketron" }, { - "name": "Graham Marsh", - "gender": "male", - "company": "Medifax" + "name": "Hattie Mullen", + "gender": "female", + "company": "Zilencio" }, { "name": "Hale Boone", @@ -300,164 +305,154 @@ "company": "Digial" }, { - "name": "Wiley Hubbard", + "name": "Graham Marsh", "gender": "male", - "company": "Zensus" + "company": "Medifax" }, { - "name": "Blackburn Drake", + "name": "Gould Randolph", "gender": "male", - "company": "Frenex" + "company": "Intergeek" }, { - "name": "Franco Hunter", - "gender": "male", - "company": "Rockabye" + "name": "Georgina Schultz", + "gender": "female", + "company": "Suretech" }, { - "name": "Barnett Case", - "gender": "male", - "company": "Norali" + "name": "Georgia Mercer", + "gender": "female", + "company": "Skyplex" }, { - "name": "Alexander Foley", + "name": "Garrett Brennan", "gender": "male", - "company": "Geekosis" - }, - { - "name": "Lynette Stein", - "gender": "female", - "company": "Macronaut" + "company": "Bluegrain" }, { - "name": "Anthony Joyner", + "name": "Gaines Beck", "gender": "male", - "company": "Senmei" + "company": "Sequitur" }, { - "name": "Garrett Brennan", + "name": "Frye Sharpe", "gender": "male", - "company": "Bluegrain" + "company": "Eplode" }, { - "name": "Betsy Horton", + "name": "Freda Mason", "gender": "female", - "company": "Zilla" + "company": "Accidency" }, { - "name": "Patton Small", + "name": "Franco Hunter", "gender": "male", - "company": "Genmex" + "company": "Rockabye" }, { - "name": "Lakisha Huber", + "name": "Francesca Elliott", "gender": "female", - "company": "Insource" + "company": "Nspire" }, { - "name": "Lindsay Avery", + "name": "Felecia Smith", "gender": "female", - "company": "Unq" - }, - { - "name": "Ayers Hood", - "gender": "male", - "company": "Accuprint" + "company": "Futurity" }, { - "name": "Torres Durham", + "name": "Everett Foreman", "gender": "male", - "company": "Uplinx" + "company": "Maineland" }, { - "name": "Vincent Hernandez", + "name": "Evans Hickman", "gender": "male", - "company": "Talendula" + "company": "Parleynet" }, { - "name": "Baird Ryan", - "gender": "male", - "company": "Aquasseur" + "name": "Ethel Price", + "gender": "female", + "company": "Enersol" }, { - "name": "Georgia Mercer", + "name": "Ericka Alvarado", "gender": "female", - "company": "Skyplex" + "company": "Lyrichord" }, { - "name": "Francesca Elliott", + "name": "Deann Bridges", "gender": "female", - "company": "Nspire" + "company": "Equitox" }, { - "name": "Lyons Peters", + "name": "Dawson Barber", "gender": "male", - "company": "Quinex" + "company": "Dymi" }, { - "name": "Kristi Brewer", + "name": "Dale Byrd", "gender": "female", - "company": "Oronoko" + "company": "Kneedles" }, { - "name": "Tonya Bray", + "name": "Cora Chase", "gender": "female", - "company": "Insuron" + "company": "Isonus" }, { - "name": "Valenzuela Huff", - "gender": "male", - "company": "Applideck" + "name": "Consuelo Dickson", + "gender": "female", + "company": "Poshome" }, { - "name": "Tiffany Anderson", + "name": "Claudine Neal", "gender": "female", - "company": "Zanymax" + "company": "Sealoud" }, { - "name": "Jerri King", + "name": "Christine Compton", "gender": "female", - "company": "Eventex" + "company": "Bleeko" }, { - "name": "Rocha Meadows", + "name": "Chaney Roach", "gender": "male", - "company": "Goko" + "company": "Qualitern" }, { - "name": "Marcy Green", - "gender": "female", - "company": "Pharmex" + "name": "Carroll Buchanan", + "gender": "male", + "company": "Ecosys" }, { - "name": "Kirk Cross", + "name": "Bruce Strong", "gender": "male", - "company": "Portico" + "company": "Xyqag" }, { - "name": "Hattie Mullen", + "name": "Blanche Conley", "gender": "female", - "company": "Zilencio" + "company": "Imkan" }, { - "name": "Deann Bridges", - "gender": "female", - "company": "Equitox" + "name": "Blackburn Drake", + "gender": "male", + "company": "Frenex" }, { - "name": "Chaney Roach", - "gender": "male", - "company": "Qualitern" + "name": "Billie Rowe", + "gender": "female", + "company": "Cemention" }, { - "name": "Consuelo Dickson", + "name": "Betsy Horton", "gender": "female", - "company": "Poshome" + "company": "Zilla" }, { - "name": "Billie Rowe", + "name": "Beryl Rice", "gender": "female", - "company": "Cemention" + "company": "Velity" }, { "name": "Bean Donovan", @@ -465,38 +460,43 @@ "company": "Mantro" }, { - "name": "Lancaster Patel", + "name": "Barr Page", "gender": "male", - "company": "Krog" + "company": "Apex" }, { - "name": "Rosa Dyer", - "gender": "female", - "company": "Netility" + "name": "Barnett Case", + "gender": "male", + "company": "Norali" }, { - "name": "Christine Compton", - "gender": "female", - "company": "Bleeko" + "name": "Baird Ryan", + "gender": "male", + "company": "Aquasseur" }, { - "name": "Milagros Finch", - "gender": "female", - "company": "Handshake" + "name": "Ayers Hood", + "gender": "male", + "company": "Accuprint" }, { - "name": "Ericka Alvarado", - "gender": "female", - "company": "Lyrichord" + "name": "Atkins Dunlap", + "gender": "male", + "company": "Comveyor" }, { - "name": "Sylvia Sosa", + "name": "Anthony Joyner", + "gender": "male", + "company": "Senmei" + }, + { + "name": "Alisha Myers", "gender": "female", - "company": "Circum" + "company": "Intradisk" }, { - "name": "Humphrey Curtis", + "name": "Alexander Foley", "gender": "male", - "company": "Corepan" + "company": "Geekosis" } -] \ No newline at end of file +] diff --git a/misc/site/index.html b/misc/site/index.html index 0f656bfd0b..1d12059b3c 100644 --- a/misc/site/index.html +++ b/misc/site/index.html @@ -69,7 +69,7 @@

Angular UI Grid

- + Tutorial

@@ -411,4 +411,4 @@

Complex Example

}); - \ No newline at end of file + diff --git a/misc/tutorial/100_preReqs.ngdoc b/misc/tutorial/100_preReqs.ngdoc index 296a986842..05197e16b3 100644 --- a/misc/tutorial/100_preReqs.ngdoc +++ b/misc/tutorial/100_preReqs.ngdoc @@ -16,6 +16,7 @@ Current list of tested browsers: AngularJS diff --git a/misc/tutorial/102_sorting.ngdoc b/misc/tutorial/102_sorting.ngdoc index 0884a113e3..bbee4d046a 100644 --- a/misc/tutorial/102_sorting.ngdoc +++ b/misc/tutorial/102_sorting.ngdoc @@ -115,7 +115,7 @@ for that column. This will let the user change the direction of the sort, but t

- +

diff --git a/misc/tutorial/103_filtering.ngdoc b/misc/tutorial/103_filtering.ngdoc index 6cba3f5afb..1aa4e6b713 100644 --- a/misc/tutorial/103_filtering.ngdoc +++ b/misc/tutorial/103_filtering.ngdoc @@ -8,7 +8,10 @@ UI-Grid allows you to filter rows. Just set the `enableFiltering` flag in your g Filtering can be disabled at the column level by setting `enableFiltering: false` in the column def. See the "company" column below for an example. -The filter field can be pre-populated by setting `filter: { term: 'xxx' }` in the column def. See the "gender" column below. +The filter field can be pre-populated by setting `filter: { term: 'xxx' }` in the column def. See the "gender" column below. Once +the grid has rendered changes to the columnDef don't reflect in the grid - if they did then users would have their changes to filters +overwritten every time the grid refreshed. If you want to programatically modify filters after initial render then modify +grid.column[i].filters[0] directly. ### Conditon @@ -46,7 +49,28 @@ Occasionally, you may want to provide two or more filters for a single column. T The elements of this array are the same as the contents of the `filter` object in all the previous examples. In fact, `filter: { term: 'xxx' }` is just an alias for `filters: [{ term: 'xxx' }]`. See the "age" column below for an example. +### Date filters +The example also includes date filters. These work, however there isn't a date chooser in the filter widget - so you may need to implement a custom field +if you want to filter dates in this way. +### Dropdowns +Filtering supports dropdowns, in order to set a particular column to use a dropdown you should set: + `type: uiGridConstants.filter.SELECT` +and + `selectOptions: [ { value: 'x', label: 'decode of x' } , .... ]` + +If you need to internationalize the labels you'll need to complete that before providing the selectOptions array. + +### Cancel icon +By default the filter shows a cancel X beside the dropdown. You can set `disableCancelFilterButton: true` to suppress +this button. + +### Programmatic setting of filters +You can set filters + +In this example we've provided a "toggle filters" button to allow you to turn the filter row on and off. To +still visually indicate which columns are filtered even when the filters aren't present, we've used the headerCellClass +to make any columns with a filter condition have blue text. @example @@ -54,13 +78,33 @@ for `filters: [{ term: 'xxx' }]`. See the "age" column below for an example. var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid']); app.controller('MainCtrl', ['$scope', '$http', 'uiGridConstants', function ($scope, $http, uiGridConstants) { + var today = new Date(); + var nextWeek = new Date(); + nextWeek.setDate(nextWeek.getDate() + 7); + + $scope.highlightFilteredHeader = function( row, rowRenderIndex, col, colRenderIndex ) { + if( col.filters[0].term ){ + return 'header-filtered'; + } else { + return ''; + } + }; + $scope.gridOptions = { enableFiltering: true, + onRegisterApi: function(gridApi){ + $scope.gridApi = gridApi; + }, columnDefs: [ // default - { field: 'name' }, + { field: 'name', headerCellClass: $scope.highlightFilteredHeader }, // pre-populated search field - { field: 'gender', filter: { term: 'male' } }, + { field: 'gender', filter: { + term: '1', + type: uiGridConstants.filter.SELECT, + selectOptions: [ { value: '1', label: 'male' }, { value: '2', label: 'female' }, { value: '3', label: 'unknown'}, { value: '4', label: 'not stated' }, { value: '5', label: 'a really long value that extends things' } ] + }, + cellFilter: 'mapGender', headerCellClass: $scope.highlightFilteredHeader }, // no filter input { field: 'company', enableFiltering: false, filter: { noTerm: true, @@ -75,7 +119,7 @@ for `filters: [{ term: 'xxx' }]`. See the "age" column below for an example. filter: { condition: uiGridConstants.filter.ENDS_WITH, placeholder: 'ends with' - } + }, headerCellClass: $scope.highlightFilteredHeader }, // custom condition function { @@ -85,7 +129,7 @@ for `filters: [{ term: 'xxx' }]`. See the "age" column below for an example. var strippedValue = (cellValue + '').replace(/[^\d]/g, ''); return strippedValue.indexOf(searchTerm) >= 0; } - } + }, headerCellClass: $scope.highlightFilteredHeader }, // multiple filters { field: 'age', filters: [ @@ -97,7 +141,14 @@ for `filters: [{ term: 'xxx' }]`. See the "age" column below for an example. condition: uiGridConstants.filter.LESS_THAN, placeholder: 'less than' } - ]} + ], headerCellClass: $scope.highlightFilteredHeader}, + // date filter + { field: 'mixedDate', cellFilter: 'date', width: '15%', filter: { + condition: uiGridConstants.filter.LESS_THAN, + placeholder: 'less than', + term: nextWeek + }, headerCellClass: $scope.highlightFilteredHeader + } ] }; @@ -105,8 +156,33 @@ for `filters: [{ term: 'xxx' }]`. See the "age" column below for an example. .success(function(data) { $scope.gridOptions.data = data; $scope.gridOptions.data[0].age = -5; + + data.forEach( function addDates( row, index ){ + row.mixedDate = new Date(); + row.mixedDate.setDate(today.getDate() + ( index % 14 ) ); + row.gender = row.gender==='male' ? '1' : '2'; + }); }); - }]); + + $scope.toggleFiltering = function(){ + $scope.gridOptions.enableFiltering = !$scope.gridOptions.enableFiltering; + $scope.gridApi.core.notifyDataChange( uiGridConstants.dataChange.COLUMN ); + }; + }]) + .filter('mapGender', function() { + var genderHash = { + 1: 'male', + 2: 'female' + }; + + return function(input) { + if (!input){ + return ''; + } else { + return genderHash[input]; + } + }; + });
@@ -116,6 +192,7 @@ for `filters: [{ term: 'xxx' }]`. See the "age" column below for an example. Note: The third column has the filter input disabled, but actually has a filter set in code that requires every company to have an 'a' in their name.

+
@@ -124,22 +201,27 @@ for `filters: [{ term: 'xxx' }]`. See the "age" column below for an example. width: 650px; height: 400px; } + + .header-filtered { + color: blue; + } var gridTestUtils = require('../../test/e2e/gridTestUtils.spec.js'); describe('first grid on the page, filtered by male by default', function() { - it('grid should have six visible columns', function () { - gridTestUtils.expectHeaderColumnCount( 'grid1', 6 ); + it('grid should have seven visible columns', function () { + gridTestUtils.expectHeaderColumnCount( 'grid1', 7 ); }); - it('filter on 4 columns, filter with greater than/less than on one, one with no filter', function () { + it('filter on 4 columns, filter with greater than/less than on one, one with no filter, then one with one filter', function () { gridTestUtils.expectFilterBoxInColumn( 'grid1', 0, 1 ); - gridTestUtils.expectFilterBoxInColumn( 'grid1', 1, 1 ); + gridTestUtils.expectFilterSelectInColumn( 'grid1', 1, 1 ); gridTestUtils.expectFilterBoxInColumn( 'grid1', 2, 0 ); gridTestUtils.expectFilterBoxInColumn( 'grid1', 3, 1 ); gridTestUtils.expectFilterBoxInColumn( 'grid1', 4, 1 ); gridTestUtils.expectFilterBoxInColumn( 'grid1', 5, 2 ); + gridTestUtils.expectFilterBoxInColumn( 'grid1', 6, 1 ); }); it('third row should be Hatfield Hudson - will be Terry Clay if filtering broken', function () { diff --git a/misc/tutorial/105_footer.ngdoc b/misc/tutorial/105_footer.ngdoc index cdfcc33035..2d86c62170 100644 --- a/misc/tutorial/105_footer.ngdoc +++ b/misc/tutorial/105_footer.ngdoc @@ -48,7 +48,7 @@ You can override the default grid footer template with gridOptions.footerTemplat $http.get('/data/500_complex.json') .success(function(data) { - angular.forEach(data, function(row) { + data.forEach( function(row) { row.registered = Date.parse(row.registered); }); $scope.gridOptions.data = data; diff --git a/misc/tutorial/110_grid_in_modal.ngdoc b/misc/tutorial/110_grid_in_modal.ngdoc index b06a0983e5..cfde0ca5dd 100644 --- a/misc/tutorial/110_grid_in_modal.ngdoc +++ b/misc/tutorial/110_grid_in_modal.ngdoc @@ -5,15 +5,19 @@ Using a grid in a modal popup. In some cases, and in particular with the bootstrap modal, you may find that your grid renders smaller than the -available width. This is because the bootstrap modal animates the initial render and the grid renders whilst the -modal is still animating - the available space isn't as expected. You can correct this by calling `handleWindowResize`. +available width (or sometimes appears to render not at all. This is believed to be because the bootstrap modal +animates the initial render and the grid renders whilst the modal is still animating - the available space isn't +as expected. You can correct this by calling `handleWindowResize`. The animation time seems to be somewhat variable, +so the currently recommended approach is to use $interval, and to call every 500ms for the first 5s after modal opening. + +In a sense this is similar to what the auto-resize feature does, but it only does it for a short period after modal opening. @example var app = angular.module('app', ['ngTouch', 'ui.grid']); - app.controller('MainCtrl', ['$rootScope', '$scope', '$http', 'modal', '$timeout', function ($rootScope, $scope, $http, modal, $timeout) { + app.controller('MainCtrl', ['$rootScope', '$scope', '$http', 'modal', '$interval', function ($rootScope, $scope, $http, modal, $interval) { var myModal = new modal(); $scope.hideGrid = true; @@ -21,7 +25,12 @@ modal is still animating - the available space isn't as expected. You can corre $rootScope.gridOptions = { onRegisterApi: function (gridApi) { $scope.gridApi = gridApi; - } + + // call resize every 200 ms for 2 s after modal finishes opening - usually only necessary on a bootstrap modal + $interval( function() { + $scope.gridApi.core.handleWindowResize(); + }, 10, 500); + } }; $http.get('/data/100.json') @@ -31,11 +40,6 @@ modal is still animating - the available space isn't as expected. You can corre $scope.showModal = function() { myModal.open(); - - // call resize after modal finishes opening - usually only necessary on a bootstrap modal - $timeout( function() { - $scope.gridApi.core.handleWindowResize(); - }); }; }]); diff --git a/misc/tutorial/117_tooltips.ngdoc b/misc/tutorial/117_tooltips.ngdoc new file mode 100644 index 0000000000..f1cc651457 --- /dev/null +++ b/misc/tutorial/117_tooltips.ngdoc @@ -0,0 +1,89 @@ +@ngdoc overview +@name Tutorial: 117 Tooltips +@description + +You can set a tooltip (actually, a title) to pop up when a user hovers over a cell. + +This tooltip can be simply the cell contents, in which case set the columnDef to have +`cellTooltip: true`. Or it can be a function that returns a value derived from the +current column and row - for example: +``` + cellTooltip: function(row, col) { + return 'Name: ' + row.entity.name + ' Company: ' + row.entity.company; + } +``` + +Note that turning on tooltips will create an extra watcher per cell, so it has an impact on overall grid +performance, it is not recommended to turn them on for every column, rather only for the columns likely to have +data that won't be displayable within the grid row (e.g. long description fields). + +Tooltips respect the cellFilter, so if you define a cellFilter it will also be used in the tooltip. + + + + var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid']); + + app.controller('MainCtrl', ['$scope', '$http', 'uiGridConstants', function ($scope, $http, uiGridConstants) { + $scope.gridOptions = { + enableSorting: true, + columnDefs: [ + { field: 'name', cellTooltip: true }, + { field: 'company', cellTooltip: + function( row, col ) { + return 'Name: ' + row.entity.name + ' Company: ' + row.entity.company; + } + }, + { field: 'gender', cellTooltip: true, cellFilter: 'mapGender' }, + ], + onRegisterApi: function( gridApi ) { + $scope.gridApi = gridApi; + $scope.gridApi.core.on.sortChanged( $scope, function( grid, sort ) { + $scope.gridApi.core.notifyDataChange( uiGridConstants.dataChange.COLUMN ); + }) + } + }; + + $http.get('/data/100.json') + .success(function(data) { + data.forEach( function setGender( row, index ){ + row.gender = row.gender==='male' ? '1' : '2'; + }); + + $scope.gridOptions.data = data; + }); + }]) + .filter('mapGender', function() { + var genderHash = { + 1: 'male', + 2: 'female' + }; + + return function(input) { + if (!input){ + return ''; + } else { + return genderHash[input]; + } + }; + }); + + +
+
+
+
+
+
+ + .grid { + width: 500px; + height: 200px; + } + .red { color: red; background-color: yellow !important; } + .blue { color: blue; } + + + var gridTestUtils = require('../../test/e2e/gridTestUtils.spec.js'); + +
+ diff --git a/misc/tutorial/304_grid_menu.ngdoc b/misc/tutorial/121_grid_menu.ngdoc similarity index 89% rename from misc/tutorial/304_grid_menu.ngdoc rename to misc/tutorial/121_grid_menu.ngdoc index 49eb9751c1..b0572bfb36 100644 --- a/misc/tutorial/304_grid_menu.ngdoc +++ b/misc/tutorial/121_grid_menu.ngdoc @@ -1,5 +1,5 @@ @ngdoc overview -@name Tutorial: 304 Grid Menu +@name Tutorial: 121 Grid Menu @description The grid menu can be enabled through setting the gridOption `enableGridMenu`. This adds a settings icon in the top right of the grid, which floats above the column header. The menu by default gives access to show/hide columns, but can be customised to show additional @@ -16,6 +16,9 @@ use i18n on this through the `gridMenuTitleFilter` setting) - `context`: by default, the `action`, `shown` and `active`'s' contexts will have a reference to the grid added as the property `grid` (accessible through `this.grid`. You can pass in your own context by supplying the `context` property to your menu item. It will be accessible through `this.context`. +- `leaveOpen`: by default false, if set to true the menu will be left open after the action +- `order`: the order in the menu that you wish your item to be. Columns are 300 -> 300 + numColumns * 2, + importer and exporter are 150 and 200 respectively The exporter feature also adds menu items to this menu. The `exporterMenuCsv` option is set to false, which suppresses csv export. The 'export selected rows' option is only available @@ -30,6 +33,14 @@ internationalization function that waits 1 second then prefixes each column with You can suppress the ability to show and hide columns by setting the gridOption `gridMenuShowHideColumns: false`, you can suppress the ability to hide individual columns by setting `enableHiding` on that columnDef to false. +The gridMenu button is still a bit ugly. If you have the skills to do so we'd welcome a PR that makes it pretty. +In the meantime, you can override the height to fit with your application in your css: +
+  .ui-grid-menu-button {
+    height: 31px;
+  }
+
+ @example @@ -58,7 +69,8 @@ you can suppress the ability to hide individual columns by setting `enableHiding title: 'Rotate Grid', action: function ($event) { this.grid.element.toggleClass('rotated'); - } + }, + order: 210 } ], onRegisterApi: function( gridApi ){ @@ -66,7 +78,7 @@ you can suppress the ability to hide individual columns by setting `enableHiding // interval of zero just to allow the directive to have initialized $interval( function() { - gridApi.core.addToGridMenu( gridApi.grid, [{ title: 'Dynamic item'}]); + gridApi.core.addToGridMenu( gridApi.grid, [{ title: 'Dynamic item', order: 100}]); }, 0, 1); gridApi.core.on.columnVisibilityChanged( $scope, function( changedColumn ){ @@ -138,6 +150,7 @@ you can suppress the ability to hide individual columns by setting `enableHiding gridTestUtils.expectHeaderCellValueMatch( 'grid1', 0, 'Name' ); gridTestUtils.expectHeaderCellValueMatch( 'grid1', 1, 'Gender' ); + gridTestUtils.unclickGridMenu( 'grid1'); // menu stays open if change columns gridTestUtils.clickGridMenuItem( 'grid1', 12 ); // there are some hidden menu items, this is company_show gridTestUtils.expectHeaderColumnCount( 'grid1', 3 ); gridTestUtils.expectHeaderCellValueMatch( 'grid1', 0, 'Name' ); diff --git a/misc/tutorial/201_editable.ngdoc b/misc/tutorial/201_editable.ngdoc index 63029c5e41..12ae4aed2e 100644 --- a/misc/tutorial/201_editable.ngdoc +++ b/misc/tutorial/201_editable.ngdoc @@ -19,12 +19,18 @@ When using a dropdown editor you need to provide an options array through the `e This array by default should be an array of `{id: xxx, value: xxx}`, although the field tags can be changed through using the `editDropdownIdLabel` and `editDropdownValueLabel` options. +A file chooser is available, through seting the 'editableCellTemplate` on the `columnDef` to `'ui-grid/fileChooserEditor'`. This +file chooser will open the file chosen by the user and assign the value of that file to the model element. In the example below +we use the file chooser to load a file, and we display the filename in the cell. The file is stored against the row in a hidden +column, which we can save to our server or otherwise process. + Custom edit templates should be used for any editor other than the default editors, but be aware that you will likely also need to provide a custom directive similar to the uiGridEditor directive so as to provide `BEGIN_CELL_EDIT, CANCEL_CELL_EDIT and END_CELL_EDIT` events. __ColumnDef Options__: +- `editModelField` (default: undefined) - a bindable expression to use instead of colDef.field when binding the edit control. - `editableCellTemplate` (default: `'ui-grid/cellEditor'`) - Valid html, templateCache Id, or url that returns html content to be compiled when edit mode is invoked. - `enableCellEdit` (default: `false` for columns of type `'object'`, `true` for all other columns) - `true` will enable @@ -50,9 +56,9 @@ The following option is available only if using cellNav feature _Note that the edit functionality uses native html5 edit widgets - the date picker, the dropdown and the input box itself. If your browser does not implement these widgets, then you won't get them. If your browser implements these -widgets in a way that isn't ideal (for example, some browsers don't allow number fields to start with '.', so you can't +widgets in a way that isn't ideal (for example, some browsers don't allow number fields to start with '.', so you can't type in '.5'), then you need to provide a custom editor instead. On the medium term roadmap there is intent -to provide a bootstrap feature, which would provide directives that were compatible with angular-bootstrap directives, +to provide a bootstrap feature, which would provide directives that were compatible with angular-bootstrap directives, allowing use of the bootstrap datepicker and input fields_ @@ -82,6 +88,23 @@ $scope.gridOptions.columnDefs = [ app.controller('MainCtrl', ['$scope', '$http', function ($scope, $http) { $scope.gridOptions = { }; + $scope.storeFile = function( gridRow, gridCol, files ) { + // ignore all but the first file, it can only select one anyway + // set the filename into this column + gridRow.entity.filename = files[0].name; + + // read the file and set it into a hidden column, which we may do stuff with later + var setFile = function(fileContent){ + gridRow.entity.file = fileContent.currentTarget.result; + // put it on scope so we can display it - you'd probably do something else with it + $scope.lastFile = fileContent.currentTarget.result; + $scope.$apply(); + }; + var reader = new FileReader(); + reader.onload = setFile; + reader.readAsText( files[0] ); + }; + $scope.gridOptions.columnDefs = [ { name: 'id', enableCellEdit: false, width: '10%' }, { name: 'name', displayName: 'Name (editable)', width: '20%' }, @@ -101,11 +124,11 @@ $scope.gridOptions.columnDefs = [ { name: 'isActive', displayName: 'Active', type: 'boolean', width: '10%' }, { name: 'pet', displayName: 'Pet', width: '20%', editableCellTemplate: 'ui-grid/dropdownEditor', editDropdownRowEntityOptionsArrayPath: 'foo.bar[0].options', editDropdownIdLabel: 'value' - } + }, + { name: 'filename', displayName: 'File', width: '20%', editableCellTemplate: 'ui-grid/fileChooserEditor', + editFileChooserCallback: $scope.storeFile } ]; - - $scope.msg = {}; $scope.gridOptions.onRegisterApi = function(gridApi){ @@ -158,6 +181,9 @@ $scope.gridOptions.columnDefs = [ Last Cell Edited: {{msg.lastCellEdited}}
+
+
Last file uploaded:
+
{{lastFile}}
diff --git a/misc/tutorial/202_cellnav.ngdoc b/misc/tutorial/202_cellnav.ngdoc index e26b8dbf74..303d58bda7 100644 --- a/misc/tutorial/202_cellnav.ngdoc +++ b/misc/tutorial/202_cellnav.ngdoc @@ -26,8 +26,10 @@ extract values of selected cells. var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.cellNav', 'ui.grid.pinning']); app.controller('MainCtrl', ['$scope', '$http', '$log', function ($scope, $http, $log) { - $scope.gridOptions = {}; - $scope.gridOptions.modifierKeysToMultiSelectCells = true; + $scope.gridOptions = { + modifierKeysToMultiSelectCells: true, + showGridFooter: true + }; $scope.gridOptions.columnDefs = [ { name: 'id', width:'150' }, { name: 'name', width:'200' }, @@ -66,7 +68,7 @@ extract values of selected cells. }; $scope.scrollTo = function( rowIndex, colIndex ) { - $scope.gridApi.cellNav.scrollTo( $scope.gridOptions.data[rowIndex], $scope.gridOptions.columnDefs[colIndex]); + $scope.gridApi.core.scrollTo( $scope.gridOptions.data[rowIndex], $scope.gridOptions.columnDefs[colIndex]); }; $scope.scrollToFocus = function( rowIndex, colIndex ) { diff --git a/misc/tutorial/204_column_resizing.ngdoc b/misc/tutorial/204_column_resizing.ngdoc index 95e19bf1c2..f5e19c20e4 100644 --- a/misc/tutorial/204_column_resizing.ngdoc +++ b/misc/tutorial/204_column_resizing.ngdoc @@ -4,7 +4,17 @@ The Resize Columns feature allows each column to be resized. -To enable, you must include the 'ui.grid.resizeColumns' module and you must include the ui-grid-resize-columns directive on your grid element. +To enable, you must include the 'ui.grid.resizeColumns' module and you must include the ui-grid-resize-columns directive +on your grid element. + +You can set individual columns to not be resizeable, if you do this it is recommended that those columns have a fixed +pixel width - otherwise they may get automatically resized to fill the remaining space if other columns are reduced in size, +and there will be no way to reduce their width again. + +When you resize a column any other columns with fixed widths, or that have already been resized, retain their width. All other +columns resize to take up the remaining space. As long as there is at least one variable column left your columns won't reduce +below the full grid width - but once you've resized all the columns then you can end up with the total column width less than the +grid width.
 angular.module('yourApp', ['ui.grid', 'ui.grid.resizeColumns']);
@@ -45,7 +55,7 @@ $scope.gridOptions = {
       $scope.gridOptions = {
         enableSorting: true,
         columnDefs: [
-          { field: 'name', minWidth: 200, width: '50%', enableColumnResizing: false },
+          { field: 'name', minWidth: 200, width: 250, enableColumnResizing: false },
           { field: 'gender', width: '30%', maxWidth: 200, minWidth: 70 },
           { field: 'company', width: '20%' }
         ]
diff --git a/misc/tutorial/208_save_state.ngdoc b/misc/tutorial/208_save_state.ngdoc
index d9c6809240..e2880435bc 100644
--- a/misc/tutorial/208_save_state.ngdoc
+++ b/misc/tutorial/208_save_state.ngdoc
@@ -30,6 +30,7 @@ default:
 - saveVisible
 - saveSort
 - saveFilter
+- savePinning
 
 @example
 In this example we provide a button to save the grid state.  You can then modify the grid layout
@@ -38,17 +39,17 @@ to something different, and use the restore button to set the grid back the way
 
 
   
-    var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid', 'ui.grid.saveState', 'ui.grid.selection', 'ui.grid.cellNav', 'ui.grid.resizeColumns', 'ui.grid.moveColumns' ]);
+    var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid', 'ui.grid.saveState', 'ui.grid.selection', 'ui.grid.cellNav', 'ui.grid.resizeColumns', 'ui.grid.moveColumns', 'ui.grid.pinning' ]);
 
     app.controller('MainCtrl', ['$scope', '$http', '$interval', function ($scope, $http, $interval) {
       $scope.gridOptions = {
         saveFocus: false,
         saveScroll: true,
-        enableFiltering: true,
         onRegisterApi: function(gridApi){ 
           $scope.gridApi = gridApi;
         }
       };
+      $scope.gridOptions.enableFiltering = true;
       $scope.state = {};
       
       $scope.saveState = function() {
@@ -68,7 +69,7 @@ to something different, and use the restore button to set the grid back the way
   
   
     
-
+
@@ -80,6 +81,7 @@ to something different, and use the restore button to set the grid back the way height: 400px; }
+ var gridTestUtils = require('../../test/e2e/gridTestUtils.spec.js'); describe( '208 save state', function() { diff --git a/misc/tutorial/209_grouping.ngdoc b/misc/tutorial/209_grouping.ngdoc index 90461c9400..9b5666dfc0 100644 --- a/misc/tutorial/209_grouping.ngdoc +++ b/misc/tutorial/209_grouping.ngdoc @@ -25,30 +25,54 @@ filtered rows. Group header rows cannot be edited, and if using the selection feature, cannot be selected. They can, however, be exported. +The group rowHeader by default is only visible when one or more columns are grouped - the aim being +to have no visual impact when grouping is turned on but unused. If you'd like the groupRowHeader permanently +present then set the `groupingRowHeaderAlwaysVisible: true` gridOption. + +If you want to change the grouping programmatically after grid initialisation, you do this through calling the +provided methods: + + - `groupColumn`: groups an individual column. Adds it to the end of the current grouping - so you need to remove + existing grouped columns first if you wanted this to be the only grouping. Adds a sort ASC if there isn't one + - `ungroupColumn`: ungroups an individual column + - `aggregateColumn`: sets aggregation on a column, including setting the aggregation off. Automatically removes + any sort first. + - `setGrouping`: sets all the grouping in one go, removing existing grouping + - `getGrouping`: gets the grouping config for the grid + - `clearGrouping`: clears all current grouping settings + Grouping is still alpha, and under development, however it is included in the distribution files to allow people to start using it. Notable outstandings are: - does not correctly handle columns that are based on functions or complex objects. The groupHeader rows create a fake row.entity, and then set the appropriate fields in that entity. This doesn't work well with complex column definitions at present -- notify data change capability is needed for when people programmatically change the grouping - some more unit testing - enhancement: allow a limit on number of columns grouped - consideration of RTL - not sure whether the indent/outdent should get reversed? - special formatting for header rows in exporter? -- add grouping options to saveState Options to watch out for include: - `groupingIndent`: the expand buttons are indented by a number of pixels (default 10) as the grouping - level gets deeper. Larger values look nicer, but mean that you probably need to make your groupHeader - wider if you're allowing deep grouping -- `groupingRowHeaderWidth`: the width of the grouping row header, important as above + level gets deeper. Larger values look nicer, but take up more space +- `groupingRowHeaderWidth`: the base width of the grouping row header - `groupingSuppressAggregationText`: if your column has a cellFilter, the insertion of text (e.g. 'min: xxxx') - usually breaks the cellFilter. So you can suppress the aggregation text, but then you don't get a clear - visual indication of what sort of aggregation is going on. Refer the example below, the balance column with - an average + usually breaks the cellFilter. So you can suppress the aggregation text, but then you don't get a clear + visual indication of what sort of aggregation is going on. Refer the example below, the balance column with + an average +- `groupingShowCounts`: set to false if you don't like the counts against the groupHeaders + +If you have data in your entity that is 1:1 with a group row (so, for example, you have + +If you would like to suppress the data in a grouped column (so it only shows in the groupHeader rows) this can +be done by overriding the cellTemplate for any of the columns you allow grouping on as follows: + + `cellTemplate: '
{{COL_FIELD CUSTOM_FILTERS}}
'` +In the example below this has been done on the state column only. This isn't included in the base code as it +could potentially interact with people's custom templates. + @example In this example we group by the state column then the gender column, and we count the names (a proxy for @@ -56,19 +80,22 @@ counting the number of rows), we find the max age for each grouping, and we calc suppress the aggregation text on the balance column because we want to format as currency...but that means that we can't easily see that it's an average. +We write a function that extracts the aggregated data for states and genders (if you change the grouping then this +function will stop working), and writes them to the console. + - var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid', 'ui.grid.grouping', 'ui.grid.pinning' ]); + var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid', 'ui.grid.grouping' ]); app.controller('MainCtrl', ['$scope', '$http', '$interval', 'uiGridGroupingConstants', function ($scope, $http, $interval, uiGridGroupingConstants ) { $scope.gridOptions = { enableFiltering: true, columnDefs: [ - { name: 'state', grouping: { groupPriority: 1 }, sort: { direction: 'desc' }, width: '25%' }, - { name: 'gender', grouping: { groupPriority: 2 }, sort: { direction: 'asc' }, width: '20%' }, - { name: 'name', grouping: { aggregation: uiGridGroupingConstants.aggregation.COUNT }, width: '30%' }, + { name: 'name', width: '30%' }, + { name: 'gender', grouping: { groupPriority: 1 }, sort: { priority: 1, direction: 'asc' }, width: '20%', cellFilter: 'mapGender' }, { name: 'age', grouping: { aggregation: uiGridGroupingConstants.aggregation.MAX }, width: '20%' }, { name: 'company', width: '25%' }, + { name: 'state', grouping: { groupPriority: 0 }, sort: { priority: 0, direction: 'desc' }, width: '35%', cellTemplate: '
{{COL_FIELD CUSTOM_FILTERS}}
' }, { name: 'balance', width: '25%', cellFilter: 'currency', groupingSuppressAggregationText: true, grouping: { aggregation: uiGridGroupingConstants.aggregation.AVG } } ], onRegisterApi: function( gridApi ) { @@ -80,8 +107,10 @@ we can't easily see that it's an average. .success(function(data) { for ( var i = 0; i < data.length; i++ ){ data[i].state = data[i].address.state; + data[i].gender = data[i].gender === 'male' ? 1: 2; data[i].balance = Number( data[i].balance.slice(1).replace(/,/,'') ); } + delete data[2].age; $scope.gridOptions.data = data; }); @@ -91,16 +120,65 @@ we can't easily see that it's an average. $scope.toggleRow = function( rowNum ){ $scope.gridApi.grouping.toggleRowGroupingState($scope.gridApi.grid.renderContainers.body.visibleRowCache[rowNum]); - } - }]); + }; + + $scope.changeGrouping = function() { + $scope.gridApi.grouping.clearGrouping(); + $scope.gridApi.grouping.groupColumn('age'); + $scope.gridApi.grouping.aggregateColumn('state', uiGridGroupingConstants.aggregation.COUNT); + }; + + $scope.getAggregates = function() { + var aggregateInfo = {}; + var lastState; + $scope.gridApi.grid.renderContainers.body.visibleRowCache.forEach( function(row) { + if( row.groupHeader ) { + if( row.groupLevel === 0 ){ + // in the format "xxxxx (10)", we want the xxxx and the 10 + if( match = row.entity.state.match(/(.+) \((\d+)\)/) ){ + aggregateInfo[ match[1] ] = { stateTotal: match[2] }; + lastState = match[1]; + } + } else if (row.groupLevel === 1){ + if( match = row.entity.gender.match(/(.+) \((\d+)\)/) ){ + aggregateInfo[ lastState ][ match[1] ] = match[2]; + } + } + } + }); + console.log(aggregateInfo); + }; + }]) + .filter('mapGender', function() { + var genderHash = { + 1: 'male', + 2: 'female' + }; + + return function(input) { + var result; + var match; + if (!input){ + return ''; + } else if (result = genderHash[input]) { + return result; + } else if ( ( match = input.match(/(.+)( \(\d+\))/) ) && ( result = genderHash[match[1]] ) ) { + return result + match[2]; + } else { + return input; + } + }; + });
-
+ + +
diff --git a/misc/tutorial/210_selection.ngdoc b/misc/tutorial/210_selection.ngdoc index 90c532be29..d38249c240 100644 --- a/misc/tutorial/210_selection.ngdoc +++ b/misc/tutorial/210_selection.ngdoc @@ -36,6 +36,11 @@ The selectAll box can be disabled by setting `enableSelectAll` to false. You can set the selection row header column width by setting 'selectionRowHeaderWidth' option. +You can use an `isRowSelectable` function to determine which rows are selectable. If you set this function in the options +after grid initialisation you need to call `gridApi.core.notifyDataChange(uiGridConstants.dataChange.OPTIONS)` to enable +the option. In the grid below pressing the button to "set selectable" will set any rows that have an age > 30 to not be +selectable, and also set the age of the first row to 31. + @example Two examples are provided, the first with rowHeaderSelection and multi-select, the second without. The first example auto-selects the first row once the data is loaded. @@ -44,7 +49,7 @@ auto-selects the first row once the data is loaded. var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.selection']); - app.controller('MainCtrl', ['$scope', '$http', '$log', '$timeout', function ($scope, $http, $log, $timeout) { + app.controller('MainCtrl', ['$scope', '$http', '$log', '$timeout', 'uiGridConstants', function ($scope, $http, $log, $timeout, uiGridConstants) { $scope.gridOptions = { enableRowSelection: true, enableSelectAll: true, @@ -93,6 +98,22 @@ auto-selects the first row once the data is loaded. $scope.toggleRow1 = function() { $scope.gridApi.selection.toggleRowSelection($scope.gridOptions.data[0]); }; + + $scope.setSelectable = function() { + $scope.gridApi.selection.clearSelectedRows(); + + $scope.gridOptions.isRowSelectable = function(row){ + if(row.entity.age > 30){ + return false; + } else { + return true; + } + }; + $scope.gridApi.core.notifyDataChange(uiGridConstants.dataChange.OPTIONS); + + $scope.gridOptions.data[0].age = 31; + $scope.gridApi.core.notifyDataChange(uiGridConstants.dataChange.EDIT); + }; $scope.gridOptions.onRegisterApi = function(gridApi){ //set gridApi on scope @@ -152,6 +173,7 @@ auto-selects the first row once the data is loaded.
+
diff --git a/misc/tutorial/211_two_grids.ngdoc b/misc/tutorial/211_two_grids.ngdoc index adb93879f3..311c8759ab 100644 --- a/misc/tutorial/211_two_grids.ngdoc +++ b/misc/tutorial/211_two_grids.ngdoc @@ -18,7 +18,7 @@ each other. }; $scope.scrollTo = function( rowIndex, colIndex ) { - $scope.gridApi.cellNav.scrollTo( $scope.gridOptions.data[rowIndex], $scope.gridOptions.columnDefs[colIndex]); + $scope.gridApi.core.scrollTo( $scope.gridOptions.data[rowIndex], $scope.gridOptions.columnDefs[colIndex]); }; $http.get('/data/100.json') diff --git a/misc/tutorial/212_infinite_scroll.ngdoc b/misc/tutorial/212_infinite_scroll.ngdoc index d8f184d9f4..bb8a72390a 100644 --- a/misc/tutorial/212_infinite_scroll.ngdoc +++ b/misc/tutorial/212_infinite_scroll.ngdoc @@ -2,89 +2,200 @@ @name Tutorial: 212 Infinite scroll @description -The infinite scroll feature allows the user to lazy load their data to gridOptions.data +The infinite scroll feature allows the user to lazy load their data to gridOptions.data. -Specify percentage when lazy load should trigger: -
-  $scope.gridOptions.infiniteScroll = 20;
-
+Once you reach the top (or bottom) of your real data set, you can notify that no more pages exist +up (or down), and infinite scroll will stop triggering events in that direction. You can also +optionally tell us up-front that there are no more pages up through `infiniteScrollUp = true` or down through +`infiniteScrollDown = true`, and we will never trigger +pages in that direction. By default we assume you have pages down but not up. + +You can specify the number of rows from the end of the dataset at which the infinite scroll will trigger a request for +more data `infiniteScrollRowsFromEnd = 20`. By default we trigger when you are 20 rows away from the end of +the grid (in either direction). + +We will raise a `needMoreData` or `needMoreDataTop` event, which you must listen to and respond to if +you have told us that you have more data available. Once you have retrieved the data and added it to your +data array (at the top if the event was `needMoreDataTop`), you need to call `dataLoaded` to tell us +that you have loaded your data. Optionally, you can tell us that there is no more data, and we won't trigger +further requests for more data in that direction. + +When you have loaded your data we will attempt to adjust the grid scroll to give the appearance of continuous +scrolling. We basically assume that your user will have reached the end of the scroll (upwards or downwards) +by the time the data comes back, and scroll the user to the beginning of the newly added data to reflect that. +In some circumstances this can give "jumpy" scrolling, particularly if you have set your rowsFromEnd to quite a high +value so that you're prefetching the data - if the user is scrolling slowly they might be 50 rows from the end, and +when we process the dataLoaded we suddenly move them to what used to be the end. To avoid this, you can explicitly +save the scroll position before you add data to your data array, through calling `saveScrollPercentage`, and the +`dataLoaded` call will then take that position into account, and attempt to adjust the scroll so that the same +rows are showing once the grid has ingested the data you have added. + +We suppress the normal grid behaviour of propagating the scroll to the parent container when you reach the end +if infinite scroll is enabled and if there is still data in that direction - so if there are pages upwards then +scrolling to the top will get those pages rather than hitting the top and then scrolling your whole page upwards. + +If you are using {@link 307_external_sorting external sorting} or {@link 308_external_filtering external filtering} you may +reload your data whenever scroll or filter events occur. In this situation you'll want to call `resetScroll` to tell the +grid not to try to preserve the previous scroll position. You may also use this call when you've otherwise reset the data +in the grid. You must also tell us whether you allow scrollUp or scrollDown from this position as part of the call. + +You may sometimes remove data, for example if you're keeping 10 pages of data in memory, and you start discarding data +from the top as you add data to the bottom. You can use the `dataRemovedTop` and `dataRemovedBottom` to tell +us that you've discarded data, and we'll aim to set the scroll back to where it was before you removed that data. @example +In this example we have a data set that starts at page 2 of a 5 page data set. Each page is 100 records, so we start at +record 200, and we can scroll up 2 pages, and scroll down 2 pages. You should see smooth scrolling as you move up, when +you hit record zero a touchpad scroll should propagate to the parent page. You should also see smooth scrolling as you +move down, and when you hit record 499 a touchpad scroll should propagate to the parent page. + +We also remove data from the data set in memory, we've decided that we only ever want to hold 4 pages in memory, so we +will discard pages and reset the scrollUp and scrollDown appropriately. Again, when this happens the grid should still +hold the scroll position. + +Finally, we can reset the data, which gets us back to the middle page and sets the scroll to the top. + var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.infiniteScroll']); - app.controller('MainCtrl', ['$scope', '$http', '$log', function ($scope, $http, $log) { - $scope.gridOptions = {}; - - /** - * @ngdoc property - * @name infiniteScrollPercentage - * @propertyOf ui.grid.class:GridOptions - * @description This setting controls at what percentage of the scroll more data - * is requested by the infinite scroll - */ - $scope.gridOptions.infiniteScrollPercentage = 15; - - $scope.gridOptions.columnDefs = [ - { name:'id'}, - { name:'name' }, - { name:'age' } - ]; - var page = 0; - var pageUp = 0; - var getData = function(data, page) { - var res = []; - for (var i = (page * 100); i < (page + 1) * 100 && i < data.length; ++i) { - res.push(data[i]); + app.controller('MainCtrl', ['$scope', '$http', '$timeout', '$q', function ($scope, $http, $timeout, $q) { + $scope.gridOptions = { + infiniteScrollRowsFromEnd: 40, + infiniteScrollUp: true, + infiniteScrollDown: true, + columnDefs: [ + { name:'id'}, + { name:'name' }, + { name:'age' } + ], + data: 'data', + onRegisterApi: function(gridApi){ + gridApi.infiniteScroll.on.needLoadMoreData($scope, $scope.getDataDown); + gridApi.infiniteScroll.on.needLoadMoreDataTop($scope, $scope.getDataUp); + $scope.gridApi = gridApi; } - return res; }; - var getDataUp = function(data, page) { - var res = []; - for (var i = data.length - (page * 100) - 1; (data.length - i) < ((page + 1) * 100) && (data.length - i) > 0; --i) { - data[i].id = -(data.length - data[i].id) - res.push(data[i]); - } - return res; + $scope.data = []; + + $scope.firstPage = 2; + $scope.lastPage = 2; + + $scope.getFirstData = function() { + var promise = $q.defer(); + $http.get('/data/10000_complex.json') + .success(function(data) { + var newData = $scope.getPage(data, $scope.lastPage); + $scope.data = $scope.data.concat(newData); + promise.resolve(); + }); + return promise.promise; + }; + + $scope.getDataDown = function() { + var promise = $q.defer(); + $http.get('/data/10000_complex.json') + .success(function(data) { + $scope.lastPage++; + var newData = $scope.getPage(data, $scope.lastPage); + $scope.gridApi.infiniteScroll.saveScrollPercentage(); + $scope.data = $scope.data.concat(newData); + $scope.gridApi.infiniteScroll.dataLoaded($scope.firstPage > 0, $scope.lastPage < 4).then(function() {$scope.checkDataLength('up');}).then(function() { + promise.resolve(); + }); + }) + .error(function(error) { + $scope.gridApi.infiniteScroll.dataLoaded(); + promise.reject(); + }); + return promise.promise; }; - $http.get('/data/10000_complex.json') + $scope.getDataUp = function() { + var promise = $q.defer(); + $http.get('/data/10000_complex.json') .success(function(data) { - $scope.gridOptions.data = getData(data, page); - ++page; + $scope.firstPage--; + var newData = $scope.getPage(data, $scope.firstPage); + $scope.gridApi.infiniteScroll.saveScrollPercentage(); + $scope.data = newData.concat($scope.data); + $scope.gridApi.infiniteScroll.dataLoaded($scope.firstPage > 0, $scope.lastPage < 4).then(function() {$scope.checkDataLength('down');}).then(function() { + promise.resolve(); + }); + }) + .error(function(error) { + $scope.gridApi.infiniteScroll.dataLoaded(); + promise.reject(); }); + return promise.promise; + }; + - $scope.gridOptions.onRegisterApi = function(gridApi){ - gridApi.infiniteScroll.on.needLoadMoreData($scope,function(){ - $http.get('/data/10000_complex.json') - .success(function(data) { - $scope.gridOptions.data = $scope.gridOptions.data.concat(getData(data, page)); - ++page; - gridApi.infiniteScroll.dataLoaded(); - }) - .error(function() { - gridApi.infiniteScroll.dataLoaded(); + $scope.getPage = function(data, page) { + var res = []; + for (var i = (page * 100); i < (page + 1) * 100 && i < data.length; ++i) { + res.push(data[i]); + } + return res; + }; + + $scope.checkDataLength = function( discardDirection) { + // work out whether we need to discard a page, if so discard from the direction passed in + if( $scope.lastPage - $scope.firstPage > 3 ){ + // we want to remove a page + $scope.gridApi.infiniteScroll.saveScrollPercentage(); + + if( discardDirection === 'up' ){ + $scope.data = $scope.data.slice(100); + $scope.firstPage++; + $timeout(function() { + // wait for grid to ingest data changes + $scope.gridApi.infiniteScroll.dataRemovedTop($scope.firstPage > 0, $scope.lastPage < 4); }); - }); - gridApi.infiniteScroll.on.needLoadMoreDataTop($scope,function(){ - $http.get('/data/10000_complex.json') - .success(function(data) { - $scope.gridOptions.data = getDataUp(data, pageUp).reverse().concat($scope.gridOptions.data); - ++pageUp; - gridApi.infiniteScroll.dataLoaded(); - }) - .error(function() { - gridApi.infiniteScroll.dataLoaded(); + } else { + $scope.data = $scope.data.slice(0, 400); + $scope.lastPage--; + $timeout(function() { + // wait for grid to ingest data changes + $scope.gridApi.infiniteScroll.dataRemovedBottom($scope.firstPage > 0, $scope.lastPage < 4); }); + } + } + }; + + $scope.reset = function() { + $scope.firstPage = 2; + $scope.lastPage = 2; + + // turn off the infinite scroll handling up and down - hopefully this won't be needed after @swalters scrolling changes + $scope.gridApi.infiniteScroll.setScrollDirections( false, false ); + $scope.data = []; + + $scope.getFirstData().then(function(){ + $timeout(function() { + // timeout needed to allow digest cycle to complete,and grid to finish ingesting the data + $scope.gridApi.infiniteScroll.resetScroll( $scope.firstPage > 0, $scope.lastPage < 4 ); + }); }); }; + + $scope.getFirstData().then(function(){ + $timeout(function() { + // timeout needed to allow digest cycle to complete,and grid to finish ingesting the data + // you need to call resetData once you've loaded your data if you want to enable scroll up, + // it adjusts the scroll position down one pixel so that we can generate scroll up events + $scope.gridApi.infiniteScroll.resetScroll( $scope.firstPage > 0, $scope.lastPage < 4 ); + }); + }); + }]);
+ +     First page: {{ firstPage }}     Last page: {{ lastPage }}     data.length: {{ data.length }}
diff --git a/misc/tutorial/214_pagination.ngdoc b/misc/tutorial/214_pagination.ngdoc index daa927fe29..7edc52881b 100644 --- a/misc/tutorial/214_pagination.ngdoc +++ b/misc/tutorial/214_pagination.ngdoc @@ -4,6 +4,10 @@ When pagination is enabled, the data is displayed in pages that can be browsed using using the built in pagination selector. +If you wanted server based pagination, you could look at {@link 318_external_pagination the external pagination tutorial} or consider +using {@link 212_infinite_scroll infinite scroll}, which also retrieves data in pages from the server. + + @example diff --git a/misc/tutorial/215_treeView.ngdoc b/misc/tutorial/215_treeView.ngdoc new file mode 100644 index 0000000000..0b1039e901 --- /dev/null +++ b/misc/tutorial/215_treeView.ngdoc @@ -0,0 +1,116 @@ +@ngdoc overview +@name Tutorial: 215 Tree View +@description The tree view feature allows you to create a tree from your grid, specifying which +of your data rows are nodes and which are leaves. + +In your data you tell us the nodes by setting the property $$treeLevel on a given row. Levels +start at 0 and increase as you move down the tree. + +If you wish to load your tree incrementally, you can listen to the rowExpanded event, which will +tell you whenever a row is expanded. You can then retrieve additional data from the server and +splice it into the data array at the right point, the grid will automatically render the data +when it arrives. + +In general it doesn't make sense to allow sorting when you're using the grid as a tree - the +structure of the data is very positional, and if the user were to sort the data it would break +the tree. + +TreeView is still alpha, and under development, however it is included in the distribution files +to allow people to start using it. Notable outstandings are: +- doesn't calculate or display counts of child nodes anywhere, it would be nice if it did +- it would be nice to display an hourglass or icon whilst additional data was loading, the current + arrangement means the grid doesn't know whether or not you're adding additional data +- perhaps we could permit sorting of the nodes, and the children within each node, rather than sorting the + whole data set. This would be a whole new sort algorithm though +- it might be nice if nodes with no children could have their + removed, we'd need some way to be sure + that these weren't nodes for which children are going to be dynamically loaded + +Options to watch out for include: + +- `treeViewIndent`: the expand buttons are indented by a number of pixels (default 10) as the tree + level gets deeper. Larger values look nicer +- `treeViewRowHeaderWidth`: the width of the tree row header + +@example +In this example most of the data is loaded on initial page load. The nodes under Guerrero Lopez, however, +are loaded only when that row is expanded. They have a 2 second delay to simulate loading from a server. + + + + var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid', 'ui.grid.treeView' ]); + + app.controller('MainCtrl', ['$scope', '$http', '$interval', 'uiGridTreeViewConstants', function ($scope, $http, $interval, uiGridTreeViewConstants ) { + $scope.gridOptions = { + enableSorting: false, + enableFiltering: false, + columnDefs: [ + { name: 'name', width: '30%' }, + { name: 'gender', width: '20%' }, + { name: 'age', width: '20%' }, + { name: 'company', width: '25%' }, + { name: 'state', width: '35%' }, + { name: 'balance', width: '25%', cellFilter: 'currency' } + ], + onRegisterApi: function( gridApi ) { + $scope.gridApi = gridApi; + $scope.gridApi.treeView.on.rowExpanded($scope, function(row) { + if( row.entity.$$hashKey === $scope.gridOptions.data[50].$$hashKey && !$scope.nodeLoaded ) { + $interval(function() { + $scope.gridOptions.data.splice(51,0, + {name: 'Dynamic 1', gender: 'female', age: 53, company: 'Griddable grids', balance: 38000, $$treeLevel: 1}, + {name: 'Dynamic 2', gender: 'male', age: 18, company: 'Griddable grids', balance: 29000, $$treeLevel: 1} + ); + $scope.nodeLoaded = true; + }, 2000, 1); + } + }); + } + }; + + $http.get('/data/500_complex.json') + .success(function(data) { + for ( var i = 0; i < data.length; i++ ){ + data[i].state = data[i].address.state; + data[i].balance = Number( data[i].balance.slice(1).replace(/,/,'') ); + } + data[0].$$treeLevel = 0; + data[1].$$treeLevel = 1; + data[10].$$treeLevel = 1; + data[20].$$treeLevel = 0; + data[25].$$treeLevel = 1; + data[50].$$treeLevel = 0; + data[51].$$treeLevel = 0; + $scope.gridOptions.data = data; + }); + + $scope.expandAll = function(){ + $scope.gridApi.treeView.expandAllRows(); + }; + + $scope.toggleRow = function( rowNum ){ + $scope.gridApi.treeView.toggleRowTreeViewState($scope.gridApi.grid.renderContainers.body.visibleRowCache[rowNum]); + }; + }]); + + + +
+ + + +
+
+
+ + + .grid { + width: 500px; + height: 400px; + } + + + var gridTestUtils = require('../../test/e2e/gridTestUtils.spec.js'); + describe( '215 tree view', function() { + }); + +
diff --git a/misc/tutorial/306_expandable_grid.ngdoc b/misc/tutorial/216_expandable_grid.ngdoc similarity index 95% rename from misc/tutorial/306_expandable_grid.ngdoc rename to misc/tutorial/216_expandable_grid.ngdoc index f336484247..2fa8d97b65 100644 --- a/misc/tutorial/306_expandable_grid.ngdoc +++ b/misc/tutorial/216_expandable_grid.ngdoc @@ -1,5 +1,5 @@ @ngdoc overview -@name Tutorial: 306 Expandable grid +@name Tutorial: 216 Expandable grid @description Module 'ui.grid.expandable' adds the subgrid feature to grid. To show the subgrid you need to provide following grid option: @@ -30,6 +30,9 @@ provided following events and methods fos subGrids: SubGrid nesting can be done upto multiple levels. +In addition to above configuration 'scrollFillerClass' is also available and can be used to style the scroll filler, scroll filler +appears when you quickly scroll through the grid. + @example @@ -162,6 +165,6 @@ SubGrid nesting can be done upto multiple levels. } -
+
\ No newline at end of file diff --git a/misc/tutorial/310_column_moving.ngdoc b/misc/tutorial/217_column_moving.ngdoc similarity index 98% rename from misc/tutorial/310_column_moving.ngdoc rename to misc/tutorial/217_column_moving.ngdoc index d55d334fd8..34352a7e87 100644 --- a/misc/tutorial/310_column_moving.ngdoc +++ b/misc/tutorial/217_column_moving.ngdoc @@ -1,5 +1,5 @@ @ngdoc overview -@name Tutorial: 310 Column Moving +@name Tutorial: 217 Column Moving @description Feature ui.grid.moveColumns allows moving column to a different position. To enable, you must include the `ui.grid.moveColumns` module diff --git a/misc/tutorial/301_custom_row_template.ngdoc b/misc/tutorial/301_custom_row_template.ngdoc deleted file mode 100644 index 9058d41693..0000000000 --- a/misc/tutorial/301_custom_row_template.ngdoc +++ /dev/null @@ -1,63 +0,0 @@ -@ngdoc overview -@name Tutorial: 301 Custom Row Template -@description - -Create a grid almost the same as the most basic one, but with a custom row template. - -You can use [grid.appScope](/docs/#/tutorial/305_appScope) in your row template to access -elements in your controller's scope. More details are on -the [external scopes](/docs/#/tutorial/305_appScope) tutorial. - -@example - - - var app = angular.module('app', ['ngTouch', 'ui.grid']); - - app.controller('MainCtrl', ['$scope', '$http', '$timeout', '$interval', function ($scope, $http, $timeout, $interval) { - var start = new Date(); - var sec = $interval(function () { - var wait = parseInt(((new Date()) - start) / 1000, 10); - $scope.wait = wait + 's'; - }, 1000); - - function rowTemplate() { - return $timeout(function() { - $scope.waiting = 'Done!'; - $interval.cancel(sec); - $scope.wait = ''; - return '
'; - }, 6000); - } - - // Access outside scope functions from row template - $scope.fnOne = function(row) { - console.log(row); - }; - - $scope.waiting = 'Waiting for row template...'; - - $http.get('/data/100.json') - .success(function (data) { - $scope.data = data; - }); - - $scope.gridOptions = { - rowTemplate: rowTemplate(), - data: 'data' - }; - }]); -
- -
- {{ wait }} -
-
-
-
- - .grid { - width: 500px; - height: 300px; - } - -
\ No newline at end of file diff --git a/misc/tutorial/306_custom_filters.ngdoc b/misc/tutorial/306_custom_filters.ngdoc new file mode 100644 index 0000000000..a581647e87 --- /dev/null +++ b/misc/tutorial/306_custom_filters.ngdoc @@ -0,0 +1,183 @@ +@ngdoc overview +@name Tutorial: 306 Custom Filters +@description + +You can provide custom templates for your filter objects, allowing you to use custom widgets +or to implement a filter that calls custom functions in your controller. + +For example, you might implement a filter widget that sets query parameters and passes them to your +http query, and that triggers a refresh whenever the filter changes. + +Alternatively you might implement a bootstrap modal that supports multiple selection, and then +insert those multiple selections into a regex that is used by the filter logic. + +You can bind to any of the information within the filters object within your template/directive. + +In this example we do both of those things: we create a directive that pops up a modal window and +allows selection of one or more from a list of values (using an embedded ng-grid), and we implement +a bootstrap dropdown. + +@example + + + var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid', 'ui.grid.selection']); + + app.controller('MainCtrl', ['$scope', '$http', 'uiGridConstants', function ($scope, $http, uiGridConstants) { + var today = new Date(); + var nextWeek = new Date(); + nextWeek.setDate(nextWeek.getDate() + 7); + + $scope.gridOptions = { + enableFiltering: true, + onRegisterApi: function(gridApi){ + $scope.gridApi = gridApi; + }, + columnDefs: [ + { field: 'name' }, + { field: 'gender', + filterHeaderTemplate: '
', + filter: { + term: 1, + options: [ {id: 1, value: 'male'}, {id: 2, value: 'female'}] // custom attribute that goes with custom directive above + }, + cellFilter: 'mapGender' }, + { field: 'company', enableFiltering: false }, + { field: 'email', enableFiltering: false }, + { field: 'phone', enableFiltering: false }, + { field: 'age', + filterHeaderTemplate: '
' + }, + { field: 'mixedDate', cellFilter: 'date', width: '15%', enableFiltering: false } + ] + }; + + $http.get('/data/500_complex.json') + .success(function(data) { + $scope.gridOptions.data = data; + $scope.gridOptions.data[0].age = -5; + + data.forEach( function addDates( row, index ){ + row.mixedDate = new Date(); + row.mixedDate.setDate(today.getDate() + ( index % 14 ) ); + row.gender = row.gender==='male' ? '1' : '2'; + }); + }); + }]) + + .filter('mapGender', function() { + var genderHash = { + 1: 'male', + 2: 'female' + }; + + return function(input) { + if (!input){ + return ''; + } else { + return genderHash[input]; + } + }; + }) + + .directive('myCustomDropdown', function() { + return { + template: '' + }; + }) + + .controller('myCustomModalCtrl', function( $scope, $compile, $timeout ) { + var $elm; + + $scope.showAgeModal = function() { + $scope.listOfAges = []; + + $scope.col.grid.appScope.gridOptions.data.forEach( function ( row ) { + if ( $scope.listOfAges.indexOf( row.age ) === -1 ) { + $scope.listOfAges.push( row.age ); + } + }); + $scope.listOfAges.sort(); + + $scope.gridOptions = { + data: [], + enableColumnMenus: false, + onRegisterApi: function( gridApi) { + $scope.gridApi = gridApi; + + if ( $scope.colFilter && $scope.colFilter.listTerm ){ + $timeout(function() { + $scope.colFilter.listTerm.forEach( function( age ) { + var entities = $scope.gridOptions.data.filter( function( row ) { + return row.age === age; + }); + + if( entities.length > 0 ) { + $scope.gridApi.selection.selectRow(entities[0]); + } + }); + }); + } + } + }; + + $scope.listOfAges.forEach(function( age ) { + $scope.gridOptions.data.push({age: age}); + }); + + var html = ''; + $elm = angular.element(html); + angular.element(document.body).prepend($elm); + + $compile($elm)($scope); + + }; + + $scope.close = function() { + var ages = $scope.gridApi.selection.getSelectedRows(); + $scope.colFilter.listTerm = []; + + ages.forEach( function( age ) { + $scope.colFilter.listTerm.push( age.age ); + }); + + $scope.colFilter.term = $scope.colFilter.listTerm.join(', '); + $scope.colFilter.condition = new RegExp($scope.colFilter.listTerm.join('|')); + + if ($elm) { + $elm.remove(); + } + }; + }) + + + .directive('myCustomModal', function() { + return { + template: '', + controller: 'myCustomModalCtrl' + }; + }) + ; + +
+ +
+
+
+
+ + .grid { + width: 650px; + height: 400px; + } + .modalGrid { + width: 100px; + height: 200px; + } + .modal-dialog { + width: 150px; + } + + + var gridTestUtils = require('../../test/e2e/gridTestUtils.spec.js'); + +
\ No newline at end of file diff --git a/misc/tutorial/307_external_sorting.ngdoc b/misc/tutorial/307_external_sorting.ngdoc index a06e6e5583..0d3ac0ad99 100644 --- a/misc/tutorial/307_external_sorting.ngdoc +++ b/misc/tutorial/307_external_sorting.ngdoc @@ -110,15 +110,15 @@ column however, so sorting by it has no effect. it('sort by name by clicking header', function () { gridTestUtils.clickHeaderCell( 'grid1', 0 ); - gridTestUtils.expectCellValueMatch( 'grid1', 0, 0, 'Beryl Rice' ); - gridTestUtils.expectCellValueMatch( 'grid1', 1, 0, 'Bruce Strong' ); + gridTestUtils.expectCellValueMatch( 'grid1', 0, 0, 'Alexander Foley' ); + gridTestUtils.expectCellValueMatch( 'grid1', 1, 0, 'Alisha Myers' ); }); it('reverse sort by name by clicking header', function () { gridTestUtils.clickHeaderCell( 'grid1', 0 ); gridTestUtils.clickHeaderCell( 'grid1', 0 ); - gridTestUtils.expectCellValueMatch( 'grid1', 0, 0, 'Wilder Gonzales' ); - gridTestUtils.expectCellValueMatch( 'grid1', 1, 0, 'Valarie Atkinson' ); + gridTestUtils.expectCellValueMatch( 'grid1', 0, 0, 'Yvonne Parsons' ); + gridTestUtils.expectCellValueMatch( 'grid1', 1, 0, 'Woods Key' ); }); it('return to original sort by name by clicking header', function () { diff --git a/misc/tutorial/312_exporting_data_complex.ngdoc b/misc/tutorial/312_exporting_data_complex.ngdoc index c21328ae12..0f027336a0 100644 --- a/misc/tutorial/312_exporting_data_complex.ngdoc +++ b/misc/tutorial/312_exporting_data_complex.ngdoc @@ -70,7 +70,7 @@ We also right align the gender column. $http.get('/data/100.json') .success(function(data) { - angular.forEach( data, function( row, index ) { + data.forEach( function( row, index ) { if( row.gender === 'female' ){ row.gender = 1; } else { diff --git a/misc/tutorial/317_custom_templates.ngdoc b/misc/tutorial/317_custom_templates.ngdoc new file mode 100644 index 0000000000..5d55d15fe3 --- /dev/null +++ b/misc/tutorial/317_custom_templates.ngdoc @@ -0,0 +1,105 @@ +@ngdoc overview +@name Tutorial: 317 Custom Templates +@description + +The grid allows you to override most of the templates, including cellTemplate, headerCellTemplate, rowTemplate +and others. You would typically do this to inject functionality like buttons or to get a very different look and +feel that you couldn't achieve through cell classes and other settings. + +It is generally good practice to at least review the standard template in https://github.com/angular-ui/ng-grid/tree/master/src/templates/ui-grid +to make sure there isn't functionality that you are overriding that you needed to keep. In many cases it is desirable to +use the standard template as a starting point, and add your customisations on top. Also remember that new features +or code changes may mean that you need to upgrade your custom template (if the standard template has been modified). + +In this example we create a grid almost the same as the most basic one, but with a custom row template, and +with a cellTemplate that totals all the items above it in the grid. This template continues to work when the data is +filtered or sorted. + +You can use [grid.appScope](/docs/#/tutorial/305_appScope) in your row template to access +elements in your controller's scope. More details are on +the [scopes](/docs/#/tutorial/305_appScope) tutorial. + +In the cellTemplate you have access to `grid`, `row` and `column`, which allows you to write any of a range of functions. + +@example + + + var app = angular.module('app', ['ngTouch', 'ui.grid']); + + app.controller('MainCtrl', ['$scope', '$http', '$timeout', '$interval', function ($scope, $http, $timeout, $interval) { + var start = new Date(); + var sec = $interval(function () { + var wait = parseInt(((new Date()) - start) / 1000, 10); + $scope.wait = wait + 's'; + }, 1000); + + // you could of course just include the template inline in your code, this example shows a template being returned from a function + function rowTemplate() { + return $timeout(function() { + $scope.waiting = 'Done!'; + $interval.cancel(sec); + $scope.wait = ''; + return '
' + + '
' + + '
'; + }, 6000); + } + + // Access outside scope functions from row template + $scope.rowFormatter = function( row ) { + return row.entity.gender === 'male'; + }; + + $scope.waiting = 'Waiting for row template...'; + + $http.get('/data/100.json') + .success(function (data) { + data.forEach( function(row, index) { + row.widgets = index % 10; + }); + $scope.data = data; + }); + + $scope.gridOptions = { + enableFiltering: true, + rowTemplate: rowTemplate(), + data: 'data', + columnDefs: [ + { name: 'name' }, + { name: 'gender' }, + { name: 'company' }, + { name: 'widgets' }, + { name: 'cumulativeWidgets', field: 'widgets', cellTemplate: '
{{grid.appScope.cumulative(grid, row)}}
' } + ] + }; + + $scope.cumulative = function( grid, myRow ) { + var myRowFound = false; + var cumulativeTotal = 0; + grid.renderContainers.body.visibleRowCache.forEach( function( row, index ) { + if( !myRowFound ) { + cumulativeTotal += row.entity.widgets; + if( row === myRow ) { + myRowFound = true; + } + } + }); + return cumulativeTotal; + }; + }]); +
+ +
+ {{ wait }} +
+
+
+
+ + .grid { + width: 500px; + height: 300px; + } + .my-css-class { color: blue } + +
\ No newline at end of file diff --git a/misc/tutorial/318_external_pagination.ngdoc b/misc/tutorial/318_external_pagination.ngdoc new file mode 100644 index 0000000000..58be6a1a20 --- /dev/null +++ b/misc/tutorial/318_external_pagination.ngdoc @@ -0,0 +1,59 @@ +@ngdoc overview +@name Tutorial: 318 External Pagination +@description + +Pagination can be server based, with clicking the pagination buttons getting a new page of data from the server +rather than internally filtering the data that is held in the grid. + +@example + + + var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.pagination']); + + app.controller('MainCtrl', ['$scope', '$http', function ($scope, $http) { + $scope.gridOptions = { + paginationPageSizes: [25, 50, 75], + paginationPageSize: 25, + useExternalPagination: true, + totalItems: 100, + columnDefs: [ + { name: 'name' }, + { name: 'gender' }, + { name: 'company' } + ], + onRegisterApi: function(gridApi){ + $scope.gridApi = gridApi; + $scope.gridApi.pagination.on.paginationChanged( $scope, function( currentPage, pageSize){ + $scope.getPage(currentPage, pageSize); + }); + } + }; + + + $scope.getPage = function(pageNumber, pageSize){ + var startingRow = pageSize * ( pageNumber - 1); // page number starts at 1, not zero + $http.get('/data/100.json') + .success(function (data) { + var newData = []; + for( var i = startingRow; i < startingRow + $scope.gridOptions.paginationPageSize; i++ ) { + newData.push( data[i] ); + } + $scope.gridOptions.data = newData; + }); + }; + + $scope.getPage(1, 25); + }]); + + +
+

Grid with native pagination controls

+
+
+
+ + .grid { + width: 600px; + } + +
diff --git a/misc/tutorial/401_AllFeatures.ngdoc b/misc/tutorial/401_AllFeatures.ngdoc index 52af778933..f57edc8a4f 100644 --- a/misc/tutorial/401_AllFeatures.ngdoc +++ b/misc/tutorial/401_AllFeatures.ngdoc @@ -11,7 +11,7 @@ All features are enabled to get an idea of performance @example - var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.cellNav', 'ui.grid.edit', 'ui.grid.resizeColumns', 'ui.grid.pinning', 'ui.grid.selection', 'ui.grid.moveColumns']); + var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.cellNav', 'ui.grid.edit', 'ui.grid.resizeColumns', 'ui.grid.pinning', 'ui.grid.selection', 'ui.grid.moveColumns', 'ui.grid.exporter', 'ui.grid.importer', 'ui.grid.grouping']); app.controller('MainCtrl', ['$scope', '$http', '$timeout', '$interval', 'uiGridConstants', function ($scope, $http, $timeout, $interval, uiGridConstants) { @@ -23,6 +23,7 @@ All features are enabled to get an idea of performance $scope.gridOptions.enableGridMenu = true; $scope.gridOptions.showGridFooter = true; $scope.gridOptions.showColumnFooter = true; + $scope.gridOptions.fastWatch = true; $scope.gridOptions.rowIdentity = function(row) { return row.id; @@ -93,7 +94,7 @@ All features are enabled to get an idea of performance
{{ myData.length }} rows
-
+
@@ -109,7 +110,7 @@ All features are enabled to get an idea of performance it('should not duplicate the menu options for pinning when resizing a column', function () { element( by.id('refreshButton') ).click(); gridTestUtils.resizeHeaderCell( 'grid1', 1 ); - gridTestUtils.expectVisibleColumnMenuItems( 'grid1', 1, 5) + gridTestUtils.expectVisibleColumnMenuItems( 'grid1', 1, 5); }); }); diff --git a/misc/tutorial/404_large_data_sets_and_performance.ngdoc b/misc/tutorial/404_large_data_sets_and_performance.ngdoc new file mode 100644 index 0000000000..4d0128fda0 --- /dev/null +++ b/misc/tutorial/404_large_data_sets_and_performance.ngdoc @@ -0,0 +1,88 @@ +@ngdoc overview +@name Tutorial: 404 Large Data Sets and Performance +@description + +The grid provides a richly featured component that allows very large data sets to be displayed without overloading +the browser. It virtualises the rows and columns actually displayed, what this means is that it provides the illusion +of displaying many rows and columns, whilst actually showing the browser only the visible cells plus one or two columns +and rows either side of those currently visible. The remainder of the grid canvas is white space, faked so as to have the +browser correctly show scroll bars that position you in the middle of this space. + +In turn, this means that each time anything changes, in particular scrolling, the grid needs to determine whether it needs +to render additional rows or columns. Rather than adding new rows or columns to the DOM (which is computationally expensive) +it instead shuffles which data elements the current rows and columns point to, and then adjusts the scroll to provide the illusion +that you are smoothly scrolling the grid. If it weren't for the fact that the values inside each of the visible cells were being +changed rapidly, what you'd actually see is the same DOM cells scrolling to the left, then jumping back to the right as another column +comes into view, then scrolling smoothly to the left again. + +As you might expect, this can be quite computationally expensive. Additionally, some operations such as sorting and filtering, must +work against the entire data set, not just those currently visible. + +A different area of complexity in the grid is the provision of complex binding methods (refer {@link 106_binding the binding tutorial}). +These binding methods add noticable overhead to every access to a cell value. The combination of large data sets and these +binding methods can result in noticeable performance degradation. + +All of this is a rather long winded way of saying that if we turn off those complex binding methods, many parts of the grid run +faster. + +So, if you have a data array that consists purely of flat objects - and each column in your grid is tied to a single field in the +entities in that array (i.e. the way we think 90% of people use the grid), then you can turn off the support for complex binding, +and the grid will run faster. + +The option for this is `flatEntityAccess`. The below grid provides a toggle on this value so you can see the difference it makes +with a data set of 640,000 rows. + +Further, the grid watches the data and the column defs to determine if anything has changed. Watches get called very frequently in +angularJS (on every digest cycle, which can be many times per second), and watching each row of a large data collection can be expensive. +The `fastWatch` gridOption watches only the data reference and it's length - in theory this means we might not notice if you replaced +a row in the data, or replaced a column in the columnDefs. If using fastWatch you should always do deletes and adds separately, never +a swap. Fastwatch cannot be dynamically toggled - you need to set it upon grid creation. + +@example + + + var app = angular.module('app', ['ngAnimate', 'ngTouch', 'ui.grid']); + + app.controller('MainCtrl', ['$scope', '$http', 'uiGridConstants', function ($scope, $http, uiGridConstants) { + $scope.gridOptions = { + enableFiltering: true, + flatEntityAccess: true, + showGridFooter: true, + fastWatch: true + }; + + $scope.gridOptions.columnDefs = [ + {name:'id'}, + {name:'name'}, + {name:'gender'}, + {field:'age'} + ]; + + $http.get('/data/10000_complex.json') + .success(function(data) { + for( var i=0; i<6; i++){ + data = data.concat(data); + } + $scope.gridOptions.data = data; + }); + + $scope.toggleFlat = function() { + $scope.gridOptions.flatEntityAccess = !$scope.gridOptions.flatEntityAccess; + } + }]); + + +
+ Current setting: {{gridOptions.flatEntityAccess}} +
+
+
+ + .grid { + width: 650px; + height: 400px; + } + + + +
\ No newline at end of file diff --git a/misc/tutorial/405_exporting_all_data_complex.ngdoc b/misc/tutorial/405_exporting_all_data_complex.ngdoc new file mode 100644 index 0000000000..1bc8c721eb --- /dev/null +++ b/misc/tutorial/405_exporting_all_data_complex.ngdoc @@ -0,0 +1,100 @@ +@ngdoc overview +@name Tutorial: 405 Exporting All Data With External Pagination +@description + +When using built in pagination, the data is fully loaded before export. + +For external pagination, use the `exportAllDataPromise` function to load all grid data. If you get all the +data (for the purposes of exporting), then it makes sense to turn off external pagination and external sorting, +as all the data is now present within AngularJS + +@example +This shows combined external pagination and sorting. + + + var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.pagination', 'ui.grid.selection', 'ui.grid.exporter']); + + app.controller('MainCtrl', [ + '$scope', '$http', 'uiGridConstants', function($scope, $http, uiGridConstants) { + + var paginationOptions = { + sort: null + }; + + $scope.gridOptions = { + paginationPageSizes: [25, 50, 75], + paginationPageSize: 25, + useExternalPagination: true, + useExternalSorting: true, + enableGridMenu: true, + columnDefs: [ + { name: 'name' }, + { name: 'gender', enableSorting: false }, + { name: 'company', enableSorting: false } + ], + exporterAllDataPromise: function() { + return getPage(1, $scope.gridOptions.totalItems, paginationOptions.sort) + .then(function() { + $scope.gridOptions.useExternalPagination = false; + $scope.gridOptions.useExternalSorting = false; + getPage = null; + }); + }, + onRegisterApi: function(gridApi) { + $scope.gridApi = gridApi; + $scope.gridApi.core.on.sortChanged($scope, function(grid, sortColumns) { + if(getPage) { + if (sortColumns.length > 0) { + paginationOptions.sort = sortColumns[0].sort.direction; + } else { + paginationOptions.sort = null; + } + getPage(grid.options.paginationCurrentPage, grid.options.paginationPageSize, paginationOptions.sort) + } + }); + gridApi.pagination.on.paginationChanged($scope, function (newPage, pageSize) { + if(getPage) { + getPage(newPage, pageSize, paginationOptions.sort); + } + }); + } + }; + + var getPage = function(curPage, pageSize, sort) { + var url; + switch(sort) { + case uiGridConstants.ASC: + url = '/data/100_ASC.json'; + break; + case uiGridConstants.DESC: + url = '/data/100_DESC.json'; + break; + default: + url = '/data/100.json'; + break; + } + + var _scope = $scope; + return $http.get(url) + .success(function (data) { + var firstRow = (curPage - 1) * pageSize; + $scope.gridOptions.totalItems = 100; + $scope.gridOptions.data = data.slice(firstRow, firstRow + pageSize) + }); + }; + + getPage(1, $scope.gridOptions.paginationPageSize); + } + ]); + + +
+
+
+
+ + .grid { + width: 600px; + } + +
diff --git a/misc/tutorial/499_FAQ.ngdoc b/misc/tutorial/499_FAQ.ngdoc index 14ef1a6db5..ac64c53d5c 100644 --- a/misc/tutorial/499_FAQ.ngdoc +++ b/misc/tutorial/499_FAQ.ngdoc @@ -27,7 +27,7 @@ There are a number of common gotchas in using the grid, this FAQ aims to cover m If the latter, then you can do it by just adding a counter column to your data: ``` - angular.forEach($scope.myData, function( row, index){ + $scope.myData.forEach( function( row, index){ row.sequence = index; }); ``` @@ -41,5 +41,32 @@ There are a number of common gotchas in using the grid, this FAQ aims to cover m which would be something like: ``` - cellTemplate: '
{{row.grid.renderContainers.body.visibleRowsCache.indexOf(row)}}
-``` \ No newline at end of file + cellTemplate: '
{{grid.renderContainers.body.visibleRowCache.indexOf(row)}}
' +``` + +### What browsers are supported by ui.grid + + Our current testing verifies against IE9+, Chrome, Firefox, Safari 5+, Opera and Android. We expect that the functionality + is compatible with any HTML5 compliant and Javascript enabled browser. Refer {@link 100_preReqs preReqs} + +## What angular versions are supported by ui.grid + + Our current testing uses 1.2.8, 1.2.14, 1.2.26, 1.3.0 and 1.3.6. We intend to remain compatible with all forward versions of 1.3. + Refer {@link 100_preReqs preReqs} + +## How can I wrap text in a cell? + Refer also http://stackoverflow.com/questions/29298968/increase-width-of-column-in-ui-grid + + Firstly, to set the column width you need to use column definitions, then you can set a width in pixels or percentage on each. Refer http://ui-grid.info/docs/#/tutorial/201_editable as an example that has column widths. + + Secondly, there is the ability to add tooltips, which are one way to show longer cells that don't fit in the space available. Refer http://ui-grid.info/docs/#/tutorial/117_tooltips + + Thirdly, you can make the rows taller and therefore have space to wrap content within them. Be aware that all rows must be the same height, so you can't make only the rows that need it taller. + + `gridOptions.rowHeight = 50;` + + You'll also need to set the white-space attribute on the div so that it wraps, which you can do by setting a class in the cellTemplate, and then adding a style to the css. + + A plunker as an example: http://plnkr.co/edit/kyhRm08ZtIKYspDqgyRa?p=preview + + \ No newline at end of file diff --git a/package.json b/package.json index a4fd95d77b..bd492aaa93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ui-grid", - "version": "3.0.0-rc.20", + "version": "3.0.0-rc.21", "description": "__Contributors:__", "directories": { "test": "test" @@ -44,6 +44,7 @@ "karma-requirejs": "~0.2", "karma-chrome-launcher": "~0.1", "karma-firefox-launcher": "~0.1", + "karma-ie-launcher": "^0.1.5", "karma-phantomjs-launcher": "~0.1", "karma-sauce-launcher": "~0.2.10", "karma-script-launcher": "~0.1", diff --git a/src/features/cellnav/js/cellnav.js b/src/features/cellnav/js/cellnav.js index c708af4aff..36066e18de 100644 --- a/src/features/cellnav/js/cellnav.js +++ b/src/features/cellnav/js/cellnav.js @@ -94,6 +94,18 @@ }; + UiGridCellNav.prototype.initializeSelection = function () { + var focusableCols = this.getFocusableCols(); + var focusableRows = this.getFocusableRows(); + if (focusableCols.length === 0 || focusableRows.length === 0) { + return null; + } + + var curRowIndex = 0; + var curColIndex = 0; + return new RowCol(focusableRows[0], focusableCols[0]); //return same row + }; + UiGridCellNav.prototype.getRowColLeft = function (curRow, curCol) { var focusableCols = this.getFocusableCols(); var focusableRows = this.getFocusableRows(); @@ -126,6 +138,8 @@ } }; + + UiGridCellNav.prototype.getRowColRight = function (curRow, curCol) { var focusableCols = this.getFocusableCols(); var focusableRows = this.getFocusableRows(); @@ -251,7 +265,12 @@ initializeGrid: function (grid) { grid.registerColumnBuilder(service.cellNavColumnBuilder); - //create variables for state + + /** + * @ngdoc object + * @name ui.grid.cellNav:Grid.cellNav + * @description cellNav properties added to grid class + */ grid.cellNav = {}; grid.cellNav.lastRowCol = null; grid.cellNav.focusedCells = []; @@ -278,24 +297,23 @@ * @param {object} newRowCol new position * @param {object} oldRowCol old position */ - navigate: function (newRowCol, oldRowCol) { - } + navigate: function (newRowCol, oldRowCol) {}, + /** + * @ngdoc event + * @name viewPortKeyDown + * @eventOf ui.grid.cellNav.api:PublicApi + * @description is raised when the viewPort receives a keyDown event. Cells never get focus in uiGrid + * due to the difficulties of setting focus on a cell that is not visible in the viewport. Use this + * event whenever you need a keydown event on a cell + *
+ * @param {object} event keydown event + * @param {object} rowCol current rowCol position + */ + viewPortKeyDown: function (event, rowCol) {} } }, methods: { cellNav: { - /** - * @ngdoc function - * @name scrollTo - * @methodOf ui.grid.cellNav.api:PublicApi - * @description brings the specified row and column into view - * @param {object} rowEntity gridOptions.data[] array instance to make visible - * @param {object} colDef to make visible - */ - scrollTo: function (rowEntity, colDef) { - service.scrollTo(grid, rowEntity, colDef); - }, - /** * @ngdoc function * @name scrollToFocus @@ -304,21 +322,10 @@ * to that cell * @param {object} rowEntity gridOptions.data[] array instance to make visible and set focus * @param {object} colDef to make visible and set focus + * @returns {promise} a promise that is resolved after any scrolling is finished */ scrollToFocus: function (rowEntity, colDef) { - service.scrollToFocus(grid, rowEntity, colDef); - }, - - /** - * @ngdoc function - * @name scrollToIfNecessary - * @methodOf ui.grid.cellNav.api:PublicApi - * @description brings the specified row and column fully into view if it isn't already - * @param {GridRow} row grid row that we should make fully visible - * @param {GridCol} col grid col to make fully visible - */ - scrollToIfNecessary: function (row, col) { - service.scrollToIfNecessary(grid, row, col); + return service.scrollToFocus(grid, rowEntity, colDef); }, /** @@ -482,30 +489,6 @@ return $q.all(promises); }, - /** - * @ngdoc method - * @methodOf ui.grid.cellNav.service:uiGridCellNavService - * @name scrollTo - * @description Scroll the grid such that the specified - * row and column is in view - * @param {Grid} grid the grid you'd like to act upon, usually available - * from gridApi.grid - * @param {object} rowEntity gridOptions.data[] array instance to make visible - * @param {object} colDef to make visible - */ - scrollTo: function (grid, rowEntity, colDef) { - var gridRow = null, gridCol = null; - - if (rowEntity !== null && typeof(rowEntity) !== 'undefined' ) { - gridRow = grid.getRow(rowEntity); - } - - if (colDef !== null && typeof(colDef) !== 'undefined' ) { - gridCol = grid.getColumn(colDef.name ? colDef.name : colDef.field); - } - this.scrollToIfNecessary(grid, gridRow, gridCol); - }, - /** * @ngdoc method * @methodOf ui.grid.cellNav.service:uiGridCellNavService @@ -516,6 +499,7 @@ * from gridApi.grid * @param {object} rowEntity gridOptions.data[] array instance to make visible and set focus to * @param {object} colDef to make visible and set focus to + * @returns {promise} a promise that is resolved after any scrolling is finished */ scrollToFocus: function (grid, rowEntity, colDef) { var gridRow = null, gridCol = null; @@ -527,211 +511,20 @@ if (typeof(colDef) !== 'undefined' && colDef !== null) { gridCol = grid.getColumn(colDef.name ? colDef.name : colDef.field); } - this.scrollToIfNecessary(grid, gridRow, gridCol); - - var rowCol = { row: gridRow, col: gridCol }; - - // Broadcast the navigation - grid.cellNav.broadcastCellNav(rowCol); - - }, - - /** - * @ngdoc method - * @methodOf ui.grid.cellNav.service:uiGridCellNavService - * @name scrollToInternal - * @description Like scrollTo, but takes gridRow and gridCol. - * In calculating the scroll height we have to deal with wanting - * 0% for the first row, and 100% for the last row. Normal maths - * for a 10 row list would return 1/10 = 10% for the first row, so - * we need to tweak the numbers to add an extra 10% somewhere. The - * formula if we're trying to get to row 0 in a 10 row list (assuming our - * index is zero based, so the last row is row 9) is: - *
-         *   0 + 0 / 10 = 0%
-         * 
- * - * To get to row 9 (i.e. the last row) in the same list, we want to - * go to: - *
-         *  ( 9 + 1 ) / 10 = 100%
-         * 
- * So we need to apportion one whole row within the overall grid scroll, - * the formula is: - *
-         *   ( index + ( index / (total rows - 1) ) / total rows
-         * 
- * @param {Grid} grid the grid you'd like to act upon, usually available - * from gridApi.grid - * @param {GridRow} gridRow row to make visible - * @param {GridCol} gridCol column to make visible - */ - scrollToInternal: function (grid, gridRow, gridCol) { - var scrollEvent = new ScrollEvent(grid,null,null,'uiGridCellNavService.scrollToInternal'); - - if (gridRow !== null) { - var seekRowIndex = grid.renderContainers.body.visibleRowCache.indexOf(gridRow); - var totalRows = grid.renderContainers.body.visibleRowCache.length; - var percentage = ( seekRowIndex + ( seekRowIndex / ( totalRows - 1 ) ) ) / totalRows; - scrollEvent.y = { percentage: percentage }; - } - - if (gridCol !== null) { - scrollEvent.x = { percentage: this.getLeftWidth(grid, gridCol) / this.getLeftWidth(grid, grid.renderContainers.body.visibleColumnCache[grid.renderContainers.body.visibleColumnCache.length - 1] ) }; - } - - if (scrollEvent.y || scrollEvent.x) { - scrollEvent.fireScrollingEvent(); - } - }, - - /** - * @ngdoc method - * @methodOf ui.grid.cellNav.service:uiGridCellNavService - * @name scrollToIfNecessary - * @description Scrolls the grid to make a certain row and column combo visible, - * in the case that it is not completely visible on the screen already. - * @param {Grid} grid the grid you'd like to act upon, usually available - * from gridApi.grid - * @param {GridRow} gridRow row to make visible - * @param {GridCol} gridCol column to make visible - */ - scrollToIfNecessary: function (grid, gridRow, gridCol) { - var scrollEvent = new ScrollEvent(grid, 'uiGridCellNavService.scrollToIfNecessary'); - - // Alias the visible row and column caches - var visRowCache = grid.renderContainers.body.visibleRowCache; - var visColCache = grid.renderContainers.body.visibleColumnCache; - - /*-- Get the top, left, right, and bottom "scrolled" edges of the grid --*/ - - // The top boundary is the current Y scroll position PLUS the header height, because the header can obscure rows when the grid is scrolled downwards - var topBound = grid.renderContainers.body.prevScrollTop + grid.headerHeight; - - // Don't the let top boundary be less than 0 - topBound = (topBound < 0) ? 0 : topBound; - - // The left boundary is the current X scroll position - var leftBound = grid.renderContainers.body.prevScrollLeft; - - // The bottom boundary is the current Y scroll position, plus the height of the grid, but minus the header height. - // Basically this is the viewport height added on to the scroll position - var bottomBound = grid.renderContainers.body.prevScrollTop + grid.gridHeight - grid.headerHeight; - - // If there's a horizontal scrollbar, remove its height from the bottom boundary, otherwise we'll be letting it obscure rows - //if (grid.horizontalScrollbarHeight) { - // bottomBound = bottomBound - grid.horizontalScrollbarHeight; - //} - - // The right position is the current X scroll position minus the grid width - var rightBound = grid.renderContainers.body.prevScrollLeft + Math.ceil(grid.gridWidth); - - // If there's a vertical scrollbar, subtract it from the right boundary or we'll allow it to obscure cells - //if (grid.verticalScrollbarWidth) { - // rightBound = rightBound - grid.verticalScrollbarWidth; - //} - - // We were given a row to scroll to - if (gridRow !== null) { - // This is the index of the row we want to scroll to, within the list of rows that can be visible - var seekRowIndex = visRowCache.indexOf(gridRow); - - // Total vertical scroll length of the grid - var scrollLength = (grid.renderContainers.body.getCanvasHeight() - grid.renderContainers.body.getViewportHeight()); - - // Add the height of the native horizontal scrollbar to the scroll length, if it's there. Otherwise it will mask over the final row - //if (grid.horizontalScrollbarHeight && grid.horizontalScrollbarHeight > 0) { - // scrollLength = scrollLength + grid.horizontalScrollbarHeight; - //} - - // This is the minimum amount of pixels we need to scroll vertical in order to see this row. - var pixelsToSeeRow = ((seekRowIndex + 1) * grid.options.rowHeight); - - // Don't let the pixels required to see the row be less than zero - pixelsToSeeRow = (pixelsToSeeRow < 0) ? 0 : pixelsToSeeRow; - - var scrollPixels, percentage; - - // If the scroll position we need to see the row is LESS than the top boundary, i.e. obscured above the top of the grid... - if (pixelsToSeeRow < topBound) { - // Get the different between the top boundary and the required scroll position and subtract it from the current scroll position\ - // to get the full position we need - scrollPixels = grid.renderContainers.body.prevScrollTop - (topBound - pixelsToSeeRow); + return grid.api.core.scrollToIfNecessary(gridRow, gridCol).then(function () { + var rowCol = { row: gridRow, col: gridCol }; - // Turn the scroll position into a percentage and make it an argument for a scroll event - percentage = scrollPixels / scrollLength; - scrollEvent.y = { percentage: percentage }; + // Broadcast the navigation + if (gridRow !== null && gridCol !== null) { + grid.cellNav.broadcastCellNav(rowCol); } - // Otherwise if the scroll position we need to see the row is MORE than the bottom boundary, i.e. obscured below the bottom of the grid... - else if (pixelsToSeeRow > bottomBound) { - // Get the different between the bottom boundary and the required scroll position and add it to the current scroll position - // to get the full position we need - scrollPixels = pixelsToSeeRow - bottomBound + grid.renderContainers.body.prevScrollTop; - - // Turn the scroll position into a percentage and make it an argument for a scroll event - percentage = scrollPixels / scrollLength; - scrollEvent.y = { percentage: percentage }; - } - } - - // We were given a column to scroll to - if (gridCol !== null) { - // This is the index of the row we want to scroll to, within the list of rows that can be visible - var seekColumnIndex = visColCache.indexOf(gridCol); - - // Total vertical scroll length of the grid - var horizScrollLength = (grid.renderContainers.body.getCanvasWidth() - grid.renderContainers.body.getViewportWidth()); - - // Add the height of the native horizontal scrollbar to the scroll length, if it's there. Otherwise it will mask over the final row - // if (grid.verticalScrollbarWidth && grid.verticalScrollbarWidth > 0) { - // horizScrollLength = horizScrollLength + grid.verticalScrollbarWidth; - // } - - // This is the minimum amount of pixels we need to scroll vertical in order to see this column - var columnLeftEdge = 0; - for (var i = 0; i < seekColumnIndex; i++) { - var col = visColCache[i]; - columnLeftEdge += col.drawnWidth; - } - columnLeftEdge = (columnLeftEdge < 0) ? 0 : columnLeftEdge; - - var columnRightEdge = columnLeftEdge + gridCol.drawnWidth; - - // Don't let the pixels required to see the column be less than zero - columnRightEdge = (columnRightEdge < 0) ? 0 : columnRightEdge; - - var horizScrollPixels, horizPercentage; + }); - // If the scroll position we need to see the row is LESS than the top boundary, i.e. obscured above the top of the grid... - if (columnLeftEdge < leftBound) { - // Get the different between the top boundary and the required scroll position and subtract it from the current scroll position\ - // to get the full position we need - horizScrollPixels = grid.renderContainers.body.prevScrollLeft - (leftBound - columnLeftEdge); - // Turn the scroll position into a percentage and make it an argument for a scroll event - horizPercentage = horizScrollPixels / horizScrollLength; - horizPercentage = (horizPercentage > 1) ? 1 : horizPercentage; - scrollEvent.x = { percentage: horizPercentage }; - } - // Otherwise if the scroll position we need to see the row is MORE than the bottom boundary, i.e. obscured below the bottom of the grid... - else if (columnRightEdge > rightBound) { - // Get the different between the bottom boundary and the required scroll position and add it to the current scroll position - // to get the full position we need - horizScrollPixels = columnRightEdge - rightBound + grid.renderContainers.body.prevScrollLeft; - - // Turn the scroll position into a percentage and make it an argument for a scroll event - horizPercentage = horizScrollPixels / horizScrollLength; - horizPercentage = (horizPercentage > 1) ? 1 : horizPercentage; - scrollEvent.x = { percentage: horizPercentage }; - } - } - // If we need to scroll on either the x or y axes, fire a scroll event - if (scrollEvent.y || scrollEvent.x) { - scrollEvent.fireScrollingEvent(); - } }, + /** * @ngdoc method * @methodOf ui.grid.cellNav.service:uiGridCellNavService @@ -805,8 +598,8 @@
*/ - module.directive('uiGridCellnav', ['gridUtil', 'uiGridCellNavService', 'uiGridCellNavConstants', 'uiGridConstants', - function (gridUtil, uiGridCellNavService, uiGridCellNavConstants, uiGridConstants) { + module.directive('uiGridCellnav', ['gridUtil', 'uiGridCellNavService', 'uiGridCellNavConstants', 'uiGridConstants', '$timeout', + function (gridUtil, uiGridCellNavService, uiGridCellNavConstants, uiGridConstants, $timeout) { return { replace: true, priority: -150, @@ -823,11 +616,15 @@ uiGridCtrl.cellNav = {}; - uiGridCtrl.cellNav.focusCell = function (row, col) { - uiGridCtrl.cellNav.broadcastCellNav({ row: row, col: col }); + uiGridCtrl.cellNav.getActiveCell = function () { + var elms = $elm[0].getElementsByClassName('ui-grid-cell-focus'); + if (elms.length > 0){ + return elms[0]; + } + + return undefined; }; - // gridUtil.logDebug('uiGridEdit preLink'); uiGridCtrl.cellNav.broadcastCellNav = grid.cellNav.broadcastCellNav = function (newRowCol, modifierDown) { modifierDown = !(modifierDown === undefined || !modifierDown); uiGridCtrl.cellNav.broadcastFocus(newRowCol, modifierDown); @@ -848,6 +645,7 @@ if (grid.cellNav.lastRowCol === null || rowColSelectIndex === -1) { var newRowCol = new RowCol(row, col); + grid.api.cellNav.raise.navigate(newRowCol, grid.cellNav.lastRowCol); grid.cellNav.lastRowCol = newRowCol; if (uiGridCtrl.grid.options.modifierKeysToMultiSelectCells && modifierDown) { @@ -865,7 +663,7 @@ uiGridCtrl.cellNav.handleKeyDown = function (evt) { var direction = uiGridCellNavService.getDirection(evt); if (direction === null) { - return true; + return null; } var containerId = 'body'; @@ -878,12 +676,15 @@ if (lastRowCol) { // Figure out which new row+combo we're navigating to var rowCol = uiGridCtrl.grid.renderContainers[containerId].cellNav.getNextRowCol(direction, lastRowCol.row, lastRowCol.col); + var focusableCols = uiGridCtrl.grid.renderContainers[containerId].cellNav.getFocusableCols(); // Shift+tab on top-left cell should exit cellnav on render container if ( // Navigating left direction === uiGridCellNavConstants.direction.LEFT && - // Trying to stay on same row + // New col is last col (i.e. wrap around) + rowCol.col === focusableCols[focusableCols.length - 1] && + // Staying on same row, which means we're at first row rowCol.row === lastRowCol.row && evt.keyCode === uiGridConstants.keymap.TAB && evt.shiftKey @@ -894,6 +695,9 @@ // Tab on bottom-right cell should exit cellnav on render container else if ( direction === uiGridCellNavConstants.direction.RIGHT && + // New col is first col (i.e. wrap around) + rowCol.col === focusableCols[0] && + // Staying on same row, which means we're at first row rowCol.row === lastRowCol.row && evt.keyCode === uiGridConstants.keymap.TAB && !evt.shiftKey @@ -902,14 +706,11 @@ return true; } - - rowCol.eventType = uiGridCellNavConstants.EVENT_TYPE.KEYDOWN; - - // Broadcast the navigation - uiGridCtrl.cellNav.broadcastCellNav(rowCol); - // Scroll to the new cell, if it's not completely visible within the render container's viewport - uiGridCellNavService.scrollToIfNecessary(grid, rowCol.row, rowCol.col); + grid.scrollToIfNecessary(rowCol.row, rowCol.col).then(function () { + uiGridCtrl.cellNav.broadcastCellNav(rowCol); + }); + evt.stopPropagation(); evt.preventDefault(); @@ -925,8 +726,8 @@ }; }]); - module.directive('uiGridRenderContainer', ['$timeout', '$document', 'gridUtil', 'uiGridConstants', 'uiGridCellNavService', 'uiGridCellNavConstants', - function ($timeout, $document, gridUtil, uiGridConstants, uiGridCellNavService, uiGridCellNavConstants) { + module.directive('uiGridRenderContainer', ['$timeout', '$document', 'gridUtil', 'uiGridConstants', 'uiGridCellNavService', '$compile', + function ($timeout, $document, gridUtil, uiGridConstants, uiGridCellNavService, $compile) { return { replace: true, priority: -99999, //this needs to run very last @@ -945,102 +746,113 @@ var grid = uiGridCtrl.grid; + // focusser only created for body + if (containerId !== 'body') { + return; + } + // Needs to run last after all renderContainers are built uiGridCellNavService.decorateRenderContainers(grid); - // Let the render container be focus-able - $elm.attr("tabindex", -1); + //add an element with no dimensions that can be used to set focus and capture keystrokes + var focuser = $compile('
')($scope); + $elm.append(focuser); + + uiGridCtrl.focus = function () { + focuser[0].focus(); + }; // Bind to keydown events in the render container - $elm.on('keydown', function (evt) { + focuser.on('keydown', function (evt) { evt.uiGridTargetRenderContainerId = containerId; - return uiGridCtrl.cellNav.handleKeyDown(evt); + var rowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell(); + var result = uiGridCtrl.cellNav.handleKeyDown(evt); + if (result === null) { + uiGridCtrl.grid.api.cellNav.raise.viewPortKeyDown(evt, rowCol); + } }); - var needFocus = false; - - // When there's a scroll event we need to make sure to re-focus the right row, because the cell contents may have changed - grid.api.core.on.scrollEvent($scope, function (args) { - // Skip if not this grid that the event was broadcast for - if (args.grid && args.grid.id !== uiGridCtrl.grid.id) { + } + }; + } + }; + }]); + + module.directive('uiGridViewport', ['$timeout', '$document', 'gridUtil', 'uiGridConstants', 'uiGridCellNavService', 'uiGridCellNavConstants','$log','$compile', + function ($timeout, $document, gridUtil, uiGridConstants, uiGridCellNavService, uiGridCellNavConstants, $log, $compile) { + return { + replace: true, + priority: -99999, //this needs to run very last + require: ['^uiGrid', '^uiGridRenderContainer', '?^uiGridCellnav'], + scope: false, + compile: function () { + return { + pre: function ($scope, $elm, $attrs, uiGridCtrl) { + }, + post: function ($scope, $elm, $attrs, controllers) { + var uiGridCtrl = controllers[0], + renderContainerCtrl = controllers[1]; + + // Skip attaching cell-nav specific logic if the directive is not attached above us + if (!uiGridCtrl.grid.api.cellNav) { return; } + + var containerId = renderContainerCtrl.containerId; + //no need to process for other containers + if (containerId !== 'body') { + return; + } + + var grid = uiGridCtrl.grid; + + + + uiGridCtrl.focus(); + + + + grid.api.core.on.scrollBegin($scope, function (args) { + + // Skip if there's no currently-focused cell + var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell(); + if (lastRowCol == null) { + return; + } + + //if not in my container, move on + //todo: worry about horiz scroll + if (!renderContainerCtrl.colContainer.containsColumn(lastRowCol.col)) { return; } + //clear dom of focused cell + + var elements = $elm[0].getElementsByClassName('ui-grid-cell-focus'); + Array.prototype.forEach.call(elements,function(e){angular.element(e).removeClass('ui-grid-cell-focus');}); + + }); + + grid.api.core.on.scrollEnd($scope, function (args) { // Skip if there's no currently-focused cell - if (uiGridCtrl.grid.api.cellNav.getFocusedCell() == null) { + var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell(); + if (lastRowCol == null) { return; } - - /* - * If we have scrolled due to cellNav, we want to set the focus to the new cell after the - * virtualisation has run, and after scroll. If we scrolled through the browser scroll - * bar or other user action, we're going to discard the focus, because it will no longer - * be valid (and, noting #2423, trying to keep it causes problems) - * - * If cellNav triggers the scroll, we get a scrollToIfNecessary, then a viewport scroll. We - * want to wait for the viewport scroll to finish, then do a refocus. - * - * If someone manually scrolls we get just the viewport scroll, no scrollToIfNecessary. We - * want to just clear the focus - * - * Logic is: - * - if cellNav scroll, set a flag that will be resolved in the native scroll - * - if native scroll, look for the cellNav promise and resolve it - * - if not present, then use a timeout to clear focus - * - if it is present, then instead use a timeout to set focus - */ - - // We have to wrap in TWO timeouts so that we run AFTER the scroll event is resolved. - if ( args.source === 'uiGridCellNavService.scrollToIfNecessary'){ - needFocus = true; -/* - focusTimeout = $timeout(function () { - if ( clearFocusTimeout ){ - $timeout.cancel(clearFocusTimeout); - } - focusTimeout = $timeout(function () { - if ( clearFocusTimeout ){ - $timeout.cancel(clearFocusTimeout); - } - // Get the last row+col combo - var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell(); - - // If the body element becomes active, re-focus on the render container so we can capture cellNav events again. - // NOTE: this happens when we navigate LET from the left-most cell (RIGHT from the right-most) and have to re-render a new - // set of cells. The cell element we are navigating to doesn't exist and focus gets lost. This will re-capture it, imperfectly... - if ($document.activeElement === $document.body) { - $elm[0].focus(); - } - - // broadcast a cellNav event so we clear the focus on all cells - uiGridCtrl.cellNav.broadcastCellNav(lastRowCol); - }); - }); - */ - } else { - if ( needFocus ){ - $timeout(function () { - $timeout(function () { - // Get the last row+col combo - var lastRowCol = uiGridCtrl.grid.api.cellNav.getFocusedCell(); - - // If the body element becomes active, re-focus on the render container so we can capture cellNav events again. - // NOTE: this happens when we navigate LET from the left-most cell (RIGHT from the right-most) and have to re-render a new - // set of cells. The cell element we are navigating to doesn't exist and focus gets lost. This will re-capture it, imperfectly... - if ($document.activeElement === $document.body) { - $elm[0].focus(); - } - - // broadcast a cellNav event so we clear the focus on all cells - uiGridCtrl.cellNav.broadcastCellNav(lastRowCol); - - needFocus = false; - }); - }); - } + + //if not in my container, move on + //todo: worry about horiz scroll + if (!renderContainerCtrl.colContainer.containsColumn(lastRowCol.col)) { + return; } - }); - + + uiGridCtrl.cellNav.broadcastCellNav(lastRowCol); + + }); + + grid.api.cellNav.on.navigate($scope, function () { + //focus again because it can be lost + uiGridCtrl.focus(); + }); + } }; } @@ -1069,13 +881,12 @@ return; } - setTabEnabled(); - // When a cell is clicked, broadcast a cellNav event saying that this row+col combo is now focused $elm.find('div').on('click', function (evt) { uiGridCtrl.cellNav.broadcastCellNav(new RowCol($scope.row, $scope.col), evt.ctrlKey || evt.metaKey); evt.stopPropagation(); + $scope.$apply(); }); $elm.find('div').on('focus', function (evt) { @@ -1098,20 +909,16 @@ setFocused(); } - // This cellNav event came from a keydown event so we can safely refocus - if (rowCol.hasOwnProperty('eventType') && rowCol.eventType === uiGridCellNavConstants.EVENT_TYPE.KEYDOWN) { - $elm.find('div')[0].focus(); - } + // // This cellNav event came from a keydown event so we can safely refocus + // if (rowCol.hasOwnProperty('eventType') && rowCol.eventType === uiGridCellNavConstants.EVENT_TYPE.KEYDOWN) { + //// $elm.find('div')[0].focus(); + // } } else if (!(uiGridCtrl.grid.options.modifierKeysToMultiSelectCells && modifierDown)) { clearFocus(); } }); - function setTabEnabled() { - $elm.find('div').attr("tabindex", 0); - } - function setFocused() { var div = $elm.find('div'); div.addClass('ui-grid-cell-focus'); diff --git a/src/features/cellnav/less/cellNav.less b/src/features/cellnav/less/cellNav.less index c976ee5a69..ce4da36786 100644 --- a/src/features/cellnav/less/cellNav.less +++ b/src/features/cellnav/less/cellNav.less @@ -9,3 +9,8 @@ outline: 0; background-color: @focusedCell; } + +.ui-grid-focuser { + width:0px; + height:0px; +} diff --git a/src/features/cellnav/test/uiGridCellNavDirective.spec.js b/src/features/cellnav/test/uiGridCellNavDirective.spec.js index 96ce38058c..1ab0d4be25 100644 --- a/src/features/cellnav/test/uiGridCellNavDirective.spec.js +++ b/src/features/cellnav/test/uiGridCellNavDirective.spec.js @@ -33,7 +33,7 @@ describe('ui.grid.cellNav directive', function () { it('should not throw exceptions when scrolling when a grid does NOT have the ui-grid-cellNav directive', function () { expect(function () { - $scope.gridApi.core.raise.scrollEvent({}); + $scope.gridApi.core.raise.scrollBegin({}); }).not.toThrow(); }); diff --git a/src/features/cellnav/test/uiGridCellNavService.spec.js b/src/features/cellnav/test/uiGridCellNavService.spec.js index d8cf28772f..95bdd4f507 100644 --- a/src/features/cellnav/test/uiGridCellNavService.spec.js +++ b/src/features/cellnav/test/uiGridCellNavService.spec.js @@ -5,19 +5,23 @@ describe('ui.grid.edit uiGridCellNavService', function () { var uiGridConstants; var uiGridCellNavConstants; var $rootScope; + var $timeout; beforeEach(module('ui.grid.cellNav')); - beforeEach(inject(function (_uiGridCellNavService_, _gridClassFactory_, $templateCache, _uiGridConstants_, _uiGridCellNavConstants_, _$rootScope_) { + beforeEach(inject(function (_uiGridCellNavService_, _gridClassFactory_, $templateCache, _uiGridConstants_, _uiGridCellNavConstants_, _$rootScope_, _$timeout_) { uiGridCellNavService = _uiGridCellNavService_; gridClassFactory = _gridClassFactory_; uiGridConstants = _uiGridConstants_; uiGridCellNavConstants = _uiGridCellNavConstants_; $rootScope = _$rootScope_; + $timeout = _$timeout_; $templateCache.put('ui-grid/uiGridCell', '
'); grid = gridClassFactory.createGrid(); + //throttled scrolling isn't working in tests for some reason + grid.options.scrollDebounce = 0; grid.options.columnDefs = [ {name: 'col0', allowCellFocus: true}, {name: 'col1', allowCellFocus: false}, @@ -192,7 +196,9 @@ describe('ui.grid.edit uiGridCellNavService', function () { grid.setVisibleColumns(grid.columns); grid.setVisibleRows(grid.rows); - + + grid.renderContainers.body.headerHeight = 0; + for ( i = 0; i < 11; i++ ){ grid.columns[i].drawnWidth = i < 6 ? 100 : 200; } @@ -200,7 +206,7 @@ describe('ui.grid.edit uiGridCellNavService', function () { $scope = $rootScope.$new(); args = null; - grid.api.core.on.scrollEvent($scope, function( receivedArgs ){ + grid.api.core.on.scrollEnd($scope, function( receivedArgs ){ args = receivedArgs; }); @@ -211,51 +217,79 @@ describe('ui.grid.edit uiGridCellNavService', function () { // but it means these unit tests are now mostly checking that it is the same it used to // be, not that it is giving some specified result (i.e. I just updated them to what they were) it('should request scroll to row and column', function () { - uiGridCellNavService.scrollTo( grid, grid.options.data[4], grid.columns[4].colDef); - + $timeout(function () { + grid.scrollTo(grid.options.data[4], grid.columns[4].colDef); + }); + $timeout.flush(); + expect(args.grid).toEqual(grid); - expect(args.y).toEqual( { percentage : 5/11 }); + expect(Math.round(args.y.percentage * 10)/10).toBe(0.4); expect(isNaN(args.x.percentage)).toEqual( true ); }); it('should request scroll to row only - first row', function () { - uiGridCellNavService.scrollTo( grid, grid.options.data[0], null); + $timeout(function () { + grid.scrollTo( grid.options.data[0], null); + }); + $timeout.flush(); - expect(args.y).toEqual( { percentage : 2/11 }); + expect(Math.round(args.y.percentage * 10)/10).toBe(0.1); }); it('should request scroll to row only - last row', function () { - uiGridCellNavService.scrollTo( grid, grid.options.data[10], null); + $timeout(function () { + grid.scrollTo( grid.options.data[10], null); + }); + $timeout.flush(); - expect(args.y).toEqual( { percentage : 1 }); + expect(args.y.percentage).toBeGreaterThan(0.5); + expect(args.x).toBe(null); }); it('should request scroll to row only - row 4', function () { - uiGridCellNavService.scrollTo( grid, grid.options.data[5], null); + $timeout(function () { + grid.scrollTo( grid.options.data[5], null); + }); + $timeout.flush(); - expect(args.y).toEqual( { percentage : 6/11 }); + expect(Math.round(args.y.percentage * 10)/10).toEqual( 0.5); + expect(args.x).toBe(null); }); it('should request scroll to column only - first column', function () { - uiGridCellNavService.scrollTo( grid, null, grid.columns[0].colDef); - + $timeout(function () { + grid.scrollTo( null, grid.columns[0].colDef); + }); + $timeout.flush(); + + expect(isNaN(args.x.percentage)).toEqual( true ); }); it('should request scroll to column only - last column', function () { - uiGridCellNavService.scrollTo( grid, null, grid.columns[10].colDef); - + $timeout(function () { + grid.scrollTo( null, grid.columns[10].colDef); + }); + $timeout.flush(); + + expect(isNaN(args.x.percentage)).toEqual( true ); }); it('should request scroll to column only - column 7', function () { - uiGridCellNavService.scrollTo( grid, null, grid.columns[8].colDef); + $timeout(function () { + grid.scrollTo( null, grid.columns[8].colDef); + }); + $timeout.flush(); expect(isNaN(args.x.percentage)).toEqual( true ); }); it('should request no scroll as no row or column', function () { - uiGridCellNavService.scrollTo( grid, null, null ); + $timeout(function () { + grid.scrollTo( null, null ); + }); + $timeout.flush(); expect(args).toEqual( null ); }); diff --git a/src/features/edit/js/gridEdit.js b/src/features/edit/js/gridEdit.js index 3a9e787bd6..28d8c88088 100644 --- a/src/features/edit/js/gridEdit.js +++ b/src/features/edit/js/gridEdit.js @@ -42,8 +42,8 @@ * * @description Services for editing features */ - module.service('uiGridEditService', ['$q', '$templateCache', 'uiGridConstants', 'gridUtil', - function ($q, $templateCache, uiGridConstants, gridUtil) { + module.service('uiGridEditService', ['$q', 'uiGridConstants', 'gridUtil', + function ($q, uiGridConstants, gridUtil) { var service = { @@ -52,6 +52,7 @@ service.defaultGridOptions(grid.options); grid.registerColumnBuilder(service.editColumnBuilder); + grid.edit = {}; /** * @ngdoc object @@ -243,6 +244,20 @@ //enableCellEditOnFocus can only be used if cellnav module is used colDef.enableCellEditOnFocus = colDef.enableCellEditOnFocus === undefined ? gridOptions.enableCellEditOnFocus : colDef.enableCellEditOnFocus; + + /** + * @ngdoc string + * @name editModelField + * @propertyOf ui.grid.edit.api:ColumnDef + * @description a bindable string value that is used when binding to edit controls instead of colDef.field + *
example: You have a complex property on and object like state:{abbrev:'MS',name:'Mississippi'}. The + * grid should display state.name in the cell and sort/filter based on the state.name property but the editor + * requires the full state object. + *
colDef.field = 'state.name' + *
colDef.editModelField = 'state' + */ + //colDef.editModelField + return $q.all(promises); }, @@ -330,6 +345,50 @@ }; }]); + /** + * @ngdoc directive + * @name ui.grid.edit.directive:uiGridRenderContainer + * @element div + * @restrict A + * + * @description Adds keydown listeners to renderContainer element so we can capture when to begin edits + * + */ + module.directive('uiGridViewport', [ 'uiGridEditConstants', + function ( uiGridEditConstants) { + return { + replace: true, + priority: -99998, //run before cellNav + require: ['^uiGrid', '^uiGridRenderContainer'], + scope: false, + compile: function () { + return { + post: function ($scope, $elm, $attrs, controllers) { + var uiGridCtrl = controllers[0]; + + // Skip attaching if edit and cellNav is not enabled + if (!uiGridCtrl.grid.api.edit || !uiGridCtrl.grid.api.cellNav) { return; } + + var containerId = controllers[1].containerId; + //no need to process for other containers + if (containerId !== 'body') { + return; + } + + //refocus on the grid + $scope.$on(uiGridEditConstants.events.CANCEL_CELL_EDIT, function () { + uiGridCtrl.focus(); + }); + $scope.$on(uiGridEditConstants.events.END_CELL_EDIT, function () { + uiGridCtrl.focus(); + }); + + } + }; + } + }; + }]); + /** * @ngdoc directive * @name ui.grid.edit.directive:uiGridCell @@ -388,6 +447,9 @@ ['$compile', '$injector', '$timeout', 'uiGridConstants', 'uiGridEditConstants', 'gridUtil', '$parse', 'uiGridEditService', '$rootScope', function ($compile, $injector, $timeout, uiGridConstants, uiGridEditConstants, gridUtil, $parse, uiGridEditService, $rootScope) { var touchstartTimeout = 500; + if ($injector.has('uiGridCellNavService')) { + var uiGridCellNavService = $injector.get('uiGridCellNavService'); + } return { priority: -100, // run after default uiGridCell directive @@ -402,23 +464,52 @@ var html; var origCellValue; var inEdit = false; - var isFocusedBeforeEdit = false; var cellModel; var cancelTouchstartTimeout; var editCellScope; + var cellNavNavigateDereg = function() {}; + + // Bind to keydown events in the render container + if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) { + + uiGridCtrl.grid.api.cellNav.on.viewPortKeyDown($scope, function (evt, rowCol) { + if (rowCol === null) { + return; + } + + if (rowCol.row === $scope.row && rowCol.col === $scope.col && !$scope.col.colDef.enableCellEditOnFocus) { + //important to do this before scrollToIfNecessary + beginEditKeyDown(evt); + uiGridCtrl.grid.api.core.scrollToIfNecessary(rowCol.row, rowCol.col); + } + + }); + } + registerBeginEditEvents(); function registerBeginEditEvents() { $elm.on('dblclick', beginEdit); - $elm.on('keydown', beginEditKeyDown); - if ($scope.col.colDef.enableCellEditOnFocus) { - $elm.find('div').on('focus', beginEditFocus); - } // Add touchstart handling. If the users starts a touch and it doesn't end after X milliseconds, then start the edit $elm.on('touchstart', touchStart); + + if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) { + cellNavNavigateDereg = uiGridCtrl.grid.api.cellNav.on.navigate($scope, function (newRowCol, oldRowCol) { + if ($scope.col.colDef.enableCellEditOnFocus) { + if (newRowCol.row === $scope.row && newRowCol.col === $scope.col) { + $timeout(function () { + beginEdit(); + }); + } + } + }); + } + + + } function touchStart(event) { @@ -452,42 +543,10 @@ function cancelBeginEditEvents() { $elm.off('dblclick', beginEdit); $elm.off('keydown', beginEditKeyDown); - if ($scope.col.colDef.enableCellEditOnFocus) { - $elm.find('div').off('focus', beginEditFocus); - } $elm.off('touchstart', touchStart); + cellNavNavigateDereg(); } - function beginEditFocus(evt) { - // gridUtil.logDebug('begin edit'); - if (uiGridCtrl && uiGridCtrl.cellNav) { - // NOTE(c0bra): This is causing a loop where focusCell causes beginEditFocus to be called.... - uiGridCtrl.cellNav.focusCell($scope.row, $scope.col); - } - - evt.stopPropagation(); - beginEdit(); - } - - // If the cellNagv module is installed and we can get the uiGridCellNavConstants value injected, - // then if the column has enableCellEditOnFocus set to true, we need to listen for cellNav events - // to this cell and start editing when the "focus" reaches us - try { - var uiGridCellNavConstants = $injector.get('uiGridCellNavConstants'); - - if ($scope.col.colDef.enableCellEditOnFocus) { - $scope.$on(uiGridCellNavConstants.CELL_NAV_EVENT, function (evt, rowCol) { - if (rowCol.row === $scope.row && rowCol.col === $scope.col) { - beginEdit(); - } - else { - endEdit(); - } - }); - } - } - catch (e) {} - function beginEditKeyDown(evt) { if (uiGridEditService.isStartEditKey(evt)) { beginEdit(); @@ -596,17 +655,18 @@ return; } - // if the cell isn't fully visible, and cellNav is present, scroll it to be fully visible before we start - if ( $scope.grid.api.cellNav ){ - $scope.grid.api.cellNav.scrollToIfNecessary( $scope.row, $scope.col ); - } - + cellModel = $parse($scope.row.getQualifiedColField($scope.col)); //get original value from the cell origCellValue = cellModel($scope); html = $scope.col.editableCellTemplate; - html = html.replace(uiGridConstants.MODEL_COL_FIELD, $scope.row.getQualifiedColField($scope.col)); + if ($scope.col.colDef.editModelField) { + html = html.replace(uiGridConstants.MODEL_COL_FIELD, gridUtil.preEval('row.entity.' + $scope.col.colDef.editModelField)); + } + else { + html = html.replace(uiGridConstants.MODEL_COL_FIELD, $scope.row.getQualifiedColField($scope.col)); + } var optionFilter = $scope.col.colDef.editDropdownFilter ? '|' + $scope.col.colDef.editDropdownFilter : ''; html = html.replace(uiGridConstants.CUSTOM_FILTERS, optionFilter); @@ -644,7 +704,6 @@ editCellScope = $scope.$new(); $compile(cellElement)(editCellScope); var gridCellContentsEl = angular.element($elm.children()[0]); - isFocusedBeforeEdit = gridCellContentsEl.hasClass('ui-grid-cell-focus'); gridCellContentsEl.addClass('ui-grid-cell-contents-hidden'); }; if (!$rootScope.$$phase) { @@ -654,7 +713,7 @@ } //stop editing when grid is scrolled - var deregOnGridScroll = $scope.col.grid.api.core.on.scrollEvent($scope, function () { + var deregOnGridScroll = $scope.col.grid.api.core.on.scrollBegin($scope, function () { endEdit(true); $scope.grid.api.edit.raise.afterCellEdit($scope.row.entity, $scope.col.colDef, cellModel($scope), origCellValue); deregOnGridScroll(); @@ -692,10 +751,6 @@ editCellScope.$destroy(); angular.element($elm.children()[1]).remove(); gridCellContentsEl.removeClass('ui-grid-cell-contents-hidden'); - if (retainFocus && isFocusedBeforeEdit) { - gridCellContentsEl[0].focus(); - } - isFocusedBeforeEdit = false; inEdit = false; registerBeginEditEvents(); $scope.grid.api.core.notifyDataChange( uiGridConstants.dataChange.EDIT ); @@ -751,8 +806,8 @@ * */ module.directive('uiGridEditor', - ['gridUtil', 'uiGridConstants', 'uiGridEditConstants', - function (gridUtil, uiGridConstants, uiGridEditConstants) { + ['gridUtil', 'uiGridConstants', 'uiGridEditConstants','$timeout', + function (gridUtil, uiGridConstants, uiGridEditConstants, $timeout) { return { scope: true, require: ['?^uiGrid', '?^uiGridRenderContainer'], @@ -770,15 +825,16 @@ $scope.$on(uiGridEditConstants.events.BEGIN_CELL_EDIT, function () { $elm[0].focus(); $elm[0].select(); + $elm.on('blur', function (evt) { $scope.stopEdit(evt); }); }); - $scope.deepEdit = false; + $scope.deepEdit = false; - $scope.stopEdit = function (evt) { + $scope.stopEdit = function (evt) { if ($scope.inputForm && !$scope.inputForm.$valid) { evt.stopPropagation(); $scope.$emit(uiGridEditConstants.events.CANCEL_CELL_EDIT); @@ -824,9 +880,11 @@ } } // Pass the keydown event off to the cellNav service, if it exists - else if (uiGridCtrl && uiGridCtrl.hasOwnProperty('cellNav') && renderContainerCtrl) { + else if (uiGridCtrl && uiGridCtrl.grid.api.cellNav) { evt.uiGridTargetRenderContainerId = renderContainerCtrl.containerId; - uiGridCtrl.cellNav.handleKeyDown(evt); + if (uiGridCtrl.cellNav.handleKeyDown(evt) !== null) { + $scope.stopEdit(evt); + } } return true; @@ -975,4 +1033,107 @@ }; }]); + /** + * @ngdoc directive + * @name ui.grid.edit.directive:uiGridEditor + * @element div + * @restrict A + * + * @description input editor directive for editable fields. + * Provides EndEdit and CancelEdit events + * + * Events that end editing: + * blur and enter keydown + * + * Events that cancel editing: + * - Esc keydown + * + */ + module.directive('uiGridEditFileChooser', + ['gridUtil', 'uiGridConstants', 'uiGridEditConstants','$timeout', + function (gridUtil, uiGridConstants, uiGridEditConstants, $timeout) { + return { + scope: true, + require: ['?^uiGrid', '?^uiGridRenderContainer'], + compile: function () { + return { + pre: function ($scope, $elm, $attrs) { + + }, + post: function ($scope, $elm, $attrs, controllers) { + var uiGridCtrl, renderContainerCtrl; + if (controllers[0]) { uiGridCtrl = controllers[0]; } + if (controllers[1]) { renderContainerCtrl = controllers[1]; } + var grid = uiGridCtrl.grid; + + var handleFileSelect = function( event ){ + var target = event.srcElement || event.target; + + if (target && target.files && target.files.length > 0) { + /** + * @ngdoc property + * @name editFileChooserCallback + * @propertyOf ui.grid.edit.api:ColumnDef + * @description A function that should be called when any files have been chosen + * by the user. You should use this to process the files appropriately for your + * application. + * + * It passes the gridCol, the gridRow (from which you can get gridRow.entity), + * and the files. The files are in the format as returned from the file chooser, + * an array of files, with each having useful information such as: + * - `files[0].lastModifiedDate` + * - `files[0].name` + * - `files[0].size` (appears to be in bytes) + * - `files[0].type` (MIME type by the looks) + * + * Typically you would do something with these files - most commonly you would + * use the filename or read the file itself in. The example function does both. + * + * @example + *
+                     *  editFileChooserCallBack: function(gridRow, gridCol, files ){
+                     *    // ignore all but the first file, it can only choose one anyway
+                     *    // set the filename into this column
+                     *    gridRow.entity.filename = file[0].name;
+                     *
+                     *    // read the file and set it into a hidden column, which we may do stuff with later
+                     *    var setFile = function(fileContent){
+                     *      gridRow.entity.file = fileContent.currentTarget.result;
+                     *    };
+                     *    var reader = new FileReader();
+                     *    reader.onload = setFile;
+                     *    reader.readAsText( files[0] );
+                     *  }
+                     *  
+ */ + if ( typeof($scope.col.colDef.editFileChooserCallback) === 'function' ) { + $scope.col.colDef.editFileChooserCallback($scope.row, $scope.col, target.files); + } else { + gridUtil.logError('You need to set colDef.editFileChooserCallback to use the file chooser'); + } + + target.form.reset(); + $scope.$emit(uiGridEditConstants.events.END_CELL_EDIT); + } else { + $scope.$emit(uiGridEditConstants.events.CANCEL_CELL_EDIT); + } + }; + + $elm[0].addEventListener('change', handleFileSelect, false); // TODO: why the false on the end? Google + + $scope.$on(uiGridEditConstants.events.BEGIN_CELL_EDIT, function () { + $elm[0].focus(); + $elm[0].select(); + + $elm.on('blur', function (evt) { + $scope.$emit(uiGridEditConstants.events.END_CELL_EDIT); + }); + }); + } + }; + } + }; + }]); + + })(); diff --git a/src/features/edit/templates/fileChooserEditor.html b/src/features/edit/templates/fileChooserEditor.html new file mode 100644 index 0000000000..c719529fbb --- /dev/null +++ b/src/features/edit/templates/fileChooserEditor.html @@ -0,0 +1,5 @@ +
+
+ +
+
diff --git a/src/features/edit/test/uiGridCell.spec.js b/src/features/edit/test/uiGridCell.spec.js index 8262392ea5..3cb4a80029 100644 --- a/src/features/edit/test/uiGridCell.spec.js +++ b/src/features/edit/test/uiGridCell.spec.js @@ -23,7 +23,7 @@ describe('ui.grid.edit GridCellDirective', function () { {name: 'col1', enableCellEdit: true} ]; grid.options.data = [ - {col1: 'val'} + {col1: 'val', col2:'col2val'} ]; uiGridEditService.initializeGrid(grid); grid.buildColumns(); @@ -119,7 +119,7 @@ describe('ui.grid.edit GridCellDirective', function () { it('should stop when grid scrolls', function () { //stop edit - scope.grid.api.core.raise.scrollEvent(); + scope.grid.api.core.raise.scrollBegin(); scope.$digest(); //back to beginning expect(element.html()).toBe(displayHtml); @@ -149,4 +149,21 @@ describe('ui.grid.edit GridCellDirective', function () { }); -}); \ No newline at end of file + + describe('ui.grid.edit should override bound value when using editModelField', function () { + var displayHtml; + beforeEach(function () { + element = angular.element('
'); + //bind the edit to another column. This could be any property on the entity + scope.grid.options.columnDefs[0].editModelField = 'col2'; + recompile(); + + displayHtml = element.html(); + expect(element.text()).toBe('val'); + //invoke edit + element.dblclick(); + expect(element.find('input')).toBeDefined(); + expect(element.find('input').val()).toBe('col2val'); + }); + }); +}); diff --git a/src/features/edit/test/uiGridCellWithDropdown.spec.js b/src/features/edit/test/uiGridCellWithDropdown.spec.js index 77a9505f34..06a3da0ae8 100644 --- a/src/features/edit/test/uiGridCellWithDropdown.spec.js +++ b/src/features/edit/test/uiGridCellWithDropdown.spec.js @@ -15,7 +15,7 @@ describe('ui.grid.edit GridCellDirective - with dropdown', function () { $timeout = _$timeout_; $templateCache.put('ui-grid/uiGridCell', '
{{COL_FIELD CUSTOM_FILTERS}}
'); - $templateCache.put('ui-grid/cellEditor', '
'); + $templateCache.put('ui-grid/dropdownEditor', '
'); scope = $rootScope.$new(); var grid = gridClassFactory.createGrid(); @@ -141,7 +141,7 @@ describe('ui.grid.edit GridCellDirective - with dropdown', function () { it('should stop when grid scrolls', function () { //stop edit - scope.grid.api.core.raise.scrollEvent(scope); + scope.grid.api.core.raise.scrollBegin(scope); scope.$digest(); //back to beginning expect(element.html()).toBe(displayHtml); diff --git a/src/features/edit/test/uiGridEditDirective.spec.js b/src/features/edit/test/uiGridEditDirective.spec.js index 040fcd0287..812bbeae12 100644 --- a/src/features/edit/test/uiGridEditDirective.spec.js +++ b/src/features/edit/test/uiGridEditDirective.spec.js @@ -54,6 +54,7 @@ describe('uiGridEditDirective', function () { expect(col).not.toBeNull(); expect(col.colDef.enableCellEdit).toBe(false); expect(col.colDef.editableCellTemplate).not.toBeDefined(); + expect(col.colDef.editModelField).not.toBeDefined(); }); @@ -76,4 +77,4 @@ describe('uiGridEditDirective', function () { }); }); -}); \ No newline at end of file +}); diff --git a/src/features/expandable/js/expandable.js b/src/features/expandable/js/expandable.js index c80f53d053..3ce2cf66d3 100644 --- a/src/features/expandable/js/expandable.js +++ b/src/features/expandable/js/expandable.js @@ -196,7 +196,7 @@ }, expandAllRows: function(grid, $scope) { - angular.forEach(grid.renderContainers.body.visibleRowCache, function(row) { + grid.renderContainers.body.visibleRowCache.forEach( function(row) { if (!row.isExpanded) { service.toggleRowExpansion(grid, row); } @@ -206,7 +206,7 @@ }, collapseAllRows: function(grid) { - angular.forEach(grid.renderContainers.body.visibleRowCache, function(row) { + grid.renderContainers.body.visibleRowCache.forEach( function(row) { if (row.isExpanded) { service.toggleRowExpansion(grid, row); } @@ -281,7 +281,7 @@ function (uiGridExpandableService, $templateCache) { return { replace: true, - priority: 1000, + priority: 599, require: '^uiGrid', scope: false, compile: function () { @@ -392,10 +392,14 @@ return ret; }; + /* + * Commented out @PaulL1. This has no purpose that I can see, and causes #2964. If this code needs to be reinstated for some + * reason it needs to use drawnWidth, not width, and needs to check column visibility. It should really use render container + * visible column cache also instead of checking column.renderContainer. function updateRowContainerWidth() { var grid = $scope.grid; var colWidth = 0; - angular.forEach(grid.columns, function (column) { + grid.columns.forEach( function (column) { if (column.renderContainer === 'left') { colWidth += column.width; } @@ -411,7 +415,7 @@ priority: 15, func: updateRowContainerWidth }); - } + }*/ }, post: function ($scope, $elm, $attrs, controllers) { diff --git a/src/features/expandable/less/expandable.less b/src/features/expandable/less/expandable.less index 90caee4d1c..389e85b66f 100644 --- a/src/features/expandable/less/expandable.less +++ b/src/features/expandable/less/expandable.less @@ -16,4 +16,9 @@ } +.scrollFiller { + float:left; + border:1px solid @borderColor; +} + .ui-grid-expandable-buttons-cell { } diff --git a/src/features/expandable/templates/expandableScrollFiller.html b/src/features/expandable/templates/expandableScrollFiller.html index 95538cee12..e9cf544c7b 100644 --- a/src/features/expandable/templates/expandableScrollFiller.html +++ b/src/features/expandable/templates/expandableScrollFiller.html @@ -1,7 +1,7 @@
*
* @@ -89,7 +85,8 @@ SUM: 'sum', MAX: 'max', MIN: 'min', - AVG: 'avg' + AVG: 'avg', + FIELD: '##@@aggregation_running_count@@##' } }); @@ -172,7 +169,9 @@ service.defaultGridOptions(grid.options); - grid.registerRowsProcessor(service.groupRows); + grid.registerRowsProcessor(service.groupRows, 400); + + grid.registerColumnsProcessor(service.groupingColumnProcessor, 400); /** * @ngdoc object @@ -245,11 +244,141 @@ * @name collapseRow * @methodOf ui.grid.grouping.api:PublicApi * @description collapse all children of the specified row. When - * you expand the row again, all grandchildren will be collapsed - * @param {gridRow} row the row you wish to expand + * you expand the row again, all grandchildren will retain their state + * @param {gridRow} row the row you wish to collapse */ collapseRow: function ( row ) { service.collapseRow(grid, row); + }, + + /** + * @ngdoc function + * @name collapseRowChildren + * @methodOf ui.grid.grouping.api:PublicApi + * @description collapse all children of the specified row. When + * you expand the row again, all grandchildren will be collapsed + * @param {gridRow} row the row you wish to collapse + */ + collapseRowChildren: function ( row ) { + service.collapseRowChildren(grid, row); + }, + + /** + * @ngdoc function + * @name getGrouping + * @methodOf ui.grid.grouping.api:PublicApi + * @description Get the grouping configuration for this grid, + * used by the saveState feature + * Returned grouping is an object + * `{ grouping: groupArray, aggregations: aggregateArray, expandedState: hash }` + * where grouping contains an array of objects: + * `{ field: column.field, colName: column.name, groupPriority: column.grouping.groupPriority }` + * and aggregations contains an array of objects: + * `{ field: column.field, colName: column.name, aggregation: column.grouping.aggregation }` + * and expandedState is a hash of the currently expanded nodes + * + * The groupArray will be sorted by groupPriority. + * + * @param {boolean} getExpanded whether or not to return the expanded state + * @returns {object} grouping configuration + */ + getGrouping: function ( getExpanded ) { + var grouping = service.getGrouping(grid); + + grouping.grouping.forEach( function( group ) { + group.colName = group.col.name; + delete group.col; + }); + + grouping.aggregations.forEach( function( aggregation ) { + aggregation.colName = aggregation.col.name; + delete aggregation.col; + }); + + if ( getExpanded ){ + grouping.rowExpandedStates = grid.grouping.rowExpandedStates; + } + + return grouping; + }, + + /** + * @ngdoc function + * @name setGrouping + * @methodOf ui.grid.grouping.api:PublicApi + * @description Set the grouping configuration for this grid, + * used by the saveState feature, but can also be used by any + * user to specify a combined grouping and aggregation configuration + * @param {object} config the config you want to apply, in the format + * provided out by getGrouping + */ + setGrouping: function ( config ) { + service.setGrouping(grid, config); + }, + + /** + * @ngdoc function + * @name groupColumn + * @methodOf ui.grid.grouping.api:PublicApi + * @description Adds this column to the existing grouping, at the end of the priority order. + * If the column doesn't have a sort, adds one, by default ASC + * + * This column will move to the left of any non-group columns, the + * move is handled in a columnProcessor, so gets called as part of refresh + * + * @param {string} columnName the name of the column we want to group + */ + groupColumn: function( columnName ) { + var column = grid.getColumn(columnName); + service.groupColumn(grid, column); + }, + + /** + * @ngdoc function + * @name ungroupColumn + * @methodOf ui.grid.grouping.api:PublicApi + * @description Removes the groupPriority from this column. If the + * column was previously aggregated the aggregation will come back. + * The sort will remain. + * + * This column will move to the right of any other group columns, the + * move is handled in a columnProcessor, so gets called as part of refresh + * + * @param {string} columnName the name of the column we want to ungroup + */ + ungroupColumn: function( columnName ) { + var column = grid.getColumn(columnName); + service.ungroupColumn(grid, column); + }, + + /** + * @ngdoc function + * @name clearGrouping + * @methodOf ui.grid.grouping.api:PublicApi + * @description Clear any grouped columns and any aggregations. Doesn't remove sorting, + * as we don't know whether that sorting was added by grouping or was there beforehand + * + */ + clearGrouping: function() { + service.clearGrouping(grid); + }, + + /** + * @ngdoc function + * @name aggregateColumn + * @methodOf ui.grid.grouping.api:PublicApi + * @description Sets the aggregation type on a column, if the + * column is currently grouped then it removes the grouping first. + * If the aggregationType is null then will result in the aggregation + * being removed + * + * @param {string} columnName the column we want to aggregate + * @param {string} aggregationType one of the recognised types + * from uiGridGroupingConstants + */ + aggregateColumn: function( columnName, aggregationType){ + var column = grid.getColumn(columnName); + service.aggregateColumn( grid, column, aggregationType ); } } } @@ -284,23 +413,42 @@ /** * @ngdoc object - * @name groupingRowHeaderWidth + * @name groupingRowHeaderBaseWidth * @propertyOf ui.grid.grouping.api:GridOptions - * @description Width of the grouping header, if your nested grouping is too - * deep you may need to increase this - *
Defaults to 40 + * @description Base width of the grouping header, provides for a single level of grouping. This + * is incremented by `groupingIndent` for each extra level + *
Defaults to 30 */ - gridOptions.groupingRowHeaderWidth = gridOptions.groupingRowHeaderWidth || 40; + gridOptions.groupingRowHeaderBaseWidth = gridOptions.groupingRowHeaderBaseWidth || 30; /** * @ngdoc object * @name groupingIndent * @propertyOf ui.grid.grouping.api:GridOptions * @description Number of pixels of indent for the icon at each grouping level, wider indents are visually more pleasing, - * but may result in you having to make the group row header wider + * but will make the group row header wider *
Defaults to 10 */ gridOptions.groupingIndent = gridOptions.groupingIndent || 10; + + /** + * @ngdoc object + * @name groupingRowHeaderAlwaysVisible + * @propertyOf ui.grid.grouping.api:GridOptions + * @description forces the groupRowHeader to always be present, even if nothing is grouped. In some situations this + * may be preferable to having the groupHeader come and go + *
Defaults to false + */ + gridOptions.groupingRowHeaderAlwaysVisible = gridOptions.groupingRowHeaderAlwaysVisible === true; + + /** + * @ngdoc object + * @name groupingShowCounts + * @propertyOf ui.grid.grouping.api:GridOptions + * @description shows counts on the groupHeader rows + *
Defaults to true + */ + gridOptions.groupingShowCounts = gridOptions.groupingShowCounts !== false; }, @@ -334,7 +482,7 @@ if (colDef.enableGrouping === false){ return; } - + /** * @ngdoc object * @name grouping @@ -459,6 +607,106 @@ }, + + /** + * @ngdoc function + * @name groupingColumnProcessor + * @methodOf ui.grid.grouping.service:uiGridGroupingService + * @description Updates the visibility of the groupingRowHeader based on whether or not + * there are any grouped columns + * + * @param {array} columns the columns to consider rendering + * @param {array} rows the grid rows, which we don't use but are passed to us + * @returns {array} updated columns array + */ + groupingColumnProcessor: function( columns, rows ) { + var grid = this; + columns.forEach( function(column, index){ + // position used to make stable sort in moveGroupColumns + column.groupingPosition = index; + + // find groupingRowHeader + if (column.name === uiGridGroupingConstants.groupingRowHeaderColName) { + var groupingConfig = service.getGrouping(column.grid); + // decide whether to make it visible + if (typeof(grid.options.groupingRowHeaderAlwaysVisible) === 'undefined' || grid.options.groupingRowHeaderAlwaysVisible === false) { + if (groupingConfig.grouping.length > 0){ + column.visible = true; + } else { + column.visible = false; + } + } + // set the width based on the depth of grouping + var indent = ( groupingConfig.grouping.length - 1 ) * grid.options.groupingIndent; + indent = indent > 0 ? indent : 0; + column.width = grid.options.groupingRowHeaderBaseWidth + indent; + } + + }); + + columns = service.moveGroupColumns(this, columns, rows); + return columns; + }, + + + /** + * @ngdoc function + * @name moveGroupColumns + * @methodOf ui.grid.grouping.service:uiGridGroupingService + * @description Moves the column order so that the grouped columns are lined up + * to the left (well, unless you're RTL, then it's the right). By doing this in + * the columnsProcessor, we make it transient - when the column is ungrouped it'll + * go back to where it was. + * + * Does nothing if the option `moveGroupColumns` is set to false. + * + * @param {Grid} grid grid object + * @param {array} columns the columns that we should process/move + * @param {array} rows the grid rows + * @returns {array} updated columns + */ + moveGroupColumns: function( grid, columns, rows ){ + if ( grid.options.moveGroupColumns === false){ + return; + } + + // optimisation - this can be done in the groupingColumnProcessor since we were already + // iterating. But commented out and left here to make the code a little more understandable + // + // columns.forEach( function(column, index) { + // column.groupingPosition = index; + // }); + + columns.sort(function(a, b){ + var a_group, b_group; + if ( typeof(a.grouping) === 'undefined' || typeof(a.grouping.groupPriority) === 'undefined' || a.grouping.groupPriority < 0){ + a_group = null; + } else { + a_group = a.grouping.groupPriority; + } + + if ( typeof(b.grouping) === 'undefined' || typeof(b.grouping.groupPriority) === 'undefined' || b.grouping.groupPriority < 0){ + b_group = null; + } else { + b_group = b.grouping.groupPriority; + } + + // groups get sorted to the top + if ( a_group !== null && b_group === null) { return -1; } + if ( b_group !== null && a_group === null) { return 1; } + if ( a_group !== null && b_group !== null) {return a_group - b_group; } + + return a.groupingPosition - b.groupingPosition; + }); + + columns.forEach( function(column, index) { + delete column.groupingPosition; + }); + + return columns; + }, + + /** * @ngdoc function * @name groupColumn @@ -466,8 +714,8 @@ * @description Adds this column to the existing grouping, at the end of the priority order. * If the column doesn't have a sort, adds one, by default ASC * - * If the option `groupMoveColumns` hasn't been set to false, moves the column to the left - * to make things look tidier. + * This column will move to the left of any non-group columns, the + * move is handled in a columnProcessor, so gets called as part of refresh * * @param {Grid} grid grid object * @param {GridCol} column the column we want to group @@ -489,13 +737,12 @@ } service.tidyPriorities( grid ); - service.moveGroupColumns( grid ); grid.queueGridRefresh(); }, - /** + /** * @ngdoc function * @name ungroupColumn * @methodOf ui.grid.grouping.service:uiGridGroupingService @@ -503,7 +750,8 @@ * column was previously aggregated the aggregation will come back. * The sort will remain. * - * This column will move to the right of any other group columns. + * This column will move to the right of any other group columns, the + * move is handled in a columnProcessor, so gets called as part of refresh * * @param {Grid} grid grid object * @param {GridCol} column the column we want to ungroup @@ -516,7 +764,6 @@ delete column.grouping.groupPriority; service.tidyPriorities( grid ); - service.moveGroupColumns( grid ); grid.queueGridRefresh(); }, @@ -548,6 +795,85 @@ grid.queueGridRefresh(); }, + + /** + * @ngdoc function + * @name setGrouping + * @methodOf ui.grid.grouping.service:uiGridGroupingService + * @description Set the grouping based on a config object, used by the save state feature + * (more specifically, by the restore function in that feature ) + * + * @param {Grid} grid grid object + * @param {object} config the config we want to set, same format as that returned by getGrouping + */ + setGrouping: function ( grid, config ){ + if ( typeof(config) === 'undefined' ){ + return; + } + + // first remove any existing grouping + service.clearGrouping(grid); + + if ( config.grouping && config.grouping.length && config.grouping.length > 0 ){ + config.grouping.forEach( function( group ) { + var col = grid.getColumn(group.colName); + + if ( col ) { + service.groupColumn( grid, col ); + } + }); + } + + if ( config.aggregations && config.aggregations.length && config.aggregations.length > 0 ){ + config.aggregations.forEach( function( aggregation ) { + var col = grid.getColumn(aggregation.colName); + + if ( col ) { + service.aggregateColumn( grid, col, aggregation.aggregation ); + } + }); + } + + if ( config.rowExpandedStates ){ + grid.grouping.rowExpandedStates = config.rowExpandedStates; + } + }, + + + /** + * @ngdoc function + * @name clearGrouping + * @methodOf ui.grid.grouping.service:uiGridGroupingService + * @description Clear any grouped columns and any aggregations. Doesn't remove sorting, + * as we don't know whether that sorting was added by grouping or was there beforehand + * + * @param {Grid} grid grid object + */ + clearGrouping: function( grid ) { + var currentGrouping = service.getGrouping(grid); + + if ( currentGrouping.grouping.length > 0 ){ + currentGrouping.grouping.forEach( function( group ) { + if (!group.col){ + // should have a group.colName if there's no col + group.col = grid.getColumn(group.colName); + } + service.ungroupColumn(grid, group.col); + }); + } + + if ( currentGrouping.aggregations.length > 0 ){ + currentGrouping.aggregations.forEach( function( aggregation ){ + if (!aggregation.col){ + // should have a group.colName if there's no col + aggregation.col = grid.getColumn(aggregation.colName); + } + service.aggregateColumn(grid, aggregation.col, null); + }); + } + + }, + /** * @ngdoc function @@ -561,10 +887,15 @@ * @param {Grid} grid grid object */ tidyPriorities: function( grid ){ + // if we're called from sortChanged, grid is in this, not passed as param + if ( typeof(grid) === 'undefined' && typeof(this.grid) !== 'undefined' ) { + grid = this.grid; + } + var groupArray = []; var sortArray = []; - angular.forEach(grid.columns, function(column, index){ + grid.columns.forEach( function(column, index){ if ( typeof(column.grouping) !== 'undefined' && typeof(column.grouping.groupPriority) !== 'undefined' && column.grouping.groupPriority >= 0){ groupArray.push(column); } else if ( typeof(column.sort) !== 'undefined' && typeof(column.sort.priority) !== 'undefined' && column.sort.priority >= 0){ @@ -573,7 +904,7 @@ }); groupArray.sort(function(a, b){ return a.grouping.groupPriority - b.grouping.groupPriority; }); - angular.forEach(groupArray, function(column, index){ + groupArray.forEach( function(column, index){ column.grouping.groupPriority = index; column.suppressRemoveSort = true; if ( typeof(column.sort) === 'undefined'){ @@ -584,7 +915,7 @@ var i = groupArray.length; sortArray.sort(function(a, b){ return a.sort.priority - b.sort.priority; }); - angular.forEach(sortArray, function(column, index){ + sortArray.forEach( function(column, index){ column.sort.priority = i; column.suppressRemoveSort = column.colDef.suppressRemoveSort; i++; @@ -592,48 +923,6 @@ }, - /** - * @ngdoc function - * @name moveGroupColumns - * @methodOf ui.grid.grouping.service:uiGridGroupingService - * @description Moves the column order so that the grouped columns are lined up - * to the left (well, unless you're RTL, then it's the right). Doesn't change - * the columnDefs, just the columns array. (check move columns that this is how it works) - * - * All other columns retain their relative position, after the group columns - * - * Does nothing if the option `moveGroupColumns` is set to false. - * - * @param {Grid} grid grid object - */ - moveGroupColumns: function( grid ){ - if ( grid.options.moveGroupColumns === false){ - return; - } - - grid.columns.sort(function(a, b){ - var a_group, b_group; - if ( typeof(a.grouping) === 'undefined' || typeof(a.grouping.groupPriority) === 'undefined' || a.grouping.groupPriority < 0){ - a_group = null; - } else { - a_group = a.grouping.groupPriority; - } - - if ( typeof(b.grouping) === 'undefined' || typeof(b.grouping.groupPriority) === 'undefined' || b.grouping.groupPriority < 0){ - b_group = null; - } else { - b_group = b.grouping.groupPriority; - } - - // groups get sorted to the top - if ( a_group !== null && b_group === null) { return -1; } - if ( b_group !== null && a_group === null) { return 1; } - if ( a_group !== null && b_group !== null) {return a_group - b_group; } - }); - grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN ); - }, - - /** * @ngdoc function * @name expandAllRows @@ -749,7 +1038,7 @@ return; } - service.setAllNodes(row.expandedState, uiGridGroupingConstants.COLLAPSED); + service.setAllNodes(row.expandedState, uiGridGroupingConstants.EXPANDED); grid.queueGridRefresh(); }, @@ -871,7 +1160,7 @@ for (var i = 0; i < renderableRows.length; i++ ){ var row = renderableRows[i]; - angular.forEach(groupingProcessingState, updateProcessingState); + groupingProcessingState.forEach( updateProcessingState); service.setVisibility( grid, row, groupingProcessingState ); } @@ -880,7 +1169,7 @@ service.writeOutAggregations( grid, groupingProcessingState, 0); - return renderableRows; + return renderableRows.filter(function (row) { return row.visible; }); }, @@ -899,11 +1188,15 @@ var processingState = []; var columnSettings = service.getGrouping( grid ); - angular.forEach(columnSettings.grouping, function( groupItem, index){ + columnSettings.grouping.forEach( function( groupItem, index){ // get the aggregation config to copy in - do this multiple times as shallow copying it // was harder than it looked, and as much work as just creating it again var aggregations = []; - angular.forEach(columnSettings.aggregations, function(aggregation, index){ + if (grid.options.groupingShowCounts){ + aggregations.push({type: uiGridGroupingConstants.aggregation.COUNT, fieldName: uiGridGroupingConstants.aggregation.FIELD, value: null }); + } else { + } + columnSettings.aggregations.forEach( function(aggregation, index){ if (aggregation.aggregation === uiGridGroupingConstants.aggregation.AVG){ aggregations.push({ type: aggregation.aggregation, fieldName: aggregation.field, col: aggregation.col, value: null, sum: null, count: null }); @@ -940,7 +1233,7 @@ var aggregateArray = []; // get all the grouping - angular.forEach(grid.columns, function(column, columnIndex){ + grid.columns.forEach( function(column, columnIndex){ if ( column.grouping ){ if ( typeof(column.grouping.groupPriority) !== 'undefined' && column.grouping.groupPriority >= 0){ groupArray.push({ field: column.field, col: column, groupPriority: column.grouping.groupPriority, grouping: column.grouping }); @@ -949,14 +1242,14 @@ } } }); - + // sort grouping into priority order groupArray.sort( function(a, b){ return a.groupPriority - b.groupPriority; }); // renumber the priority in case it was somewhat messed up, then remove the grouping reference - angular.forEach( groupArray, function( group, index) { + groupArray.forEach( function( group, index) { group.grouping.groupPriority = index; group.groupPriority = index; delete group.grouping; @@ -1044,18 +1337,24 @@ */ writeOutAggregation: function( grid, processingState ) { if ( processingState.currentGroupHeader ){ - angular.forEach(processingState.runningAggregations, function( aggregation, index ){ - if (aggregation.col.groupingSuppressAggregationText){ - processingState.currentGroupHeader.entity[aggregation.fieldName] = aggregation.value; + processingState.runningAggregations.forEach( function( aggregation, index ){ + if (aggregation.fieldName === uiGridGroupingConstants.aggregation.FIELD){ + // running total to include in the groupHeader + processingState.currentGroupHeader.entity[processingState.fieldName] = processingState.currentValue + ' (' + aggregation.value + ')'; + aggregation.value = null; } else { - processingState.currentGroupHeader.entity[aggregation.fieldName] = i18nService.get().aggregation[aggregation.type] + aggregation.value; - } - aggregation.value = null; - if ( aggregation.sum ){ - aggregation.sum = null; - } - if ( aggregation.count ){ - aggregation.count = null; + if (aggregation.col.groupingSuppressAggregationText){ + processingState.currentGroupHeader.entity[aggregation.fieldName] = aggregation.value; + } else { + processingState.currentGroupHeader.entity[aggregation.fieldName] = i18nService.get().aggregation[aggregation.type] + aggregation.value; + } + aggregation.value = null; + if ( aggregation.sum ){ + aggregation.sum = null; + } + if ( aggregation.count ){ + aggregation.count = null; + } } }); } @@ -1112,18 +1411,12 @@ return; } - var visible = true; var groupLevel = typeof(row.groupLevel) !== 'undefined' ? row.groupLevel : groupingProcessingState.length; for (var i = 0; i < groupLevel; i++){ if ( groupingProcessingState[i].currentGroupHeader.expandedState.state === uiGridGroupingConstants.COLLAPSED ){ - visible = false; + row.visible = false; } } - - // we're running in a rowProcessor, so default is always visible, we don't need to set it unless we want invisible - if ( !visible ){ - row.setThisRowInvisible( 'grouping', true ); - } }, @@ -1139,39 +1432,40 @@ */ aggregate: function( grid, row, groupFieldState ){ // TODO: check data types, cast as necessary, all that jazz - angular.forEach( groupFieldState.runningAggregations, function( aggregation, index ){ - var fieldValue = grid.getCellValue(row, aggregation.col); - var numValue = Number(fieldValue); - switch (aggregation.type) { - case uiGridGroupingConstants.aggregation.COUNT: - aggregation.value++; - break; - case uiGridGroupingConstants.aggregation.SUM: - if (!isNaN(numValue)){ - aggregation.value += numValue; - } - break; - case uiGridGroupingConstants.aggregation.MIN: - if (fieldValue !== null && (fieldValue < aggregation.value || aggregation.value === null)){ - aggregation.value = fieldValue; - } - break; - case uiGridGroupingConstants.aggregation.MAX: - if (fieldValue > aggregation.value){ - aggregation.value = fieldValue; - } - break; - case uiGridGroupingConstants.aggregation.AVG: - aggregation.count++; - if (!isNaN(numValue)){ - aggregation.sum += numValue; - } - aggregation.value = aggregation.sum / aggregation.count; - break; + groupFieldState.runningAggregations.forEach( function( aggregation, index ){ + if (aggregation.type === uiGridGroupingConstants.aggregation.COUNT){ + // don't need getCellValue for counting, and column isn't present sometimes + aggregation.value++; + } else { + var fieldValue = grid.getCellValue(row, aggregation.col); + var numValue = Number(fieldValue); + switch (aggregation.type) { + case uiGridGroupingConstants.aggregation.SUM: + if (!isNaN(numValue)){ + aggregation.value += numValue; + } + break; + case uiGridGroupingConstants.aggregation.MIN: + if (fieldValue !== undefined && fieldValue !== null && (fieldValue < aggregation.value || aggregation.value === null)){ + aggregation.value = fieldValue; + } + break; + case uiGridGroupingConstants.aggregation.MAX: + if (fieldValue !== undefined && fieldValue > aggregation.value){ + aggregation.value = fieldValue; + } + break; + case uiGridGroupingConstants.aggregation.AVG: + aggregation.count++; + if (!isNaN(numValue)){ + aggregation.sum += numValue; + } + aggregation.value = aggregation.sum / aggregation.count; + break; + } } }); - } - + } }; return service; @@ -1227,7 +1521,7 @@ var groupingRowHeaderDef = { name: uiGridGroupingConstants.groupingRowHeaderColName, displayName: '', - width: uiGridCtrl.grid.options.groupingRowHeaderWidth, + width: uiGridCtrl.grid.options.groupingRowHeaderBaseWidth, minWidth: 10, cellTemplate: 'ui-grid/groupingRowHeader', headerCellTemplate: 'ui-grid/groupingHeaderCell', diff --git a/src/features/grouping/test/grouping.spec.js b/src/features/grouping/test/grouping.spec.js index 696c1cb25d..4fc0dadc0d 100644 --- a/src/features/grouping/test/grouping.spec.js +++ b/src/features/grouping/test/grouping.spec.js @@ -79,19 +79,14 @@ describe('ui.grid.grouping uiGridGroupingService', function () { grid.columns[0].grouping = { groupPriority: 1 }; grid.columns[1].grouping = { groupPriority: 2 }; - var groupedRows = uiGridGroupingService.groupRows.call( grid, grid.rows ); - -/* - console.log('data'); - for (var i = 0; i < 10; i++) { - console.log(grid.options.data[i]); - } - - console.log('results'); - for (i = 0; i < 18; i++) { - console.log(grid.rows[i].entity); - } -*/ + var groupedRows = uiGridGroupingService.groupRows.call( grid, grid.rows.slice(0) ); + expect( groupedRows.length ).toEqual( 3, 'only the level 1 rows are visible' ); + + grid.api.grouping.expandAllRows(); + grid.rows.forEach(function( row ){ + row.visible = true; + }); + groupedRows = uiGridGroupingService.groupRows.call( grid, grid.rows.slice(0) ); expect( groupedRows.length ).toEqual( 18, 'we\'ve added 3 col0 headers, and 5 col2 headers' ); }); }); @@ -105,9 +100,10 @@ describe('ui.grid.grouping uiGridGroupingService', function () { ]); }); - it('no aggregation', function() { + it('no aggregation, but groupingShowCounts', function() { grid.columns[1].grouping = {groupPriority: 3}; grid.columns[3].grouping = {groupPriority: 2}; + grid.options.groupingShowCounts = true; var result = uiGridGroupingService.initialiseProcessingState(grid); expect(result[0].col).toEqual(grid.columns[3]); @@ -116,8 +112,29 @@ describe('ui.grid.grouping uiGridGroupingService', function () { delete result[1].col; expect(result).toEqual([ - { fieldName: 'col3', initialised: false, currentValue: null, currentGroupHeader: null, runningAggregations: {} }, - { fieldName: 'col1', initialised: false, currentValue: null, currentGroupHeader: null, runningAggregations: {} } + { fieldName: 'col3', initialised: false, currentValue: null, currentGroupHeader: null, runningAggregations: [ + { type : uiGridGroupingConstants.aggregation.COUNT, fieldName : uiGridGroupingConstants.aggregation.FIELD, value : null } + ] }, + { fieldName: 'col1', initialised: false, currentValue: null, currentGroupHeader: null, runningAggregations: [ + { type : uiGridGroupingConstants.aggregation.COUNT, fieldName : uiGridGroupingConstants.aggregation.FIELD, value : null } + ] } + ]); + }); + + it('no aggregation, without groupingShowCounts', function() { + grid.columns[1].grouping = {groupPriority: 3}; + grid.columns[3].grouping = {groupPriority: 2}; + grid.options.groupingShowCounts = false; + + var result = uiGridGroupingService.initialiseProcessingState(grid); + expect(result[0].col).toEqual(grid.columns[3]); + delete result[0].col; + expect(result[1].col).toEqual(grid.columns[1]); + delete result[1].col; + + expect(result).toEqual([ + { fieldName: 'col3', initialised: false, currentValue: null, currentGroupHeader: null, runningAggregations: [] }, + { fieldName: 'col1', initialised: false, currentValue: null, currentGroupHeader: null, runningAggregations: [] } ]); }); @@ -126,27 +143,30 @@ describe('ui.grid.grouping uiGridGroupingService', function () { grid.columns[1].grouping = {groupPriority: 3}; grid.columns[2].grouping = {aggregation: uiGridGroupingConstants.aggregation.SUM}; grid.columns[3].grouping = {groupPriority: 2}; + grid.options.groupingShowCounts = true; // when expected results go wrong the messages suck if columns are in the results...so check them individually then delete them out var result = uiGridGroupingService.initialiseProcessingState(grid); expect(result[0].col).toEqual(grid.columns[3]); delete result[0].col; - expect(result[0].runningAggregations[0].col).toEqual(grid.columns[0]); - delete result[0].runningAggregations[0].col; - expect(result[0].runningAggregations[1].col).toEqual(grid.columns[2]); + expect(result[0].runningAggregations[1].col).toEqual(grid.columns[0]); delete result[0].runningAggregations[1].col; + expect(result[0].runningAggregations[2].col).toEqual(grid.columns[2]); + delete result[0].runningAggregations[2].col; expect(result[1].col).toEqual(grid.columns[1]); delete result[1].col; - expect(result[1].runningAggregations[0].col).toEqual(grid.columns[0]); - delete result[1].runningAggregations[0].col; - expect(result[1].runningAggregations[1].col).toEqual(grid.columns[2]); + expect(result[1].runningAggregations[1].col).toEqual(grid.columns[0]); delete result[1].runningAggregations[1].col; + expect(result[1].runningAggregations[2].col).toEqual(grid.columns[2]); + delete result[1].runningAggregations[2].col; expect(result).toEqual([ { fieldName: 'col3', initialised: false, currentValue: null, currentGroupHeader: null, runningAggregations: [ + { type : uiGridGroupingConstants.aggregation.COUNT, fieldName : uiGridGroupingConstants.aggregation.FIELD, value : null }, { type: uiGridGroupingConstants.aggregation.COUNT, fieldName: 'col0', value: null }, { type: uiGridGroupingConstants.aggregation.SUM, fieldName: 'col2', value: null } ] }, { fieldName: 'col1', initialised: false, currentValue: null, currentGroupHeader: null, runningAggregations: [ + { type : uiGridGroupingConstants.aggregation.COUNT, fieldName : uiGridGroupingConstants.aggregation.FIELD, value : null }, { type: uiGridGroupingConstants.aggregation.COUNT, fieldName: 'col0', value: null }, { type: uiGridGroupingConstants.aggregation.SUM, fieldName: 'col2', value: null } ] } @@ -238,7 +258,153 @@ describe('ui.grid.grouping uiGridGroupingService', function () { }); }); }); - + + + describe('getGrouping via api (returns colName)', function() { + it('should find no grouping', function() { + expect(grid.api.grouping.getGrouping( true )).toEqual({ + grouping: [], + aggregations: [], + rowExpandedStates: {} + }); + }); + + it('should find no grouping, no expanded states', function() { + expect(grid.api.grouping.getGrouping( false )).toEqual({ + grouping: [], + aggregations: [] + }); + }); + + it('should find no grouping, expanded states present', function() { + grid.grouping.rowExpandedStates = { male: { state: 'expanded' } }; + expect(grid.api.grouping.getGrouping( true )).toEqual({ + grouping: [], + aggregations: [], + rowExpandedStates: { male: { state: 'expanded' } } + }); + }); + + it('finds one grouping', function() { + grid.columns[1].grouping = {groupPriority: 0}; + expect(grid.api.grouping.getGrouping(true)).toEqual({ + grouping: [{ field: 'col1', colName: 'col1', groupPriority: 0 }], + aggregations: [], + rowExpandedStates: {} + }); + }); + + it('finds one aggregation, has no priority', function() { + grid.columns[1].grouping = {aggregation: uiGridGroupingConstants.aggregation.COUNT}; + expect(grid.api.grouping.getGrouping(false)).toEqual({ + grouping: [], + aggregations: [{ field: 'col1', colName: 'col1', aggregation: uiGridGroupingConstants.aggregation.COUNT} ] + }); + }); + + it('finds one aggregation, has a priority, aggregation is ignored', function() { + grid.columns[1].grouping = {groupPriority: 0, aggregation: uiGridGroupingConstants.aggregation.COUNT}; + expect(grid.api.grouping.getGrouping(false)).toEqual({ + grouping: [{ field: 'col1', colName: 'col1', groupPriority: 0 }], + aggregations: [] + }); + }); + + it('finds one aggregation, has no priority, aggregation is stored', function() { + grid.columns[1].grouping = {groupPriority: -1, aggregation: uiGridGroupingConstants.aggregation.COUNT}; + expect(grid.api.grouping.getGrouping(false)).toEqual({ + grouping: [], + aggregations: [ { field: 'col1', colName: 'col1', aggregation: uiGridGroupingConstants.aggregation.COUNT } ] + }); + }); + + it('multiple finds, sorts correctly', function() { + grid.columns[1].grouping = {aggregation: uiGridGroupingConstants.aggregation.COUNT}; + grid.columns[2].grouping = {groupPriority: 1}; + grid.columns[3].grouping = {groupPriority: 0, aggregation: uiGridGroupingConstants.aggregation.COUNT}; + expect(grid.api.grouping.getGrouping(false)).toEqual({ + grouping: [ + { field: 'col3', colName: 'col3', groupPriority: 0 }, + { field: 'col2', colName: 'col2', groupPriority: 1 } + ], + aggregations: [ + { field: 'col1', colName: 'col1', aggregation: uiGridGroupingConstants.aggregation.COUNT} + ] + }); + }); + }); + + + describe('setGrouping', function() { + it('no grouping', function() { + grid.api.grouping.setGrouping( + {} + ); + expect(grid.api.grouping.getGrouping( true )).toEqual( + { grouping: [], aggregations: [], rowExpandedStates: {} } + ); + }); + + it('grouping, aggregations and rowExpandedStates', function() { + grid.api.grouping.setGrouping({ + grouping: [ + { field: 'col3', colName: 'col3', groupPriority: 0 }, + { field: 'col2', colName: 'col2', groupPriority: 1 } + ], + aggregations: [ + { field: 'col1', colName: 'col1', aggregation: uiGridGroupingConstants.aggregation.COUNT} + ], + rowExpandedStates: { male: { state: 'expanded' } } + }); + expect(grid.api.grouping.getGrouping(true)).toEqual({ + grouping: [ + { field: 'col3', colName: 'col3', groupPriority: 0 }, + { field: 'col2', colName: 'col2', groupPriority: 1 } + ], + aggregations: [ + { field: 'col1', colName: 'col1', aggregation: uiGridGroupingConstants.aggregation.COUNT} + ], + rowExpandedStates: { male: { state: 'expanded' } } + }); + }); + + }); + + + describe('clearGrouping', function() { + it('no grouping', function() { + grid.api.grouping.setGrouping( + {} + ); + + // really just checking there are no errors, it should do nothing + grid.api.grouping.clearGrouping(); + + expect(grid.api.grouping.getGrouping( true )).toEqual( + { grouping: [], aggregations: [], rowExpandedStates: {} } + ); + }); + + it('clear grouping, aggregations and rowExpandedStates', function() { + grid.api.grouping.setGrouping({ + grouping: [ + { field: 'col3', colName: 'col3', groupPriority: 0 }, + { field: 'col2', colName: 'col2', groupPriority: 1 } + ], + aggregations: [ + { field: 'col1', colName: 'col1', aggregation: uiGridGroupingConstants.aggregation.COUNT} + ], + rowExpandedStates: { male: { state: 'expanded' } } + }); + grid.api.grouping.clearGrouping(); + + expect(grid.api.grouping.getGrouping( true )).toEqual( + { grouping: [], aggregations: [], rowExpandedStates: { male : { state : 'expanded' } } } + ); + }); + + }); + describe('insertGroupHeader', function() { it('inserts a header in the middle', function() { @@ -662,7 +828,6 @@ describe('ui.grid.grouping uiGridGroupingService', function () { uiGridGroupingService.setVisibility( grid, grid.rows[1], processingStates ); expect( grid.rows[1].visible ).toEqual(false); - expect( grid.rows[1].invisibleReason.grouping).toEqual(true); }); it( 'visible', function() { diff --git a/src/features/importer/js/importer.js b/src/features/importer/js/importer.js index 972de40f8c..fdcff340d2 100644 --- a/src/features/importer/js/importer.js +++ b/src/features/importer/js/importer.js @@ -325,13 +325,15 @@ addToMenu: function ( grid ) { grid.api.core.addToGridMenu( grid, [ { - title: i18nService.getSafeText('gridMenu.importerTitle') + title: i18nService.getSafeText('gridMenu.importerTitle'), + order: 150 }, { templateUrl: 'ui-grid/importerMenuItemContainer', action: function ($event) { this.grid.api.importer.importAFile( grid ); - } + }, + order: 151 } ]); }, @@ -384,7 +386,11 @@ var newObjects = []; var newObject; - angular.forEach( service.parseJson( grid, importFile ), function( value, index ) { + var importArray = service.parseJson( grid, importFile ); + if (importArray === null){ + return; + } + importArray.forEach( function( value, index ) { newObject = service.newObject( grid ); angular.extend( newObject, value ); newObject = grid.options.importerObjectCallback( grid, newObject ); @@ -488,8 +494,7 @@ * the columns in the column defs. The resulting objects will have attributes * that are named based on the column.field or column.name, in that order. * @param {Grid} grid the grid that we want to import into - * @param {FileObject} importFile the file that we want to import, as a - * file object + * @param {Array} importArray the data that we want to import, as an array */ createCsvObjects: function( grid, importArray ){ // pull off header row and turn into headers @@ -501,13 +506,15 @@ var newObjects = []; var newObject; - angular.forEach( importArray, function( row, index ) { + importArray.forEach( function( row, index ) { newObject = service.newObject( grid ); - angular.forEach( row, function( field, index ){ - if ( headerMapping[index] !== null ){ - newObject[ headerMapping[index] ] = field; - } - }); + if ( row !== null ){ + row.forEach( function( field, index ){ + if ( headerMapping[index] !== null ){ + newObject[ headerMapping[index] ] = field; + } + }); + } newObject = grid.options.importerObjectCallback( grid, newObject ); newObjects.push( newObject ); }); @@ -534,13 +541,13 @@ if ( !grid.options.columnDefs || grid.options.columnDefs.length === 0 ){ // we are going to create new columnDefs for all these columns, so just remove // spaces from the names to create fields - angular.forEach( headerRow, function( value, index ) { + headerRow.forEach( function( value, index ) { headers.push( value.replace( /[^0-9a-zA-Z\-_]/g, '_' ) ); }); return headers; } else { var lookupHash = service.flattenColumnDefs( grid, grid.options.columnDefs ); - angular.forEach( headerRow, function( value, index ) { + headerRow.forEach( function( value, index ) { if ( lookupHash[value] ) { headers.push( lookupHash[value] ); } else if ( lookupHash[ value.toLowerCase() ] ) { @@ -569,7 +576,7 @@ */ flattenColumnDefs: function( grid, columnDefs ){ var flattenedHash = {}; - angular.forEach( columnDefs, function( columnDef, index) { + columnDefs.forEach( function( columnDef, index) { if ( columnDef.name ){ flattenedHash[ columnDef.name ] = columnDef.field || columnDef.name; flattenedHash[ columnDef.name.toLowerCase() ] = columnDef.field || columnDef.name; diff --git a/src/features/infinite-scroll/js/infinite-scroll.js b/src/features/infinite-scroll/js/infinite-scroll.js index bd90ae8747..d421025078 100644 --- a/src/features/infinite-scroll/js/infinite-scroll.js +++ b/src/features/infinite-scroll/js/infinite-scroll.js @@ -17,7 +17,7 @@ * * @description Service for infinite scroll features */ - module.service('uiGridInfiniteScrollService', ['gridUtil', '$compile', '$timeout', 'uiGridConstants', function (gridUtil, $compile, $timeout, uiGridConstants) { + module.service('uiGridInfiniteScrollService', ['gridUtil', '$compile', '$timeout', 'uiGridConstants', 'ScrollEvent', '$q', function (gridUtil, $compile, $timeout, uiGridConstants, ScrollEvent, $q) { var service = { @@ -28,9 +28,17 @@ * @description This method register events and methods into grid public API */ - initializeGrid: function(grid) { + initializeGrid: function(grid, $scope) { service.defaultGridOptions(grid.options); + if (!grid.options.enableInfiniteScroll){ + return; + } + + grid.infiniteScroll = { dataLoading: false }; + service.setScrollDirections( grid, grid.options.infiniteScrollUp, grid.options.infiniteScrollDown ); + grid.api.core.on.scrollEnd($scope, service.handleScroll); + /** * @ngdoc object * @name ui.grid.infiniteScroll.api:PublicAPI @@ -45,7 +53,7 @@ * @ngdoc event * @name needLoadMoreData * @eventOf ui.grid.infiniteScroll.api:PublicAPI - * @description This event fires when scroll reached bottom percentage of grid + * @description This event fires when scroll reaches bottom percentage of grid * and needs to load data */ @@ -56,7 +64,7 @@ * @ngdoc event * @name needLoadMoreDataTop * @eventOf ui.grid.infiniteScroll.api:PublicAPI - * @description This event fires when scroll reached top percentage of grid + * @description This event fires when scroll reaches top percentage of grid * and needs to load data */ @@ -71,20 +79,120 @@ * @ngdoc function * @name dataLoaded * @methodOf ui.grid.infiniteScroll.api:PublicAPI - * @description This function is used as a promise when data finished loading. - * See infinite_scroll ngdoc for example of usage + * @description Call this function when you have loaded the additional data + * requested. You should set scrollUp and scrollDown to indicate + * whether there are still more pages in each direction. + * + * If you call dataLoaded without first calling `saveScrollPercentage` then we will + * scroll the user to the start of the newly loaded data, which usually gives a smooth scroll + * experience, but can give a jumpy experience with large `infiniteScrollRowsFromEnd` values, and + * on variable speed internet connections. Using `saveScrollPercentage` as demonstrated in the tutorial + * should give a smoother scrolling experience for users. + * + * See infinite_scroll tutorial for example of usage + * @param {boolean} scrollUp if set to false flags that there are no more pages upwards, so don't fire + * any more infinite scroll events upward + * @param {boolean} scrollDown if set to false flags that there are no more pages downwards, so don't + * fire any more infinite scroll events downward + * @returns {promise} a promise that is resolved when the grid scrolling is fully adjusted. If you're + * planning to remove pages, you should wait on this promise first, or you'll break the scroll positioning */ + dataLoaded: function( scrollUp, scrollDown ) { + service.setScrollDirections(grid, scrollUp, scrollDown); + + var promise = service.adjustScroll(grid).then(function() { + grid.infiniteScroll.dataLoading = false; + }); + + return promise; + }, + + /** + * @ngdoc function + * @name resetScroll + * @methodOf ui.grid.infiniteScroll.api:PublicAPI + * @description Call this function when you have taken some action that makes the current + * scroll position invalid. For example, if you're using external sorting and you've resorted + * then you might reset the scroll, or if you've otherwise substantially changed the data, perhaps + * you've reused an existing grid for a new data set + * + * You must tell us whether there is data upwards or downwards after the reset + * + * @param {boolean} scrollUp flag that there are pages upwards, fire + * infinite scroll events upward + * @param {boolean} scrollDown flag that there are pages downwards, so + * fire infinite scroll events downward + * @returns {promise} promise that is resolved when the scroll reset is complete + */ + resetScroll: function( scrollUp, scrollDown ) { + service.setScrollDirections( grid, scrollUp, scrollDown); + + return service.adjustInfiniteScrollPosition(grid, 0); + }, + + + /** + * @ngdoc function + * @name saveScrollPercentage + * @methodOf ui.grid.infiniteScroll.api:PublicAPI + * @description Saves the scroll percentage and number of visible rows before you adjust the data, + * used if you're subsequently going to call `dataRemovedTop` or `dataRemovedBottom` + */ + saveScrollPercentage: function() { + grid.infiniteScroll.prevScrolltopPercentage = grid.renderContainers.body.prevScrolltopPercentage; + grid.infiniteScroll.previousVisibleRows = grid.renderContainers.body.visibleRowCache.length; + }, + + + /** + * @ngdoc function + * @name dataRemovedTop + * @methodOf ui.grid.infiniteScroll.api:PublicAPI + * @description Adjusts the scroll position after you've removed data at the top + * @param {boolean} scrollUp flag that there are pages upwards, fire + * infinite scroll events upward + * @param {boolean} scrollDown flag that there are pages downwards, so + * fire infinite scroll events downward + */ + dataRemovedTop: function( scrollUp, scrollDown ) { + service.dataRemovedTop( grid, scrollUp, scrollDown ); + }, - dataLoaded: function() { - grid.options.loadTimout = false; + /** + * @ngdoc function + * @name dataRemovedBottom + * @methodOf ui.grid.infiniteScroll.api:PublicAPI + * @description Adjusts the scroll position after you've removed data at the bottom + * @param {boolean} scrollUp flag that there are pages upwards, fire + * infinite scroll events upward + * @param {boolean} scrollDown flag that there are pages downwards, so + * fire infinite scroll events downward + */ + dataRemovedBottom: function( scrollUp, scrollDown ) { + service.dataRemovedBottom( grid, scrollUp, scrollDown ); + }, + + /** + * @ngdoc function + * @name setScrollDirections + * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService + * @description Sets the scrollUp and scrollDown flags, handling nulls and undefined, + * and also sets the grid.suppressParentScroll + * @param {boolean} scrollUp whether there are pages available up - defaults to false + * @param {boolean} scrollDown whether there are pages available down - defaults to true + */ + setScrollDirections: function ( scrollUp, scrollDown ) { + service.setScrollDirections( grid, scrollUp, scrollDown ); } + } } }; - grid.options.loadTimout = false; grid.api.registerEventsFromObject(publicApi.events); grid.api.registerMethodsFromObject(publicApi.methods); }, + + defaultGridOptions: function (gridOptions) { //default option to true unless it was explicitly set to false /** @@ -103,6 +211,95 @@ *
Defaults to true */ gridOptions.enableInfiniteScroll = gridOptions.enableInfiniteScroll !== false; + + /** + * @ngdoc property + * @name infiniteScrollRowsFromEnd + * @propertyOf ui.grid.class:GridOptions + * @description This setting controls how close to the end of the dataset a user gets before + * more data is requested by the infinite scroll, whether scrolling up or down. This allows you to + * 'prefetch' rows before the user actually runs out of scrolling. + * + * Note that if you set this value too high it may give jumpy scrolling behaviour, if you're getting + * this behaviour you could use the `saveScrollPercentageMethod` right before loading your data, and we'll + * preserve that scroll position + * + *
Defaults to 20 + */ + gridOptions.infiniteScrollRowsFromEnd = gridOptions.infiniteScrollRowsFromEnd || 20; + + /** + * @ngdoc property + * @name infiniteScrollUp + * @propertyOf ui.grid.class:GridOptions + * @description Whether you allow infinite scroll up, implying that the first page of data + * you have displayed is in the middle of your data set. If set to true then we trigger the + * needMoreDataTop event when the user hits the top of the scrollbar. + *
Defaults to false + */ + gridOptions.infiniteScrollUp = gridOptions.infiniteScrollUp === true; + + /** + * @ngdoc property + * @name infiniteScrollDown + * @propertyOf ui.grid.class:GridOptions + * @description Whether you allow infinite scroll down, implying that the first page of data + * you have displayed is in the middle of your data set. If set to true then we trigger the + * needMoreData event when the user hits the bottom of the scrollbar. + *
Defaults to true + */ + gridOptions.infiniteScrollDown = gridOptions.infiniteScrollDown !== false; + }, + + + /** + * @ngdoc function + * @name setScrollDirections + * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService + * @description Sets the scrollUp and scrollDown flags, handling nulls and undefined, + * and also sets the grid.suppressParentScroll + * @param {grid} grid the grid we're operating on + * @param {boolean} scrollUp whether there are pages available up - defaults to false + * @param {boolean} scrollDown whether there are pages available down - defaults to true + */ + setScrollDirections: function ( grid, scrollUp, scrollDown ) { + grid.infiniteScroll.scrollUp = ( scrollUp === true ); + grid.suppressParentScrollUp = ( scrollUp === true ); + + grid.infiniteScroll.scrollDown = ( scrollDown !== false); + grid.suppressParentScrollDown = ( scrollDown !== false); + }, + + + /** + * @ngdoc function + * @name handleScroll + * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService + * @description Called whenever the grid scrolls, determines whether the scroll should + * trigger an infinite scroll request for more data + * @param {object} args the args from the event + */ + handleScroll: function (args) { + // don't request data if already waiting for data, or if source is coming from ui.grid.adjustInfiniteScrollPosition() function + if ( args.grid.infiniteScroll && args.grid.infiniteScroll.dataLoading || args.source === 'ui.grid.adjustInfiniteScrollPosition' ){ + return; + } + + if (args.y) { + var percentage; + var targetPercentage = args.grid.options.infiniteScrollRowsFromEnd / args.grid.renderContainers.body.visibleRowCache.length; + if (args.grid.scrollDirection === uiGridConstants.scrollDirection.UP ) { + percentage = args.y.percentage; + if (percentage <= targetPercentage){ + service.loadData(args.grid); + } + } else if (args.grid.scrollDirection === uiGridConstants.scrollDirection.DOWN) { + percentage = 1 - args.y.percentage; + if (percentage <= targetPercentage){ + service.loadData(args.grid); + } + } + } }, @@ -111,43 +308,164 @@ * @name loadData * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService * @description This function fires 'needLoadMoreData' or 'needLoadMoreDataTop' event based on scrollDirection + * and whether there are more pages upwards or downwards. It also stores the number of rows that we had previously, + * and clears out any saved scroll position so that we know whether or not the user calls `saveScrollPercentage` + * @param {Grid} grid the grid we're working on */ - loadData: function (grid) { - grid.options.loadTimout = true; - if (grid.scrollDirection === uiGridConstants.scrollDirection.UP) { + // save number of currently visible rows to calculate new scroll position later - we know that we want + // to be at approximately the row we're currently at + grid.infiniteScroll.previousVisibleRows = grid.renderContainers.body.visibleRowCache.length; + grid.infiniteScroll.direction = grid.scrollDirection; + delete grid.infiniteScroll.prevScrolltopPercentage; + + if (grid.scrollDirection === uiGridConstants.scrollDirection.UP && grid.infiniteScroll.scrollUp ) { + grid.infiniteScroll.dataLoading = true; grid.api.infiniteScroll.raise.needLoadMoreDataTop(); - return; + } else if (grid.scrollDirection === uiGridConstants.scrollDirection.DOWN && grid.infiniteScroll.scrollDown ) { + grid.infiniteScroll.dataLoading = true; + grid.api.infiniteScroll.raise.needLoadMoreData(); } - grid.api.infiniteScroll.raise.needLoadMoreData(); }, - + + /** * @ngdoc function - * @name checkScroll + * @name adjustScroll * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService - * @description This function checks scroll position inside grid and - * calls 'loadData' function when scroll reaches 'infiniteScrollPercentage' + * @description Once we are informed that data has been loaded, adjust the scroll position to account for that + * addition and to make things look clean. + * + * If we're scrolling up we scroll to the first row of the old data set - + * so we're assuming that you would have gotten to the top of the grid (from the 20% need more data trigger) by + * the time the data comes back. If we're scrolling down we scoll to the last row of the old data set - so we're + * assuming that you would have gotten to the bottom of the grid (from the 80% need more data trigger) by the time + * the data comes back. + * + * Neither of these are good assumptions, but making this a smoother experience really requires + * that trigger to not be a percentage, and to be much closer to the end of the data (say, 5 rows off the end). Even then + * it'd be better still to actually run into the end. But if the data takes a while to come back, they may have scrolled + * somewhere else in the mean-time, in which case they'll get a jump back to the new data. Anyway, this will do for + * now, until someone wants to do better. + * @param {Grid} grid the grid we're working on + * @returns {promise} a promise that is resolved when scrolling has finished */ + adjustScroll: function(grid){ + var promise = $q.defer(); + $timeout(function () { + var newPercentage; + + if ( grid.infiniteScroll.direction === undefined ){ + // called from initialize, tweak our scroll up a little + service.adjustInfiniteScrollPosition(grid, 0); + } - checkScroll: function(grid, scrollTop) { + var newVisibleRows = grid.renderContainers.body.visibleRowCache.length; + var oldPercentage, oldTopRow; + var halfViewport = grid.getViewportHeight() / grid.options.rowHeight / 2; + + if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.UP ){ + oldPercentage = grid.infiniteScroll.prevScrolltopPercentage || 0; + oldTopRow = oldPercentage * grid.infiniteScroll.previousVisibleRows; + newPercentage = ( newVisibleRows - grid.infiniteScroll.previousVisibleRows + oldTopRow + halfViewport ) / newVisibleRows; + service.adjustInfiniteScrollPosition(grid, newPercentage); + $timeout( function() { + promise.resolve(); + }); + } - /* Take infiniteScrollPercentage value or use 20% as default */ - var infiniteScrollPercentage = grid.options.infiniteScrollPercentage ? grid.options.infiniteScrollPercentage : 20; + if ( grid.infiniteScroll.direction === uiGridConstants.scrollDirection.DOWN ){ + oldPercentage = grid.infiniteScroll.prevScrolltopPercentage || 1; + oldTopRow = oldPercentage * grid.infiniteScroll.previousVisibleRows; + newPercentage = ( oldTopRow - halfViewport ) / newVisibleRows; + service.adjustInfiniteScrollPosition(grid, newPercentage); + $timeout( function() { + promise.resolve(); + }); + } + }, 0); + + return promise.promise; + }, + + + /** + * @ngdoc function + * @name adjustInfiniteScrollPosition + * @methodOf ui.grid.infiniteScroll.service:uiGridInfiniteScrollService + * @description This function fires 'needLoadMoreData' or 'needLoadMoreDataTop' event based on scrollDirection + * @param {Grid} grid the grid we're working on + * @param {number} percentage the percentage through the grid that we want to scroll to + * @returns {promise} a promise that is resolved when the scrolling finishes + */ + adjustInfiniteScrollPosition: function (grid, percentage) { + var scrollEvent = new ScrollEvent(grid, null, null, 'ui.grid.adjustInfiniteScrollPosition'); - if (!grid.options.loadTimout && scrollTop <= infiniteScrollPercentage) { - this.loadData(grid); - return true; + //for infinite scroll, if there are pages upwards then never allow it to be at the zero position so the up button can be active + if ( percentage === 0 && grid.infiniteScroll.scrollUp ) { + scrollEvent.y = {pixels: 1}; } - return false; - } + else { + scrollEvent.y = {percentage: percentage}; + } + grid.scrollContainers('', scrollEvent); + }, + + + /** + * @ngdoc function + * @name dataRemovedTop + * @methodOf ui.grid.infiniteScroll.api:PublicAPI + * @description Adjusts the scroll position after you've removed data at the top. You should + * have called `saveScrollPercentage` before you remove the data, and if you're doing this in + * response to a `needMoreData` you should wait until the promise from `loadData` has resolved + * before you start removing data + * @param {Grid} grid the grid we're working on + * @param {boolean} scrollUp flag that there are pages upwards, fire + * infinite scroll events upward + * @param {boolean} scrollDown flag that there are pages downwards, so + * fire infinite scroll events downward + * @returns {promise} a promise that is resolved when the scrolling finishes + */ + dataRemovedTop: function( grid, scrollUp, scrollDown ) { + service.setScrollDirections( grid, scrollUp, scrollDown ); + + var newVisibleRows = grid.renderContainers.body.visibleRowCache.length; + var oldScrollRow = grid.infiniteScroll.prevScrolltopPercentage * grid.infiniteScroll.previousVisibleRows; + + // since we removed from the top, our new scroll row will be the old scroll row less the number + // of rows removed + var newScrollRow = oldScrollRow - ( grid.infiniteScroll.previousVisibleRows - newVisibleRows ); + var newScrollPercent = newScrollRow / newVisibleRows; + + return service.adjustInfiniteScrollPosition( grid, newScrollPercent ); + }, + /** - * @ngdoc property - * @name infiniteScrollPercentage - * @propertyOf ui.grid.class:GridOptions - * @description This setting controls at what percentage of the scroll more data - * is requested by the infinite scroll + * @ngdoc function + * @name dataRemovedBottom + * @methodOf ui.grid.infiniteScroll.api:PublicAPI + * @description Adjusts the scroll position after you've removed data at the bottom. You should + * have called `saveScrollPercentage` before you remove the data, and if you're doing this in + * response to a `needMoreData` you should wait until the promise from `loadData` has resolved + * before you start removing data + * @param {Grid} grid the grid we're working on + * @param {boolean} scrollUp flag that there are pages upwards, fire + * infinite scroll events upward + * @param {boolean} scrollDown flag that there are pages downwards, so + * fire infinite scroll events downward */ + dataRemovedBottom: function( grid, scrollUp, scrollDown ) { + service.setScrollDirections( grid, scrollUp, scrollDown ); + + var newVisibleRows = grid.renderContainers.body.visibleRowCache.length; + var oldScrollRow = grid.infiniteScroll.prevScrolltopPercentage * grid.infiniteScroll.previousVisibleRows; + + // since we removed from the bottom, our new scroll row will be same as the old scroll row + var newScrollPercent = oldScrollRow / newVisibleRows; + + return service.adjustInfiniteScrollPosition( grid, newScrollPercent ); + } }; return service; }]); @@ -193,7 +511,7 @@ compile: function($scope, $elm, $attr){ return { pre: function($scope, $elm, $attr, uiGridCtrl) { - uiGridInfiniteScrollService.initializeGrid(uiGridCtrl.grid); + uiGridInfiniteScrollService.initializeGrid(uiGridCtrl.grid, $scope); }, post: function($scope, $elm, $attr) { } @@ -202,26 +520,4 @@ }; }]); - module.directive('uiGridViewport', - ['$compile', 'gridUtil', 'uiGridInfiniteScrollService', 'uiGridConstants', - function ($compile, gridUtil, uiGridInfiniteScrollService, uiGridConstants) { - return { - priority: -200, - scope: false, - link: function ($scope, $elm, $attr){ - if ($scope.grid.options.enableInfiniteScroll) { - $scope.grid.api.core.on.scrollEvent($scope, function (args) { - //Prevent circular scroll references, if source is coming from ui.grid.adjustInfiniteScrollPosition() function - if (args.y && (args.source !== 'ui.grid.adjustInfiniteScrollPosition')) { - var percentage = 100 - (args.y.percentage * 100); - if ($scope.grid.scrollDirection === uiGridConstants.scrollDirection.UP) { - percentage = (args.y.percentage * 100); - } - uiGridInfiniteScrollService.checkScroll($scope.grid, percentage); - } - }); - } - } - }; - }]); })(); \ No newline at end of file diff --git a/src/features/infinite-scroll/test/infiniteScroll.spec.js b/src/features/infinite-scroll/test/infiniteScroll.spec.js index dd9af5c09e..d996818eb8 100644 --- a/src/features/infinite-scroll/test/infiniteScroll.spec.js +++ b/src/features/infinite-scroll/test/infiniteScroll.spec.js @@ -1,74 +1,130 @@ /* global _ */ (function () { - 'use strict'; - describe('ui.grid.infiniteScroll uiGridInfiniteScrollService', function () { + 'use strict'; + describe('ui.grid.infiniteScroll uiGridInfiniteScrollService', function () { - var uiGridInfiniteScrollService; - var grid; - var gridClassFactory; + var uiGridInfiniteScrollService; + var grid; + var gridClassFactory; var uiGridConstants; + var $rootScope; + var $scope; - beforeEach(module('ui.grid.infiniteScroll')); + beforeEach(module('ui.grid.infiniteScroll')); - beforeEach(inject(function (_uiGridInfiniteScrollService_, _gridClassFactory_, _uiGridConstants_) { - uiGridInfiniteScrollService = _uiGridInfiniteScrollService_; - gridClassFactory = _gridClassFactory_; + beforeEach(inject(function (_uiGridInfiniteScrollService_, _gridClassFactory_, _uiGridConstants_, _$rootScope_) { + uiGridInfiniteScrollService = _uiGridInfiniteScrollService_; + gridClassFactory = _gridClassFactory_; uiGridConstants = _uiGridConstants_; - - grid = gridClassFactory.createGrid({}); - - grid.options.columnDefs = [ - {field: 'col1'} - ]; - grid.options.infiniteScroll = 20; - - grid.options.onRegisterApi = function (gridApi) { - gridApi.infiniteScroll.on.needLoadMoreData(function(){ - return []; - }); - gridApi.infiniteScroll.on.needLoadMoreDataTop(function(){ - return []; - }); - - }; - - uiGridInfiniteScrollService.initializeGrid(grid); + $rootScope = _$rootScope_; + $scope = $rootScope.$new(); + + grid = gridClassFactory.createGrid({}); + + grid.options.columnDefs = [ + {field: 'col1'} + ]; + grid.options.infiniteScrollRowsFromEnd = 20; + + uiGridInfiniteScrollService.initializeGrid(grid, $scope); spyOn(grid.api.infiniteScroll.raise, 'needLoadMoreData'); spyOn(grid.api.infiniteScroll.raise, 'needLoadMoreDataTop'); - grid.options.data = [{col1:'a'},{col1:'b'}]; + grid.options.data = [{col1:'a'},{col1:'b'}]; - grid.buildColumns(); + grid.buildColumns(); + + })); - })); - - describe('event handling', function () { - it('should return false if scrollTop is positioned more than 20% of scrollHeight', function() { - var scrollHeight = 100; - var scrollTop = 80; - var callResult = uiGridInfiniteScrollService.checkScroll(grid, scrollTop); - expect(callResult).toBe(false); - }); + describe('event handling', function () { + beforeEach(function() { + spyOn(uiGridInfiniteScrollService, 'loadData').andCallFake(function() {}); + var arrayOf100 = []; + for ( var i = 0; i < 100; i++ ){ + arrayOf100.push(i); + } + grid.renderContainers = { body: { visibleRowCache: arrayOf100}}; + }); + + it('should not request more data if scroll up to 21%', function() { + grid.scrollDirection = uiGridConstants.scrollDirection.UP; + uiGridInfiniteScrollService.handleScroll( { grid: grid, y: { percentage: 0.21 }}); + expect(uiGridInfiniteScrollService.loadData).not.toHaveBeenCalled(); + }); - it('should return false if scrollTop is positioned less than 20% of scrollHeight', function() { - var scrollHeight = 100; - var scrollTop = 19; - var callResult = uiGridInfiniteScrollService.checkScroll(grid, scrollTop); - expect(callResult).toBe(true); - }); + it('should request more data if scroll up to 20%', function() { + grid.scrollDirection = uiGridConstants.scrollDirection.UP; + uiGridInfiniteScrollService.handleScroll( { grid: grid, y: { percentage: 0.20 }}); + expect(uiGridInfiniteScrollService.loadData).toHaveBeenCalled(); + }); - it('should call load data function on grid event raise', function () { - uiGridInfiniteScrollService.loadData(grid); - expect(grid.api.infiniteScroll.raise.needLoadMoreData).toHaveBeenCalled(); - }); + it('should not request more data if scroll down to 79%', function() { + grid.scrollDirection = uiGridConstants.scrollDirection.DOWN; + uiGridInfiniteScrollService.handleScroll( {grid: grid, y: { percentage: 0.79 }}); + expect(uiGridInfiniteScrollService.loadData).not.toHaveBeenCalled(); + }); - it('should call load data top function on grid event raise', function () { + it('should request more data if scroll down to 80%', function() { + grid.scrollDirection = uiGridConstants.scrollDirection.DOWN; + uiGridInfiniteScrollService.handleScroll( { grid: grid, y: { percentage: 0.80 }}); + expect(uiGridInfiniteScrollService.loadData).toHaveBeenCalled(); + }); + }); + + describe('loadData', function() { + it('scroll up and there is data up', function() { grid.scrollDirection = uiGridConstants.scrollDirection.UP; + grid.infiniteScroll.scrollUp = true; + uiGridInfiniteScrollService.loadData(grid); + expect(grid.api.infiniteScroll.raise.needLoadMoreDataTop).toHaveBeenCalled(); + expect(grid.infiniteScroll.previousVisibleRows).toEqual(0); + expect(grid.infiniteScroll.direction).toEqual(uiGridConstants.scrollDirection.UP); }); + it('scroll up and there isn\'t data up', function() { + grid.scrollDirection = uiGridConstants.scrollDirection.UP; + grid.infiniteScroll.scrollUp = false; + + uiGridInfiniteScrollService.loadData(grid); + + expect(grid.api.infiniteScroll.raise.needLoadMoreDataTop).not.toHaveBeenCalled(); + }); + + it('scroll down and there is data down', function() { + grid.scrollDirection = uiGridConstants.scrollDirection.DOWN; + grid.infiniteScroll.scrollDown = true; + + uiGridInfiniteScrollService.loadData(grid); + + expect(grid.api.infiniteScroll.raise.needLoadMoreData).toHaveBeenCalled(); + expect(grid.infiniteScroll.previousVisibleRows).toEqual(0); + expect(grid.infiniteScroll.direction).toEqual(uiGridConstants.scrollDirection.DOWN); + }); + + it('scroll down and there isn\'t data down', function() { + grid.scrollDirection = uiGridConstants.scrollDirection.DOWN; + grid.infiniteScroll.scrollDown = false; + + uiGridInfiniteScrollService.loadData(grid); + + expect(grid.api.infiniteScroll.raise.needLoadMoreData).not.toHaveBeenCalled(); + }); }); - }); + + describe( 'dataRemovedTop', function() { + it( 'adjusts scroll as expected', function() { + + }); + }); + + + describe( 'dataRemovedBottom', function() { + it( 'adjusts scroll as expected', function() { + + }); + }); + }); })(); \ No newline at end of file diff --git a/src/features/move-columns/js/column-movable.js b/src/features/move-columns/js/column-movable.js index 7b2b59f364..fd9391e957 100644 --- a/src/features/move-columns/js/column-movable.js +++ b/src/features/move-columns/js/column-movable.js @@ -150,9 +150,9 @@ } } columns[newPosition] = originalColumn; + grid.queueGridRefresh(); $timeout(function () { grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN ); - grid.queueGridRefresh(); grid.api.colMovable.raise.columnPositionChanged(originalColumn.colDef, originalPosition, newPosition); }); } @@ -247,220 +247,229 @@ if ($scope.col.colDef.enableColumnMoving) { - $scope.$on(uiGridConstants.events.COLUMN_HEADER_CLICK, function (event, args) { - - if (args.columnName === $scope.col.colDef.name && !$scope.col.renderContainer) { - - var evt = args.event; - if (evt.target.className !== 'ui-grid-icon-angle-down' && evt.target.tagName !== 'I' && - evt.target.className.indexOf('ui-grid-filter-input') < 0) { - - //Setting some variables required for calculations. - var gridLeft = $scope.grid.element[0].getBoundingClientRect().left; - var previousMouseX = evt.pageX; - var totalMouseMovement = 0; - var rightMoveLimit = gridLeft + $scope.grid.getViewportWidth();// - $scope.grid.verticalScrollbarWidth; - - //Clone element should move horizontally with mouse. - var elmCloned = false; - var movingElm; - var reducedWidth; - - var cloneElement = function () { - elmCloned = true; - - //Cloning header cell and appending to current header cell. - movingElm = $elm.clone(); - $elm.parent().append(movingElm); - - //Left of cloned element should be aligned to original header cell. - movingElm.addClass('movingColumn'); - var movingElementStyles = {}; - var elmLeft = $elm[0].getBoundingClientRect().left; - movingElementStyles.left = (elmLeft - gridLeft) + 'px'; - var gridRight = $scope.grid.element[0].getBoundingClientRect().right; - var elmRight = $elm[0].getBoundingClientRect().right; - if (elmRight > gridRight) { - reducedWidth = $scope.col.drawnWidth + (gridRight - elmRight); - movingElementStyles.width = reducedWidth + 'px'; - } - movingElm.css(movingElementStyles); - }; - - var moveElement = function (changeValue) { - //Hide column menu - uiGridCtrl.fireEvent('hide-menu'); - - //Calculate total column width - var columns = $scope.grid.columns; - var totalColumnWidth = 0; - for (var i = 0; i < columns.length; i++) { - if (angular.isUndefined(columns[i].colDef.visible) || columns[i].colDef.visible === true) { - totalColumnWidth += columns[i].drawnWidth || columns[i].width || columns[i].colDef.width; - } - } + /* + * Our general approach to column move is that we listen to a touchstart or mousedown + * event over the column header. When we hear one, then we wait for a move of the same type + * - if we are a touchstart then we listen for a touchmove, if we are a mousedown we listen for + * a mousemove (i.e. a drag) before we decide that there's a move underway. If there's never a move, + * and we instead get a mouseup or a touchend, then we just drop out again and do nothing. + * + */ + var $contentsElm = angular.element( $elm[0].querySelectorAll('.ui-grid-cell-contents') ); - //Calculate new position of left of column - var currentElmLeft = movingElm[0].getBoundingClientRect().left - 1; - var currentElmRight = movingElm[0].getBoundingClientRect().right; - var newElementLeft; - if (gridUtil.detectBrowser() === 'ie') { - newElementLeft = currentElmLeft + changeValue; - } - else { - newElementLeft = currentElmLeft - gridLeft + changeValue; - } - newElementLeft = newElementLeft < rightMoveLimit ? newElementLeft : rightMoveLimit; + var gridLeft; + var previousMouseX; + var totalMouseMovement; + var rightMoveLimit; + var elmCloned = false; + var movingElm; + var reducedWidth; + var moveOccurred = false; - //Update css of moving column to adjust to new left value or fire scroll in case column has reached edge of grid - if ((currentElmLeft >= gridLeft || changeValue > 0) && (currentElmRight <= rightMoveLimit || changeValue < 0)) { - movingElm.css({visibility: 'visible', 'left': newElementLeft + 'px'}); - } - else if (totalColumnWidth > Math.ceil(uiGridCtrl.grid.gridWidth)) { - changeValue *= 8; - var scrollEvent = new ScrollEvent($scope.col.grid, null, null, 'uiGridHeaderCell.moveElement'); - scrollEvent.x = {pixels: changeValue}; - scrollEvent.fireScrollingEvent(); - } + var downFn = function( event ){ + //Setting some variables required for calculations. + gridLeft = $scope.grid.element[0].getBoundingClientRect().left; + previousMouseX = event.pageX; + totalMouseMovement = 0; + rightMoveLimit = gridLeft + $scope.grid.getViewportWidth(); - //Calculate total width of columns on the left of the moving column and the mouse movement - var totalColumnsLeftWidth = 0; - for (var il = 0; il < columns.length; il++) { - if (angular.isUndefined(columns[il].colDef.visible) || columns[il].colDef.visible === true) { - if (columns[il].colDef.name !== $scope.col.colDef.name) { - totalColumnsLeftWidth += columns[il].drawnWidth || columns[il].width || columns[il].colDef.width; - } - else { - break; - } - } - } - if ($scope.newScrollLeft === undefined) { - totalMouseMovement += changeValue; - } - else { - totalMouseMovement = $scope.newScrollLeft + newElementLeft - totalColumnsLeftWidth; - } + if ( event.type === 'mousedown' ){ + $document.on('mousemove', moveFn); + $document.on('mouseup', upFn); + } else if ( event.type === 'touchstart' ){ + $document.on('touchmove', moveFn); + $document.on('touchend', upFn); + } + }; - //Increase width of moving column, in case the rightmost column was moved and its width was - //decreased because of overflow - if (reducedWidth < $scope.col.drawnWidth) { - reducedWidth += Math.abs(changeValue); - movingElm.css({'width': reducedWidth + 'px'}); - } - }; + var moveFn = function( event ) { + //Disable text selection in Chrome during column move + document.onselectstart = function() { return false; }; - var mouseMoveHandler = function (evt) { - //Disable text selection in Chrome during column move - document.onselectstart = function() { return false; }; + moveOccurred = true; - var changeValue = evt.pageX - previousMouseX; - if (!elmCloned && Math.abs(changeValue) > 50) { - cloneElement(); - } - else if (elmCloned) { - moveElement(changeValue); - previousMouseX = evt.pageX; - } - }; - - /* - //Commenting these lines as they are creating trouble with column moving when grid has huge scroll - // On scope destroy, remove the mouse event handlers from the document body - $scope.$on('$destroy', function () { - $document.off('mousemove', mouseMoveHandler); - $document.off('mouseup', mouseUpHandler); - }); - */ - $document.on('mousemove', mouseMoveHandler); - - var mouseUpHandler = function (evt) { - //Re-enable text selection after column move - document.onselectstart = null; - - //Remove the cloned element on mouse up. - if (movingElm) { - movingElm.remove(); - } + var changeValue = event.pageX - previousMouseX; + if (!elmCloned) { + cloneElement(); + } + else if (elmCloned) { + moveElement(changeValue); + previousMouseX = event.pageX; + } + }; - var columns = $scope.grid.columns; - var columnIndex = 0; - for (var i = 0; i < columns.length; i++) { - if (columns[i].colDef.name !== $scope.col.colDef.name) { - columnIndex++; - } - else { - break; - } - } + var upFn = function( event ){ + //Re-enable text selection after column move + document.onselectstart = null; - //Case where column should be moved to a position on its left - if (totalMouseMovement < 0) { - var totalColumnsLeftWidth = 0; - for (var il = columnIndex - 1; il >= 0; il--) { - if (angular.isUndefined(columns[il].colDef.visible) || columns[il].colDef.visible === true) { - totalColumnsLeftWidth += columns[il].drawnWidth || columns[il].width || columns[il].colDef.width; - if (totalColumnsLeftWidth > Math.abs(totalMouseMovement)) { - uiGridMoveColumnService.redrawColumnAtPosition - ($scope.grid, columnIndex, il + 1); - break; - } - } - } - //Case where column should be moved to beginning of the grid. - if (totalColumnsLeftWidth < Math.abs(totalMouseMovement)) { - uiGridMoveColumnService.redrawColumnAtPosition - ($scope.grid, columnIndex, 0); - } - } + //Remove the cloned element on mouse up. + if (movingElm) { + movingElm.remove(); + elmCloned = false; + } + + offAllEvents(); + onDownEvents(); + + if (!moveOccurred){ + return; + } - //Case where column should be moved to a position on its right - else if (totalMouseMovement > 0) { - var totalColumnsRightWidth = 0; - for (var ir = columnIndex + 1; ir < columns.length; ir++) { - if (angular.isUndefined(columns[ir].colDef.visible) || columns[ir].colDef.visible === true) { - totalColumnsRightWidth += columns[ir].drawnWidth || columns[ir].width || columns[ir].colDef.width; - if (totalColumnsRightWidth > totalMouseMovement) { - uiGridMoveColumnService.redrawColumnAtPosition - ($scope.grid, columnIndex, ir - 1); - break; - } - } - } - //Case where column should be moved to end of the grid. - if (totalColumnsRightWidth < totalMouseMovement) { - uiGridMoveColumnService.redrawColumnAtPosition - ($scope.grid, columnIndex, columns.length - 1); - } + var columns = $scope.grid.columns; + var columnIndex = 0; + for (var i = 0; i < columns.length; i++) { + if (columns[i].colDef.name !== $scope.col.colDef.name) { + columnIndex++; + } + else { + break; + } + } + + //Case where column should be moved to a position on its left + if (totalMouseMovement < 0) { + var totalColumnsLeftWidth = 0; + for (var il = columnIndex - 1; il >= 0; il--) { + if (angular.isUndefined(columns[il].colDef.visible) || columns[il].colDef.visible === true) { + totalColumnsLeftWidth += columns[il].drawnWidth || columns[il].width || columns[il].colDef.width; + if (totalColumnsLeftWidth > Math.abs(totalMouseMovement)) { + uiGridMoveColumnService.redrawColumnAtPosition + ($scope.grid, columnIndex, il + 1); + break; } -/* - else if (totalMouseMovement === 0) { - if (uiGridCtrl.grid.options.enableSorting && $scope.col.enableSorting) { - //sort the current column - var add = false; - if (evt.shiftKey) { - add = true; - } - // Sort this column then rebuild the grid's rows - uiGridCtrl.grid.sortColumn($scope.col, add) - .then(function () { - if (uiGridCtrl.columnMenuScope) { - uiGridCtrl.columnMenuScope.hideMenu(); - } - uiGridCtrl.grid.refresh(); - }); - } + } + } + //Case where column should be moved to beginning of the grid. + if (totalColumnsLeftWidth < Math.abs(totalMouseMovement)) { + uiGridMoveColumnService.redrawColumnAtPosition + ($scope.grid, columnIndex, 0); + } + } + + //Case where column should be moved to a position on its right + else if (totalMouseMovement > 0) { + var totalColumnsRightWidth = 0; + for (var ir = columnIndex + 1; ir < columns.length; ir++) { + if (angular.isUndefined(columns[ir].colDef.visible) || columns[ir].colDef.visible === true) { + totalColumnsRightWidth += columns[ir].drawnWidth || columns[ir].width || columns[ir].colDef.width; + if (totalColumnsRightWidth > totalMouseMovement) { + uiGridMoveColumnService.redrawColumnAtPosition + ($scope.grid, columnIndex, ir - 1); + break; } -*/ - $document.off('mousemove', mouseMoveHandler); - $document.off('mouseup', mouseUpHandler); - }; + } + } + //Case where column should be moved to end of the grid. + if (totalColumnsRightWidth < totalMouseMovement) { + uiGridMoveColumnService.redrawColumnAtPosition + ($scope.grid, columnIndex, columns.length - 1); + } + } + }; + + var onDownEvents = function(){ + $contentsElm.on('touchstart', downFn); + $contentsElm.on('mousedown', downFn); + }; + + var offAllEvents = function() { + $contentsElm.off('touchstart', downFn); + $contentsElm.off('mousedown', downFn); + + $document.off('mousemove', moveFn); + $document.off('touchmove', moveFn); + + $document.off('mouseup', upFn); + $document.off('touchend', upFn); + }; + + onDownEvents(); + + + var cloneElement = function () { + elmCloned = true; + + //Cloning header cell and appending to current header cell. + movingElm = $elm.clone(); + $elm.parent().append(movingElm); + + //Left of cloned element should be aligned to original header cell. + movingElm.addClass('movingColumn'); + var movingElementStyles = {}; + var elmLeft; + if (gridUtil.detectBrowser() === 'safari') { + //Correction for Safari getBoundingClientRect, + //which does not correctly compute when there is an horizontal scroll + elmLeft = $elm[0].offsetLeft + $elm[0].offsetWidth - $elm[0].getBoundingClientRect().width; + } + else { + elmLeft = $elm[0].getBoundingClientRect().left; + } + movingElementStyles.left = (elmLeft - gridLeft) + 'px'; + var gridRight = $scope.grid.element[0].getBoundingClientRect().right; + var elmRight = $elm[0].getBoundingClientRect().right; + if (elmRight > gridRight) { + reducedWidth = $scope.col.drawnWidth + (gridRight - elmRight); + movingElementStyles.width = reducedWidth + 'px'; + } + movingElm.css(movingElementStyles); + }; + + var moveElement = function (changeValue) { + //Calculate total column width + var columns = $scope.grid.columns; + var totalColumnWidth = 0; + for (var i = 0; i < columns.length; i++) { + if (angular.isUndefined(columns[i].colDef.visible) || columns[i].colDef.visible === true) { + totalColumnWidth += columns[i].drawnWidth || columns[i].width || columns[i].colDef.width; + } + } + + //Calculate new position of left of column + var currentElmLeft = movingElm[0].getBoundingClientRect().left - 1; + var currentElmRight = movingElm[0].getBoundingClientRect().right; + var newElementLeft; + + newElementLeft = currentElmLeft - gridLeft + changeValue; + newElementLeft = newElementLeft < rightMoveLimit ? newElementLeft : rightMoveLimit; - //Binding the mouseup event handler - $document.on('mouseup', mouseUpHandler); + //Update css of moving column to adjust to new left value or fire scroll in case column has reached edge of grid + if ((currentElmLeft >= gridLeft || changeValue > 0) && (currentElmRight <= rightMoveLimit || changeValue < 0)) { + movingElm.css({visibility: 'visible', 'left': newElementLeft + 'px'}); + } + else if (totalColumnWidth > Math.ceil(uiGridCtrl.grid.gridWidth)) { + changeValue *= 8; + var scrollEvent = new ScrollEvent($scope.col.grid, null, null, 'uiGridHeaderCell.moveElement'); + scrollEvent.x = {pixels: changeValue}; + scrollEvent.grid.scrollContainers('',scrollEvent); + } + + //Calculate total width of columns on the left of the moving column and the mouse movement + var totalColumnsLeftWidth = 0; + for (var il = 0; il < columns.length; il++) { + if (angular.isUndefined(columns[il].colDef.visible) || columns[il].colDef.visible === true) { + if (columns[il].colDef.name !== $scope.col.colDef.name) { + totalColumnsLeftWidth += columns[il].drawnWidth || columns[il].width || columns[il].colDef.width; + } + else { + break; + } } } - }); + if ($scope.newScrollLeft === undefined) { + totalMouseMovement += changeValue; + } + else { + totalMouseMovement = $scope.newScrollLeft + newElementLeft - totalColumnsLeftWidth; + } + + //Increase width of moving column, in case the rightmost column was moved and its width was + //decreased because of overflow + if (reducedWidth < $scope.col.drawnWidth) { + reducedWidth += Math.abs(changeValue); + movingElm.css({'width': reducedWidth + 'px'}); + } + }; } } }; diff --git a/src/features/move-columns/less/colMovable.less b/src/features/move-columns/less/colMovable.less index 54ed21f3bd..8b0efc5b16 100644 --- a/src/features/move-columns/less/colMovable.less +++ b/src/features/move-columns/less/colMovable.less @@ -1,12 +1,12 @@ @import '../../../less/variables'; .movingColumn { - - position: fixed; + position: absolute; + top: 0; border: 1px solid @borderColor; box-shadow: inset 0 0 14px rgba(0, 0, 0, 0.2); .ui-grid-icon-angle-down { display: none; } -} \ No newline at end of file +} diff --git a/src/features/move-columns/test/column-movable.spec.js b/src/features/move-columns/test/column-movable.spec.js index c2d0e32f1f..d8bb84a599 100644 --- a/src/features/move-columns/test/column-movable.spec.js +++ b/src/features/move-columns/test/column-movable.spec.js @@ -1,6 +1,6 @@ describe('ui.grid.moveColumns', function () { - var scope, element, timeout, gridUtil; + var scope, element, timeout, gridUtil, document; var data = [ { "name": "Ethel Price", "gender": "female", "age": 25, "company": "Enersol", phone: '111'}, @@ -11,12 +11,13 @@ describe('ui.grid.moveColumns', function () { beforeEach(module('ui.grid.moveColumns')); - beforeEach(inject(function (_$compile_, $rootScope, $timeout, _gridUtil_) { + beforeEach(inject(function (_$compile_, $rootScope, $timeout, _gridUtil_, $document) { var $compile = _$compile_; scope = $rootScope; timeout = $timeout; gridUtil = _gridUtil_; + document = $document; scope.gridOptions = {}; @@ -103,37 +104,39 @@ describe('ui.grid.moveColumns', function () { expect(functionCalled).toBe(true); }); - // this test doesn't currently pass: TODO: fix - xit('expect column to move right when dragged right', function () { - var event = jQuery.Event("mousedown"); - event.toElement = {className: '.ui-grid-header-cell'}; - var columnHeader = angular.element(element.find('.ui-grid-header-cell')[0]); + it('expect column to move right when dragged right', function () { + var event = jQuery.Event("mousedown", { + pageX: 0 + }); + var columnHeader = angular.element(element.find('.ui-grid-cell-contents')[0]); columnHeader.trigger(event); event = jQuery.Event("mousemove", { - pageX: 250 + pageX: 200 }); - columnHeader.trigger(event); + document.trigger(event); + document.trigger(event); event = jQuery.Event("mouseup"); - columnHeader.trigger(event); + document.trigger(event); expect(scope.grid.columns[0].name).toBe('gender'); - expect(scope.grid.columns[1].name).toBe('name'); - expect(scope.grid.columns[2].name).toBe('age'); + expect(scope.grid.columns[1].name).toBe('age'); + expect(scope.grid.columns[2].name).toBe('name'); expect(scope.grid.columns[3].name).toBe('company'); expect(scope.grid.columns[4].name).toBe('phone'); }); - // this test doesn't currently pass: TODO: fix - xit('expect column to move left when dragged left', function () { - var event = jQuery.Event("mousedown"); - event.toElement = {className: '.ui-grid-header-cell'}; - var columnHeader = angular.element(element.find('.ui-grid-header-cell')[1]); + it('expect column to move left when dragged left', function () { + var event = jQuery.Event("mousedown", { + pageX: 0 + }); + var columnHeader = angular.element(element.find('.ui-grid-cell-contents')[1]); columnHeader.trigger(event); event = jQuery.Event("mousemove", { - pageX: -250 + pageX: -200 }); - columnHeader.trigger(event); + document.trigger(event); + document.trigger(event); event = jQuery.Event("mouseup"); - columnHeader.trigger(event); + document.trigger(event); expect(scope.grid.columns[0].name).toBe('gender'); expect(scope.grid.columns[1].name).toBe('name'); expect(scope.grid.columns[2].name).toBe('age'); @@ -141,19 +144,19 @@ describe('ui.grid.moveColumns', function () { expect(scope.grid.columns[4].name).toBe('phone'); }); - // this test doesn't currently pass: TODO: fix - xit('expect column movement to not happen if enableColumnMoving is false', function () { - var event = jQuery.Event("mousedown"); - event.toElement = {className: '.ui-grid-header-cell'}; - var columnHeader = angular.element(element.find('.ui-grid-header-cell')[3]); + it('expect column movement to not happen if enableColumnMoving is false', function () { + var event = jQuery.Event("mousedown", { + pageX: 0 + }); + var columnHeader = angular.element(element.find('.ui-grid-cell-contents')[3]); columnHeader.trigger(event); event = jQuery.Event("mousemove", { - pageX: 75 + pageX: 200 }); - columnHeader.trigger(event); + document.trigger(event); + document.trigger(event); event = jQuery.Event("mouseup"); - columnHeader.trigger(event); - scope.grid.refresh(); + document.trigger(event); expect(scope.grid.columns[0].name).toBe('name'); expect(scope.grid.columns[1].name).toBe('gender'); expect(scope.grid.columns[2].name).toBe('age'); diff --git a/src/features/pagination/js/pagination.js b/src/features/pagination/js/pagination.js index 8b2032a297..fda52b8926 100644 --- a/src/features/pagination/js/pagination.js +++ b/src/features/pagination/js/pagination.js @@ -131,7 +131,8 @@ grid.api.registerEventsFromObject(publicApi.events); grid.api.registerMethodsFromObject(publicApi.methods); - grid.registerRowsProcessor(function (renderableRows) { + + var processPagination = function( renderableRows ){ if (grid.options.useExternalPagination || !grid.options.enablePagination) { return renderableRows; } @@ -148,7 +149,9 @@ firstRow = (currentPage - 1) * pageSize; } return visibleRows.slice(firstRow, firstRow + pageSize); - }); + }; + + grid.registerRowsProcessor(processPagination, 900 ); }, defaultGridOptions: function (gridOptions) { diff --git a/src/features/pagination/test/pagination.spec,js b/src/features/pagination/test/pagination.spec.js similarity index 79% rename from src/features/pagination/test/pagination.spec,js rename to src/features/pagination/test/pagination.spec.js index 5f4cc3f9fe..0ad6952511 100644 --- a/src/features/pagination/test/pagination.spec,js +++ b/src/features/pagination/test/pagination.spec.js @@ -4,12 +4,14 @@ describe('ui.grid.pagination uiGridPaginationService', function () { var gridApi; var gridElement; var $rootScope; + var $timeout; beforeEach(module('ui.grid')); beforeEach(module('ui.grid.pagination')); - beforeEach(inject(function (_$rootScope_, $compile) { + beforeEach(inject(function (_$rootScope_, _$timeout_, $compile) { $rootScope = _$rootScope_; + $timeout = _$timeout_; $rootScope.gridOptions = { columnDefs: [ @@ -90,6 +92,7 @@ describe('ui.grid.pagination uiGridPaginationService', function () { it('displays page 2 if I call nextPage()', function () { gridApi.pagination.nextPage(); $rootScope.$digest(); + $timeout.flush(); var gridRows = gridElement.find('div.ui-grid-row'); @@ -106,6 +109,7 @@ describe('ui.grid.pagination uiGridPaginationService', function () { it('displays only 6 rows on page 3', function () { gridApi.pagination.seek(3); $rootScope.$digest(); + $timeout.flush(); var gridRows = gridElement.find('div.ui-grid-row'); @@ -145,29 +149,29 @@ describe('ui.grid.pagination uiGridPaginationService', function () { it('paginates correctly on a sorted grid', function() { gridApi.grid.sortColumn(gridApi.grid.columns[1]).then(function () { - gridApi.grid.refresh(); + $rootScope.$digest(); + $timeout.flush(); + + var gridRows = gridElement.find('div.ui-grid-row'); + expect(gridApi.pagination.getPage()).toBe(1); + expect(gridRows.eq(0).find('div.ui-grid-cell').eq(1).text()).toBe('A'); + expect(gridRows.eq(1).find('div.ui-grid-cell').eq(1).text()).toBe('B'); + expect(gridRows.eq(2).find('div.ui-grid-cell').eq(1).text()).toBe('C'); + expect(gridRows.eq(3).find('div.ui-grid-cell').eq(1).text()).toBe('D'); + expect(gridRows.eq(4).find('div.ui-grid-cell').eq(1).text()).toBe('E'); + expect(gridRows.eq(5).find('div.ui-grid-cell').eq(1).text()).toBe('F'); + expect(gridRows.eq(6).find('div.ui-grid-cell').eq(1).text()).toBe('G'); + expect(gridRows.eq(7).find('div.ui-grid-cell').eq(1).text()).toBe('H'); + expect(gridRows.eq(8).find('div.ui-grid-cell').eq(1).text()).toBe('I'); + expect(gridRows.eq(9).find('div.ui-grid-cell').eq(1).text()).toBe('J'); + + gridApi.pagination.nextPage(); + $rootScope.$digest(); + + gridRows = gridElement.find('div.ui-grid-row'); + expect(gridApi.pagination.getPage()).toBe(2); + expect(gridRows.eq(0).find('div.ui-grid-cell').eq(1).text()).toBe('K'); }); - $rootScope.$digest(); - - var gridRows = gridElement.find('div.ui-grid-row'); - expect(gridApi.pagination.getPage()).toBe(1); - expect(gridRows.eq(0).find('div.ui-grid-cell').eq(1).text()).toBe('A'); - expect(gridRows.eq(1).find('div.ui-grid-cell').eq(1).text()).toBe('B'); - expect(gridRows.eq(2).find('div.ui-grid-cell').eq(1).text()).toBe('C'); - expect(gridRows.eq(3).find('div.ui-grid-cell').eq(1).text()).toBe('D'); - expect(gridRows.eq(4).find('div.ui-grid-cell').eq(1).text()).toBe('E'); - expect(gridRows.eq(5).find('div.ui-grid-cell').eq(1).text()).toBe('F'); - expect(gridRows.eq(6).find('div.ui-grid-cell').eq(1).text()).toBe('G'); - expect(gridRows.eq(7).find('div.ui-grid-cell').eq(1).text()).toBe('H'); - expect(gridRows.eq(8).find('div.ui-grid-cell').eq(1).text()).toBe('I'); - expect(gridRows.eq(9).find('div.ui-grid-cell').eq(1).text()).toBe('J'); - - gridApi.pagination.nextPage(); - $rootScope.$digest(); - - gridRows = gridElement.find('div.ui-grid-row'); - expect(gridApi.pagination.getPage()).toBe(2); - expect(gridRows.eq(0).find('div.ui-grid-cell').eq(1).text()).toBe('K'); }); }); }); diff --git a/src/features/pinning/js/pinning.js b/src/features/pinning/js/pinning.js index d357a73c14..bcd5b5c1ce 100644 --- a/src/features/pinning/js/pinning.js +++ b/src/features/pinning/js/pinning.js @@ -16,7 +16,15 @@ var module = angular.module('ui.grid.pinning', ['ui.grid']); - module.service('uiGridPinningService', ['gridUtil', 'GridRenderContainer', 'i18nService', function (gridUtil, GridRenderContainer, i18nService) { + module.constant('uiGridPinningConstants', { + container: { + LEFT: 'left', + RIGHT: 'right', + NONE: '' + } + }); + + module.service('uiGridPinningService', ['gridUtil', 'GridRenderContainer', 'i18nService', 'uiGridPinningConstants', function (gridUtil, GridRenderContainer, i18nService, uiGridPinningConstants) { var service = { initializeGrid: function (grid) { @@ -24,6 +32,54 @@ // Register a column builder to add new menu items for pinning left and right grid.registerColumnBuilder(service.pinningColumnBuilder); + + /** + * @ngdoc object + * @name ui.grid.pinning.api:PublicApi + * + * @description Public Api for pinning feature + */ + var publicApi = { + events: { + pinning: { + /** + * @ngdoc event + * @name columnPin + * @eventOf ui.grid.pinning.api:PublicApi + * @description raised when column pin state has changed + *
+               *   gridApi.pinning.on.columnPinned(scope, function(colDef){})
+               * 
+ * @param {object} colDef the column that was changed + * @param {string} container the render container the column is in ('left', 'right', '') + */ + columnPinned: function(colDef, container) { + } + } + }, + methods: { + pinning: { + /** + * @ngdoc function + * @name pinColumn + * @methodOf ui.grid.pinning.api:PublicApi + * @description pin column left, right, or none + *
+               *   gridApi.pinning.pinColumn(col, uiGridPinningConstants.container.LEFT)
+               * 
+ * @param {gridColumn} col the column being pinned + * @param {string} container one of the recognised types + * from uiGridPinningConstants + */ + pinColumn: function(col, container) { + service.pinColumn(grid, col, container); + } + } + } + }; + + grid.api.registerEventsFromObject(publicApi.events); + grid.api.registerMethodsFromObject(publicApi.methods); }, defaultGridOptions: function (gridOptions) { @@ -84,38 +140,12 @@ *
Defaults to false */ if (colDef.pinnedLeft) { - if (col.width === '*') { - // Need to refresh so the width can be calculated. - col.grid.refresh() - .then(function () { - col.renderContainer = 'left'; - // Need to calculate the width. If col.drawnWidth is used instead then the width - // will be 100% if it's the first column, 50% if it's the second etc. - col.width = col.grid.canvasWidth / col.grid.columns.length; - col.grid.createLeftContainer(); - }); - } - else { - col.renderContainer = 'left'; - col.grid.createLeftContainer(); - } + col.renderContainer = 'left'; + col.grid.createLeftContainer(); } else if (colDef.pinnedRight) { - if (col.width === '*') { - // Need to refresh so the width can be calculated. - col.grid.refresh() - .then(function () { - col.renderContainer = 'right'; - // Need to calculate the width. If col.drawnWidth is used instead then the width - // will be 100% if it's the first column, 50% if it's the second etc. - col.width = col.grid.canvasWidth / col.grid.columns.length; - col.grid.createRightContainer(); - }); - } - else { - col.renderContainer = 'right'; - col.grid.createRightContainer(); - } + col.renderContainer = 'right'; + col.grid.createRightContainer(); } if (!colDef.enablePinning) { @@ -130,16 +160,7 @@ return typeof(this.context.col.renderContainer) === 'undefined' || !this.context.col.renderContainer || this.context.col.renderContainer !== 'left'; }, action: function () { - this.context.col.renderContainer = 'left'; - this.context.col.width = this.context.col.drawnWidth; - this.context.col.grid.createLeftContainer(); - - // Need to call refresh twice; once to move our column over to the new render container and then - // a second time to update the grid viewport dimensions with our adjustments - col.grid.refresh() - .then(function () { - col.grid.refresh(); - }); + service.pinColumn(this.context.col.grid, this.context.col, uiGridPinningConstants.container.LEFT); } }; @@ -151,17 +172,7 @@ return typeof(this.context.col.renderContainer) === 'undefined' || !this.context.col.renderContainer || this.context.col.renderContainer !== 'right'; }, action: function () { - this.context.col.renderContainer = 'right'; - this.context.col.width = this.context.col.drawnWidth; - this.context.col.grid.createRightContainer(); - - - // Need to call refresh twice; once to move our column over to the new render container and then - // a second time to update the grid viewport dimensions with our adjustments - col.grid.refresh() - .then(function () { - col.grid.refresh(); - }); + service.pinColumn(this.context.col.grid, this.context.col, uiGridPinningConstants.container.RIGHT); } }; @@ -173,14 +184,7 @@ return typeof(this.context.col.renderContainer) !== 'undefined' && this.context.col.renderContainer !== null && this.context.col.renderContainer !== 'body'; }, action: function () { - this.context.col.renderContainer = null; - - // Need to call refresh twice; once to move our column over to the new render container and then - // a second time to update the grid viewport dimensions with our adjustments - col.grid.refresh() - .then(function () { - col.grid.refresh(); - }); + service.pinColumn(this.context.col.grid, this.context.col, uiGridPinningConstants.container.UNPIN); } }; @@ -193,6 +197,26 @@ if (!gridUtil.arrayContainsObjectWithProperty(col.menuItems, 'name', 'ui.grid.pinning.unpin')) { col.menuItems.push(removePinAction); } + }, + + pinColumn: function(grid, col, container) { + if (container === uiGridPinningConstants.container.NONE) { + col.renderContainer = null; + } + else { + col.renderContainer = container; + if (container === uiGridPinningConstants.container.LEFT) { + grid.createLeftContainer(); + } + else if (container === uiGridPinningConstants.container.RIGHT) { + grid.createRightContainer(); + } + } + + grid.refresh() + .then(function() { + grid.api.pinning.raise.columnPinned( col.colDef, container ); + }); } }; diff --git a/src/features/pinning/less/pinning.less b/src/features/pinning/less/pinning.less index 9081e6eb99..d1340ce7ad 100644 --- a/src/features/pinning/less/pinning.less +++ b/src/features/pinning/less/pinning.less @@ -1,21 +1,32 @@ @import '../../../less/variables'; .ui-grid-pinned-container { - // position: absolute; - float: left; + position: absolute; + display: inline; + top: 0; + + &.ui-grid-pinned-container-left { + float: left; + left: 0; + } + + &.ui-grid-pinned-container-right { + float: right; + right: 0; + } &.ui-grid-pinned-container-left .ui-grid-header-cell:last-child { box-sizing: border-box; border-right: @gridBorderWidth solid; border-width: @gridBorderWidth; - border-color: darken(@headerVerticalBarColor, 15%); + border-right-color: darken(@headerVerticalBarColor, 15%); } &.ui-grid-pinned-container-left .ui-grid-cell:last-child { box-sizing: border-box; border-right: @gridBorderWidth solid; border-width: @gridBorderWidth; - border-color: darken(@verticalBarColor, 15%); + border-right-color: darken(@verticalBarColor, 15%); } &.ui-grid-pinned-container-left .ui-grid-header-cell:not(:last-child) .ui-grid-vertical-bar, .ui-grid-cell:not(:last-child) .ui-grid-vertical-bar { @@ -41,14 +52,14 @@ box-sizing: border-box; border-left: @gridBorderWidth solid; border-width: @gridBorderWidth; - border-color: darken(@headerVerticalBarColor, 15%); + border-left-color: darken(@headerVerticalBarColor, 15%); } &.ui-grid-pinned-container-right .ui-grid-cell:first-child { box-sizing: border-box; border-left: @gridBorderWidth solid; border-width: @gridBorderWidth; - border-color: darken(@verticalBarColor, 15%); + border-left-color: darken(@verticalBarColor, 15%); } &.ui-grid-pinned-container-right .ui-grid-header-cell:not(:first-child) .ui-grid-vertical-bar, .ui-grid-cell:not(:first-child) .ui-grid-vertical-bar { @@ -71,5 +82,5 @@ } .ui-grid-render-container-body { - float: left; -} \ No newline at end of file + // float: left; +} diff --git a/src/features/pinning/test/uiGridPinningService.spec.js b/src/features/pinning/test/uiGridPinningService.spec.js index ac66b19f94..3c518e20b7 100644 --- a/src/features/pinning/test/uiGridPinningService.spec.js +++ b/src/features/pinning/test/uiGridPinningService.spec.js @@ -1,14 +1,16 @@ /* global _ */ describe('ui.grid.pinning uiGridPinningService', function () { var uiGridPinningService; + var uiGridPinningConstants; var gridClassFactory; var grid; var GridRenderContainer; beforeEach(module('ui.grid.pinning')); - beforeEach(inject(function (_uiGridPinningService_,_gridClassFactory_, $templateCache, _GridRenderContainer_) { + beforeEach(inject(function (_uiGridPinningService_,_gridClassFactory_, $templateCache, _GridRenderContainer_, _uiGridPinningConstants_) { uiGridPinningService = _uiGridPinningService_; + uiGridPinningConstants = _uiGridPinningConstants_; gridClassFactory = _gridClassFactory_; GridRenderContainer = _GridRenderContainer_; @@ -131,4 +133,73 @@ describe('ui.grid.pinning uiGridPinningService', function () { }); + describe('pinColumn', function() { + + var previousWidth; + + beforeEach(function() { + spyOn(grid, 'createLeftContainer').andCallThrough(); + spyOn(grid, 'createRightContainer').andCallThrough(); + previousWidth = grid.columns[0].drawnWidth; + }); + + describe('left', function() { + + beforeEach(function() { + grid.api.pinning.pinColumn(grid.columns[0], uiGridPinningConstants.container.LEFT); + }); + + it('should set renderContainer to be left', function(){ + expect(grid.columns[0].renderContainer).toEqual('left'); + }); + + it('should call createLeftContainer', function() { + expect(grid.createLeftContainer).toHaveBeenCalled(); + }); + + it('should set width based on previous setting', function() { + expect(grid.width).toEqual(previousWidth); + }); + + }); + + describe('right', function() { + + beforeEach(function() { + grid.api.pinning.pinColumn(grid.columns[0], uiGridPinningConstants.container.RIGHT); + }); + + it('should set renderContainer to be right', function(){ + expect(grid.columns[0].renderContainer).toEqual('right'); + }); + + it('should call createLeftContainer', function() { + expect(grid.createRightContainer).toHaveBeenCalled(); + }); + + it('should set width based on previous setting', function() { + expect(grid.width).toEqual(previousWidth); + }); + + }); + + describe('none', function() { + + beforeEach(function() { + grid.api.pinning.pinColumn(grid.columns[0], uiGridPinningConstants.container.NONE); + }); + + it('should set renderContainer to be null', function(){ + expect(grid.columns[0].renderContainer).toBeNull(); + }); + + it('should NOT call either container creation methods', function() { + expect(grid.createLeftContainer).not.toHaveBeenCalled(); + expect(grid.createRightContainer).not.toHaveBeenCalled(); + }); + + }); + + }); + }); \ No newline at end of file diff --git a/src/features/resize-columns/js/ui-grid-column-resizer.js b/src/features/resize-columns/js/ui-grid-column-resizer.js index 26776bc988..88c73bc675 100644 --- a/src/features/resize-columns/js/ui-grid-column-resizer.js +++ b/src/features/resize-columns/js/ui-grid-column-resizer.js @@ -93,7 +93,11 @@ fireColumnSizeChanged: function (grid, colDef, deltaChange) { $timeout(function () { - grid.api.colResizable.raise.columnSizeChanged(colDef, deltaChange); + if ( grid.api.colResizable ){ + grid.api.colResizable.raise.columnSizeChanged(colDef, deltaChange); + } else { + gridUtil.logError("The resizeable api is not registered, this may indicate that you've included the module but not added the 'ui-grid-resize-columns' directive to your grid definition. Cannot raise any events."); + } }); }, @@ -288,19 +292,6 @@ module.directive('uiGridColumnResizer', ['$document', 'gridUtil', 'uiGridConstants', 'uiGridResizeColumnsService', function ($document, gridUtil, uiGridConstants, uiGridResizeColumnsService) { var resizeOverlay = angular.element('
'); - var downEvent, upEvent, moveEvent; - - if (gridUtil.isTouchEnabled()) { - downEvent = 'touchstart'; - upEvent = 'touchend'; - moveEvent = 'touchmove'; - } - else { - downEvent = 'mousedown'; - upEvent = 'mouseup'; - moveEvent = 'mousemove'; - } - var resizer = { priority: 0, scope: { @@ -328,22 +319,6 @@ $elm.addClass('right'); } - // Resize all the other columns around col - function resizeAroundColumn(col) { - // Get this column's render container - var renderContainer = col.getRenderContainer(); - - renderContainer.visibleColumnCache.forEach(function (column) { - // Skip the column we just resized - if (column === col) { return; } - - var colDef = column.colDef; - if (!colDef.width || (angular.isString(colDef.width) && (colDef.width.indexOf('*') !== -1 || colDef.width.indexOf('%') !== -1))) { - column.width = column.drawnWidth; - } - }); - } - // Build the columns then refresh the grid canvas // takes an argument representing the diff along the X-axis that the resize had function buildColumnsAndRefresh(xDiff) { @@ -374,7 +349,15 @@ } - function mousemove(event, args) { + /* + * Our approach to event handling aims to deal with both touch devices and mouse devices + * We register down handlers on both touch and mouse. When a touchstart or mousedown event + * occurs, we register the corresponding touchmove/touchend, or mousemove/mouseend events. + * + * This way we can listen for both without worrying about the fact many touch devices also emulate + * mouse events - basically whichever one we hear first is what we'll go with. + */ + function moveFunction(event, args) { if (event.originalEvent) { event = event.originalEvent; } event.preventDefault(); @@ -409,7 +392,7 @@ } - function mouseup(event, args) { + function upFunction(event, args) { if (event.originalEvent) { event = event.originalEvent; } event.preventDefault(); @@ -422,8 +405,10 @@ var xDiff = x - startX; if (xDiff === 0) { - $document.off(upEvent, mouseup); - $document.off(moveEvent, mousemove); + // no movement, so just reset event handlers, including turning back on both + // down events - we turned one off when this event started + offAllEvents(); + onDownEvents(); return; } @@ -440,19 +425,18 @@ // check we're not outside the allowable bounds for this column col.width = constrainWidth(col, newWidth); - // All other columns because fixed to their drawn width, if they aren't already - resizeAroundColumn(col); - buildColumnsAndRefresh(xDiff); uiGridResizeColumnsService.fireColumnSizeChanged(uiGridCtrl.grid, col.colDef, xDiff); - $document.off(upEvent, mouseup); - $document.off(moveEvent, mousemove); + // stop listening of up and move events - wait for next down + // reset the down events - we will have turned one off when this event started + offAllEvents(); + onDownEvents(); } - $elm.on(downEvent, function(event, args) { + var downFunction = function(event, args) { if (event.originalEvent) { event = event.originalEvent; } event.stopPropagation(); @@ -469,14 +453,40 @@ // Place the resizer overlay at the start position resizeOverlay.css({ left: startX }); - // Add handlers for mouse move and up events - $document.on(upEvent, mouseup); - $document.on(moveEvent, mousemove); - }); - + // Add handlers for move and up events - if we were mousedown then we listen for mousemove and mouseup, if + // we were touchdown then we listen for touchmove and touchup. Also remove the handler for the equivalent + // down event - so if we're touchdown, then remove the mousedown handler until this event is over, if we're + // mousedown then remove the touchdown handler until this event is over, this avoids processing duplicate events + if ( event.type === 'touchstart' ){ + $document.on('touchend', upFunction); + $document.on('touchmove', moveFunction); + $elm.off('mousedown', downFunction); + } else { + $document.on('mouseup', upFunction); + $document.on('mousemove', moveFunction); + $elm.off('touchstart', downFunction); + } + }; + + var onDownEvents = function() { + $elm.on('mousedown', downFunction); + $elm.on('touchstart', downFunction); + }; + + var offAllEvents = function() { + $document.off('mouseup', upFunction); + $document.off('touchend', upFunction); + $document.off('mousemove', moveFunction); + $document.off('touchmove', moveFunction); + $elm.off('mousedown', downFunction); + $elm.off('touchstart', downFunction); + }; + + onDownEvents(); + // On doubleclick, resize to fit all rendered cells - $elm.on('dblclick', function(event, args) { + var dblClickFn = function(event, args){ event.stopPropagation(); var col = uiGridResizeColumnsService.findTargetCol($scope.col, $scope.position, rtlMultiplier); @@ -527,19 +537,14 @@ // check we're not outside the allowable bounds for this column col.width = constrainWidth(col, maxWidth); - // All other columns because fixed to their drawn width, if they aren't already - resizeAroundColumn(col); - buildColumnsAndRefresh(xDiff); - uiGridResizeColumnsService.fireColumnSizeChanged(uiGridCtrl.grid, col.colDef, xDiff); - }); + uiGridResizeColumnsService.fireColumnSizeChanged(uiGridCtrl.grid, col.colDef, xDiff); }; + $elm.on('dblclick', dblClickFn); $elm.on('$destroy', function() { - $elm.off(downEvent); - $elm.off('dblclick'); - $document.off(moveEvent, mousemove); - $document.off(upEvent, mouseup); + $elm.off('dblclick', dblClickFn); + offAllEvents(); }); } }; @@ -547,4 +552,4 @@ return resizer; }]); -})(); \ No newline at end of file +})(); diff --git a/src/features/resize-columns/templates/columnResizer.html b/src/features/resize-columns/templates/columnResizer.html index 78e8e64436..597648d597 100644 --- a/src/features/resize-columns/templates/columnResizer.html +++ b/src/features/resize-columns/templates/columnResizer.html @@ -4,5 +4,6 @@ class="ui-grid-column-resizer" col="col" position="right" - render-index="renderIndex"> + render-index="renderIndex" + unselectable="on">
\ No newline at end of file diff --git a/src/features/row-edit/js/gridRowEdit.js b/src/features/row-edit/js/gridRowEdit.js index 99bd861362..b2f453355d 100644 --- a/src/features/row-edit/js/gridRowEdit.js +++ b/src/features/row-edit/js/gridRowEdit.js @@ -218,6 +218,11 @@ return function() { gridRow.isSaving = true; + if ( gridRow.rowEditSavePromise ){ + // don't save the row again if it's already saving - that causes stale object exceptions + return gridRow.rowEditSavePromise; + } + var promise = grid.api.rowEdit.raise.saveRow( gridRow.entity ); if ( gridRow.rowEditSavePromise ){ @@ -270,6 +275,7 @@ delete gridRow.isDirty; delete gridRow.isError; delete gridRow.rowEditSaveTimer; + delete gridRow.rowEditSavePromise; self.removeRow( grid.rowEdit.errorRows, gridRow ); self.removeRow( grid.rowEdit.dirtyRows, gridRow ); }; @@ -290,6 +296,7 @@ return function() { delete gridRow.isSaving; delete gridRow.rowEditSaveTimer; + delete gridRow.rowEditSavePromise; gridRow.isError = true; @@ -314,7 +321,11 @@ * @param {GridRow} gridRow the row that should be removed */ removeRow: function( rowArray, removeGridRow ){ - angular.forEach( rowArray, function( gridRow, index ){ + if (typeof(rowArray) === 'undefined' || rowArray === null){ + return; + } + + rowArray.forEach( function( gridRow, index ){ if ( gridRow.uid === removeGridRow.uid ){ rowArray.splice( index, 1); } @@ -333,7 +344,7 @@ */ isRowPresent: function( rowArray, removeGridRow ){ var present = false; - angular.forEach( rowArray, function( gridRow, index ){ + rowArray.forEach( function( gridRow, index ){ if ( gridRow.uid === removeGridRow.uid ){ present = true; } @@ -359,7 +370,7 @@ */ flushDirtyRows: function(grid){ var promises = []; - angular.forEach(grid.rowEdit.dirtyRows, function( gridRow ){ + grid.rowEdit.dirtyRows.forEach( function( gridRow ){ service.saveRow( grid, gridRow )(); promises.push( gridRow.rowEditSavePromise ); }); diff --git a/src/features/row-edit/test/uiGridRowEditService.spec.js b/src/features/row-edit/test/uiGridRowEditService.spec.js index 1fe2305f14..2a61fd1bf2 100644 --- a/src/features/row-edit/test/uiGridRowEditService.spec.js +++ b/src/features/row-edit/test/uiGridRowEditService.spec.js @@ -341,6 +341,9 @@ describe('ui.grid.edit uiGridRowEditService', function () { expect( grid.rows[0].isError ).toEqual(true); expect( grid.rowEdit.dirtyRows.length ).toEqual(1); expect( grid.rowEdit.errorRows.length ).toEqual(1); + + $rootScope.$apply(); + expect( grid.rows[0].rowEditSavePromise ).not.toEqual(undefined, 'save promise should be set'); promise.resolve(1); $rootScope.$apply(); @@ -350,6 +353,7 @@ describe('ui.grid.edit uiGridRowEditService', function () { expect( grid.rows[0].isError ).toEqual(undefined); expect( grid.rowEdit.dirtyRows.length ).toEqual(0); expect( grid.rowEdit.errorRows.length ).toEqual(0); + expect( grid.rowEdit.rowEditSavePromise ).toEqual(undefined); }); it( 'saveRow on dirty row, promise rejected so goes to error state', function() { @@ -367,6 +371,9 @@ describe('ui.grid.edit uiGridRowEditService', function () { expect( grid.rows[0].isError ).toEqual(undefined); expect( grid.rowEdit.dirtyRows.length ).toEqual(1); expect( grid.rowEdit.errorRows ).toEqual(undefined); + + $rootScope.$apply(); + expect( grid.rows[0].rowEditSavePromise ).not.toEqual(undefined, 'save promise should be set'); promise.reject(); $rootScope.$apply(); @@ -376,6 +383,7 @@ describe('ui.grid.edit uiGridRowEditService', function () { expect( grid.rows[0].isError ).toEqual(true); expect( grid.rowEdit.dirtyRows.length ).toEqual(1); expect( grid.rowEdit.errorRows.length ).toEqual(1); + expect( grid.rowEdit.rowEditSavePromise ).toEqual(undefined); }); }); @@ -434,6 +442,45 @@ describe('ui.grid.edit uiGridRowEditService', function () { expect( failure ).toEqual(false); }); + it( 'one dirty rows, already saving, doesn\'t call save again', function() { + var promises = [$q.defer()]; + var promiseCounter = 0; + var success = false; + var failure = false; + + grid.rows[0].isDirty = true; + + grid.rowEdit.dirtyRows = [ grid.rows[0] ]; + + grid.api.rowEdit.on.saveRow( $scope, function( rowEntity ){ + grid.api.rowEdit.setSavePromise( rowEntity, promises[promiseCounter].promise); + promiseCounter++; + }); + + // set row saving + uiGridRowEditService.saveRow( grid, grid.rows[0] )(); + + expect( grid.rows[0].isSaving ).toEqual(true); + expect( grid.rowEdit.dirtyRows.length ).toEqual(1); + expect( promiseCounter ).toEqual(1); + + // flush dirty rows, expect no new promise + var overallPromise = uiGridRowEditService.flushDirtyRows( grid ); + overallPromise.then( function() { success = true; }, function() { failure = true; }); + + expect( grid.rows[0].isSaving ).toEqual(true); + expect( grid.rowEdit.dirtyRows.length ).toEqual(1); + expect( promiseCounter ).toEqual(1); + + promises[0].resolve(1); + $rootScope.$apply(); + + expect( grid.rows[0].isSaving ).toEqual(undefined); + expect( grid.rows[0].isDirty ).toEqual(undefined); + expect( grid.rowEdit.dirtyRows.length ).toEqual(0); + expect( success ).toEqual(true); + }); + it( 'three dirty rows, one save fails', function() { var promises = [$q.defer(), $q.defer(), $q.defer()]; var promiseCounter = 0; diff --git a/src/features/saveState/js/saveState.js b/src/features/saveState/js/saveState.js index 2aaf688a3b..dc2985c6fb 100644 --- a/src/features/saveState/js/saveState.js +++ b/src/features/saveState/js/saveState.js @@ -8,19 +8,19 @@ * * # ui.grid.saveState * This module provides the ability to save the grid state, and restore - * it when the user returns to the page. - * - * No UI is provided, the caller should provide their own UI/buttons + * it when the user returns to the page. + * + * No UI is provided, the caller should provide their own UI/buttons * as appropriate. Usually the navigate events would be used to save * the grid state and restore it. - * + * *
*
* *
*/ - var module = angular.module('ui.grid.saveState', ['ui.grid', 'ui.grid.selection', 'ui.grid.cellNav']); + var module = angular.module('ui.grid.saveState', ['ui.grid', 'ui.grid.selection', 'ui.grid.cellNav', 'ui.grid.grouping', 'ui.grid.pinning', 'ui.grid.treeView']); /** * @ngdoc object @@ -67,7 +67,7 @@ * @ngdoc function * @name save * @methodOf ui.grid.saveState.api:PublicApi - * @description Packages the current state of the grid into + * @description Packages the current state of the grid into * an object, and provides it to the user for saving * @returns {object} the state as a javascript object that can be saved */ @@ -92,7 +92,7 @@ grid.api.registerEventsFromObject(publicApi.events); grid.api.registerMethodsFromObject(publicApi.methods); - + }, defaultGridOptions: function (gridOptions) { @@ -101,7 +101,7 @@ * @ngdoc object * @name ui.grid.saveState.api:GridOptions * - * @description GridOptions for saveState feature, these are available to be + * @description GridOptions for saveState feature, these are available to be * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions} */ /** @@ -118,7 +118,7 @@ * @ngdoc object * @name saveOrder * @propertyOf ui.grid.saveState.api:GridOptions - * @description Save the current column order. Note that unless + * @description Restore the current column order. Note that unless * you've provided the user with some way to reorder their columns (for * example the move columns feature), this makes little sense. *
Defaults to true @@ -130,12 +130,12 @@ * @propertyOf ui.grid.saveState.api:GridOptions * @description Save the current scroll position. Note that this * is saved as the percentage of the grid scrolled - so if your - * user returns to a grid with a significantly different number of - * rows (perhaps some data has been deleted) then the scroll won't + * user returns to a grid with a significantly different number of + * rows (perhaps some data has been deleted) then the scroll won't * actually show the same rows as before. If you want to scroll to * a specific row then you should instead use the saveFocus option, which * is the default. - * + * * Note that this element will only be saved if the cellNav feature is * enabled *
Defaults to false @@ -148,20 +148,20 @@ * @description Save the current focused cell. On returning * to this focused cell we'll also scroll. This option is * preferred to the saveScroll option, so is set to true by - * default. If saveScroll is set to true then this option will - * be disabled. - * - * By default this option saves the current row number and column + * default. If saveScroll is set to true then this option will + * be disabled. + * + * By default this option saves the current row number and column * number, and returns to that row and column. However, if you define - * a saveRowIdentity function, then it will return you to the currently + * a saveRowIdentity function, then it will return you to the currently * selected column within that row (in a business sense - so if some - * rows have been deleted, it will still find the same data, presuming it + * rows have been deleted, it will still find the same data, presuming it * still exists in the list. If it isn't in the list then it will instead * return to the same row number - i.e. scroll percentage) - * + * * Note that this option will do nothing if the cellNav * feature is not enabled. - * + * *
Defaults to true (unless saveScroll is true) */ gridOptions.saveFocus = gridOptions.saveScroll !== true && gridOptions.saveFocus !== false; @@ -169,14 +169,14 @@ * @ngdoc object * @name saveRowIdentity * @propertyOf ui.grid.saveState.api:GridOptions - * @description A function that can be called, passing in a rowEntity, - * and that will return a unique id for that row. This might simply + * @description A function that can be called, passing in a rowEntity, + * and that will return a unique id for that row. This might simply * return the `id` field from that row (if you have one), or it might * concatenate some fields within the row to make a unique value. - * - * This value will be used to find the same row again and set the focus + * + * This value will be used to find the same row again and set the focus * to it, if it exists when we return. - * + * *
Defaults to undefined */ /** @@ -184,25 +184,25 @@ * @name saveVisible * @propertyOf ui.grid.saveState.api:GridOptions * @description Save whether or not columns are visible. - * + * *
Defaults to true */ - gridOptions.saveVisible = gridOptions.saveVisible !== false; + gridOptions.saveVisible = gridOptions.saveVisible !== false; /** * @ngdoc object * @name saveSort * @propertyOf ui.grid.saveState.api:GridOptions * @description Save the current sort state for each column - * + * *
Defaults to true */ - gridOptions.saveSort = gridOptions.saveSort !== false; + gridOptions.saveSort = gridOptions.saveSort !== false; /** * @ngdoc object * @name saveFilter * @propertyOf ui.grid.saveState.api:GridOptions * @description Save the current filter state for each column - * + * *
Defaults to true */ gridOptions.saveFilter = gridOptions.saveFilter !== false; @@ -214,13 +214,55 @@ * is defined, then it will save the id of the row and select that. If not, then * it will attempt to select the rows by row number, which will give the wrong results * if the data set has changed in the mean-time. - * + * * Note that this option only does anything - * if the selection feature is enabled. - * + * if the selection feature is enabled. + * + *
Defaults to true + */ + gridOptions.saveSelection = gridOptions.saveSelection !== false; + /** + * @ngdoc object + * @name saveGrouping + * @propertyOf ui.grid.saveState.api:GridOptions + * @description Save the grouping configuration. If set to true and the + * grouping feature is not enabled then does nothing. + * + *
Defaults to true + */ + gridOptions.saveGrouping = gridOptions.saveGrouping !== false; + /** + * @ngdoc object + * @name saveGroupingExpandedStates + * @propertyOf ui.grid.saveState.api:GridOptions + * @description Save the grouping row expanded states. If set to true and the + * grouping feature is not enabled then does nothing. + * + * This can be quite a bit of data, in many cases you wouldn't want to save this + * information. + * + *
Defaults to false + */ + gridOptions.saveGroupingExpandedStates = gridOptions.saveGroupingExpandedStates === true; + /** + * @ngdoc object + * @name savePinning + * @propertyOf ui.grid.saveState.api:GridOptions + * @description Save pinning state for columns. + * + *
Defaults to true + */ + gridOptions.savePinning = gridOptions.savePinning !== false; + /** + * @ngdoc object + * @name saveTreeView + * @propertyOf ui.grid.saveState.api:GridOptions + * @description Save the treeView configuration. If set to true and the + * treeView feature is not enabled then does nothing. + * *
Defaults to true */ - gridOptions.saveSelection = gridOptions.saveSelection !== false; + gridOptions.saveTreeView = gridOptions.saveTreeView !== false; }, @@ -236,21 +278,23 @@ */ save: function (grid) { var savedState = {}; - + savedState.columns = service.saveColumns( grid ); savedState.scrollFocus = service.saveScrollFocus( grid ); savedState.selection = service.saveSelection( grid ); - + savedState.grouping = service.saveGrouping( grid ); + savedState.treeView = service.saveTreeView( grid ); + return savedState; }, - - + + /** * @ngdoc function * @name restore * @methodOf ui.grid.saveState.service:uiGridSaveStateService * @description Applies the provided state to the grid - * + * * @param {Grid} grid the grid whose state we'd like to restore * @param {scope} $scope a scope that we can broadcast on * @param {object} state the state we'd like to restore @@ -259,26 +303,34 @@ if ( state.columns ) { service.restoreColumns( grid, state.columns ); } - + if ( state.scrollFocus ){ service.restoreScrollFocus( grid, $scope, state.scrollFocus ); } - + if ( state.selection ){ service.restoreSelection( grid, state.selection ); } - - grid.queueGridRefresh(); + + if ( state.grouping ){ + service.restoreGrouping( grid, state.grouping ); + } + + if ( state.treeView ){ + service.restoreTreeView( grid, state.treeView ); + } + + grid.refresh(); }, - - + + /** * @ngdoc function * @name saveColumns * @methodOf ui.grid.saveState.service:uiGridSaveStateService - * @description Saves the column setup, including sort, filters, ordering - * and column widths. - * + * @description Saves the column setup, including sort, filters, ordering, + * pinning and column widths. + * * Works through the current columns, storing them in order. Stores the * column name, then the visible flag, width, sort and filters for each column. * @@ -287,38 +339,54 @@ */ saveColumns: function( grid ) { var columns = []; - angular.forEach( grid.columns, function( column ) { + grid.getOnlyDataColumns().forEach( function( column ) { var savedColumn = {}; savedColumn.name = column.name; - savedColumn.visible = column.visible; - savedColumn.width = column.width; - + + if ( grid.options.saveVisible ){ + savedColumn.visible = column.visible; + } + + if ( grid.options.saveWidths ){ + savedColumn.width = column.width; + } + // these two must be copied, not just pointed too - otherwise our saved state is pointing to the same object as current state - savedColumn.sort = angular.copy( column.sort ); - savedColumn.filters = angular.copy ( column.filters ); + if ( grid.options.saveSort ){ + savedColumn.sort = angular.copy( column.sort ); + } + + if ( grid.options.saveFilter ){ + savedColumn.filters = angular.copy ( column.filters ); + } + + if ( !!grid.api.pinning && grid.options.savePinning ){ + savedColumn.pinned = column.renderContainer ? column.renderContainer : ''; + } + columns.push( savedColumn ); }); - + return columns; }, - + /** * @ngdoc function * @name saveScrollFocus * @methodOf ui.grid.saveState.service:uiGridSaveStateService * @description Saves the currently scroll or focus. - * + * * If cellNav isn't present then does nothing - we can't return * to the scroll position without cellNav anyway. - * + * * If the cellNav module is present, and saveFocus is true, then * it saves the currently focused cell. If rowIdentity is present * then saves using rowIdentity, otherwise saves visibleRowNum. - * + * * If the cellNav module is not present, and saveScroll is true, then * it approximates the current scroll row and column, and saves that. - * + * * @param {Grid} grid the grid whose state we'd like to save * @returns {object} the selection state ready to be saved */ @@ -326,29 +394,35 @@ if ( !grid.api.cellNav ){ return {}; } - + var scrollFocus = {}; if ( grid.options.saveFocus ){ scrollFocus.focus = true; var rowCol = grid.api.cellNav.getFocusedCell(); if ( rowCol !== null ) { - scrollFocus.colName = rowCol.col.colDef.name; - scrollFocus.rowVal = service.getRowVal( grid, rowCol.row ); + if ( rowCol.col !== null ){ + scrollFocus.colName = rowCol.col.colDef.name; + } + if ( rowCol.row !== null ){ + scrollFocus.rowVal = service.getRowVal( grid, rowCol.row ); + } } - } else if ( grid.options.saveScroll ) { + } + + if ( grid.options.saveScroll || grid.options.saveFocus && !scrollFocus.colName && !scrollFocus.rowVal ) { scrollFocus.focus = false; if ( grid.renderContainers.body.prevRowScrollIndex ){ scrollFocus.rowVal = service.getRowVal( grid, grid.renderContainers.body.visibleRowCache[ grid.renderContainers.body.prevRowScrollIndex ]); } - + if ( grid.renderContainers.body.prevColScrollIndex ){ scrollFocus.colName = grid.renderContainers.body.visibleColumnCache[ grid.renderContainers.body.prevColScrollIndex ].name; } - } - + } + return scrollFocus; }, - + /** * @ngdoc function @@ -356,11 +430,11 @@ * @methodOf ui.grid.saveState.service:uiGridSaveStateService * @description Saves the currently selected rows, if the selection feature is enabled * @param {Grid} grid the grid whose state we'd like to save - * @returns {object} the selection state ready to be saved + * @returns {array} the selection state ready to be saved */ saveSelection: function( grid ){ if ( !grid.api.selection || !grid.options.saveSelection ){ - return {}; + return []; } var selection = grid.api.selection.getSelectedGridRows().map( function( gridRow ) { @@ -369,24 +443,58 @@ return selection; }, - - + + + /** + * @ngdoc function + * @name saveGrouping + * @methodOf ui.grid.saveState.service:uiGridSaveStateService + * @description Saves the grouping state, if the grouping feature is enabled + * @param {Grid} grid the grid whose state we'd like to save + * @returns {object} the grouping state ready to be saved + */ + saveGrouping: function( grid ){ + if ( !grid.api.grouping || !grid.options.saveGrouping ){ + return {}; + } + + return grid.api.grouping.getGrouping( grid.options.saveGroupingExpandedStates ); + }, + + + /** + * @ngdoc function + * @name saveTreeView + * @methodOf ui.grid.saveState.service:uiGridSaveStateService + * @description Saves the tree view state, if the tree feature is enabled + * @param {Grid} grid the grid whose state we'd like to save + * @returns {object} the tree view state ready to be saved + */ + saveTreeView: function( grid ){ + if ( !grid.api.treeView || !grid.options.saveTreeView ){ + return {}; + } + + return grid.api.treeView.getTreeView(); + }, + + /** * @ngdoc function * @name getRowVal * @methodOf ui.grid.saveState.service:uiGridSaveStateService * @description Helper function that gets either the rowNum or - * the saveRowIdentity, given a gridRow + * the saveRowIdentity, given a gridRow * @param {Grid} grid the grid the row is in * @param {GridRow} gridRow the row we want the rowNum for * @returns {object} an object containing { identity: true/false, row: rowNumber/rowIdentity } - * + * */ getRowVal: function( grid, gridRow ){ if ( !gridRow ) { return null; } - + var rowVal = {}; if ( grid.options.saveRowIdentity ){ rowVal.identity = true; @@ -397,64 +505,74 @@ } return rowVal; }, - - + + /** * @ngdoc function * @name restoreColumns * @methodOf ui.grid.saveState.service:uiGridSaveStateService - * @description Restores the columns, including order, visible, width - * sort and filters. - * + * @description Restores the columns, including order, visible, width, + * pinning, sort and filters. + * * @param {Grid} grid the grid whose state we'd like to restore * @param {object} columnsState the list of columns we had before, with their state */ restoreColumns: function( grid, columnsState ){ - angular.forEach( columnsState, function( columnState, index ) { - var currentCol = grid.columns.filter( function( column ) { - return column.name === columnState.name; - }); - - if ( currentCol.length > 0 ){ - var currentIndex = grid.columns.indexOf( currentCol[0] ); - - if ( grid.columns[currentIndex].visible !== columnState.visible || - grid.columns[currentIndex].colDef.visible !== columnState.visible ){ - grid.columns[currentIndex].visible = columnState.visible; - grid.columns[currentIndex].colDef.visible = columnState.visible; - grid.api.core.raise.columnVisibilityChanged( grid.columns[currentIndex]); + columnsState.forEach( function( columnState, index ) { + var currentCol = grid.getColumn( columnState.name ); + + + + if ( currentCol && !grid.isRowHeaderColumn(currentCol) ){ + if ( grid.options.saveVisible && + ( currentCol.visible !== columnState.visible || + currentCol.colDef.visible !== columnState.visible ) ){ + currentCol.visible = columnState.visible; + currentCol.colDef.visible = columnState.visible; + grid.api.core.raise.columnVisibilityChanged(currentCol); + } + + if ( grid.options.saveWidths ){ + currentCol.width = columnState.width; } - - grid.columns[currentIndex].width = columnState.width; - if ( !angular.equals(grid.columns[currentIndex].sort, columnState.sort) && - !( grid.columns[currentIndex].sort === undefined && angular.isEmpty(columnState.sort) ) ){ - grid.columns[currentIndex].sort = angular.copy( columnState.sort ); + if ( grid.options.saveSort && + !angular.equals(currentCol.sort, columnState.sort) && + !( currentCol.sort === undefined && angular.isEmpty(columnState.sort) ) ){ + currentCol.sort = angular.copy( columnState.sort ); grid.api.core.raise.sortChanged(); } - if ( !angular.equals(grid.columns[currentIndex].filters, columnState.filters ) ){ - grid.columns[currentIndex].filters = angular.copy( columnState.filters ); + if ( grid.options.saveFilter && + !angular.equals(currentCol.filters, columnState.filters ) ){ + currentCol.filters = angular.copy( columnState.filters ); grid.api.core.raise.filterChanged(); } - - if ( currentIndex !== index ){ - var column = grid.columns.splice( currentIndex, 1 )[0]; - grid.columns.splice( index, 0, column ); + + if ( !!grid.api.pinning && grid.options.savePinning && currentCol.renderContainer !== columnState.pinned ){ + grid.api.pinning.pinColumn(currentCol, columnState.pinned); + } + + var currentIndex = grid.getOnlyDataColumns().indexOf( currentCol ); + if (currentIndex !== -1) { + if (grid.options.saveOrder && currentIndex !== index) { + var column = grid.columns.splice(currentIndex + grid.rowHeaderColumns.length, 1)[0]; + grid.columns.splice(index + grid.rowHeaderColumns.length, 0, column); + } } } }); }, - + /** * @ngdoc function * @name restoreScrollFocus * @methodOf ui.grid.saveState.service:uiGridSaveStateService * @description Scrolls to the position that was saved. If focus is true, then - * sets focus to the specified row/col. If focus is false, then scrolls to the + * sets focus to the specified row/col. If focus is false, then scrolls to the * specified row/col. - * + * * @param {Grid} grid the grid whose state we'd like to restore * @param {scope} $scope a scope that we can broadcast on * @param {object} scrollFocusState the scroll/focus state ready to be restored @@ -463,7 +581,7 @@ if ( !grid.api.cellNav ){ return; } - + var colDef, row; if ( scrollFocusState.colName ){ var colDefs = grid.options.columnDefs.filter( function( colDef ) { return colDef.name === scrollFocusState.colName; }); @@ -471,7 +589,7 @@ colDef = colDefs[0]; } } - + if ( scrollFocusState.rowVal && scrollFocusState.rowVal.row ){ if ( scrollFocusState.rowVal.identity ){ row = service.findRowByIdentity( grid, scrollFocusState.rowVal ); @@ -479,18 +597,18 @@ row = grid.renderContainers.body.visibleRowCache[ scrollFocusState.rowVal.row ]; } } - + var entity = row && row.entity ? row.entity : null ; - if ( colDef || entity ) { + if ( colDef || entity ) { if (scrollFocusState.focus ){ grid.api.cellNav.scrollToFocus( entity, colDef ); } else { - grid.api.cellNav.scrollTo( entity, colDef ); + grid.scrollTo( entity, colDef ); } } }, - + /** * @ngdoc function @@ -506,24 +624,58 @@ if ( !grid.api.selection ){ return; } - + grid.api.selection.clearSelectedRows(); - angular.forEach( selectionState, function( rowVal ) { + selectionState.forEach( function( rowVal ) { if ( rowVal.identity ){ var foundRow = service.findRowByIdentity( grid, rowVal ); - + if ( foundRow ){ grid.api.selection.selectRow( foundRow.entity ); } - + } else { grid.api.selection.selectRowByVisibleIndex( rowVal.row ); } }); }, - - + + + /** + * @ngdoc function + * @name restoreGrouping + * @methodOf ui.grid.saveState.service:uiGridSaveStateService + * @description Restores the grouping configuration, if the grouping feature + * is enabled. + * @param {Grid} grid the grid whose state we'd like to restore + * @param {object} groupingState the grouping state ready to be restored + */ + restoreGrouping: function( grid, groupingState ){ + if ( !grid.api.grouping || typeof(groupingState) === 'undefined' || groupingState === null || angular.equals(groupingState, {}) ){ + return; + } + + grid.api.grouping.setGrouping( groupingState ); + }, + + /** + * @ngdoc function + * @name restoreTreeView + * @methodOf ui.grid.saveState.service:uiGridSaveStateService + * @description Restores the tree view configuration, if the tree view feature + * is enabled. + * @param {Grid} grid the grid whose state we'd like to restore + * @param {object} treeViewState the tree view state ready to be restored + */ + restoreTreeView: function( grid, treeViewState ){ + if ( !grid.api.treeView || typeof(treeViewState) === 'undefined' || treeViewState === null || angular.equals(treeViewState, {}) ){ + return; + } + + grid.api.treeView.setTreeView( treeViewState ); + }, + /** * @ngdoc function * @name findRowByIdentity @@ -538,7 +690,7 @@ if ( !grid.options.saveRowIdentity ){ return null; } - + var filteredRows = grid.rows.filter( function( gridRow ) { if ( grid.options.saveRowIdentity( gridRow.entity ) === rowVal.row ){ return true; @@ -546,7 +698,7 @@ return false; } }); - + if ( filteredRows.length > 0 ){ return filteredRows[0]; } else { diff --git a/src/features/saveState/test/saveState.spec.js b/src/features/saveState/test/saveState.spec.js index 9277f09ac1..23cdd1b3f3 100644 --- a/src/features/saveState/test/saveState.spec.js +++ b/src/features/saveState/test/saveState.spec.js @@ -3,64 +3,80 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { var uiGridSaveStateConstants; var uiGridSelectionService; var uiGridCellNavService; + var uiGridGroupingService; + var uiGridTreeViewService; + var uiGridPinningService; var gridClassFactory; var grid; var $compile; var $scope; var $document; + var $timeout; beforeEach(module('ui.grid.saveState')); beforeEach(inject(function (_uiGridSaveStateService_, _gridClassFactory_, _uiGridSaveStateConstants_, _$compile_, _$rootScope_, _$document_, _uiGridSelectionService_, - _uiGridCellNavService_ ) { + _uiGridCellNavService_, _uiGridGroupingService_, _uiGridTreeViewService_, + _uiGridPinningService_, _$timeout_) { uiGridSaveStateService = _uiGridSaveStateService_; uiGridSaveStateConstants = _uiGridSaveStateConstants_; uiGridSelectionService = _uiGridSelectionService_; uiGridCellNavService = _uiGridCellNavService_; + uiGridGroupingService = _uiGridGroupingService_; + uiGridTreeViewService = _uiGridTreeViewService_; + uiGridPinningService = _uiGridPinningService_; gridClassFactory = _gridClassFactory_; $compile = _$compile_; $scope = _$rootScope_.$new(); $document = _$document_; + $timeout = _$timeout_; grid = gridClassFactory.createGrid({}); grid.options.columnDefs = [ - {field: 'col1', name: 'col1', displayName: 'Col1', width: 50}, + {field: 'col1', name: 'col1', displayName: 'Col1', width: 50, pinnedLeft:true }, {field: 'col2', name: 'col2', displayName: 'Col2', width: '*', type: 'number'}, {field: 'col3', name: 'col3', displayName: 'Col3', width: 100}, - {field: 'col4', name: 'col4', displayName: 'Col4', width: 200} + {field: 'col4', name: 'col4', displayName: 'Col4', width: 200, pinnedRight:true } ]; _uiGridSaveStateService_.initializeGrid(grid); - + var data = []; for (var i = 0; i < 4; i++) { data.push({col1:'a_'+i, col2:'b_'+i, col3:'c_'+i, col4:'d_'+i}); } grid.options.data = data; - grid.buildColumns(); + $timeout(function () { + grid.addRowHeaderColumn({name: 'header'}); + }); + $timeout.flush(); + expect(grid.columns.length).toBe(5); + + + grid.modifyRows(grid.options.data); grid.rows[1].visible = false; - grid.columns[2].visible = false; + grid.getOnlyDataColumns()[2].visible = false; grid.setVisibleRows(grid.rows); grid.setVisibleColumns(grid.columns); grid.gridWidth = 500; - grid.columns[0].drawnWidth = 50; - grid.columns[1].drawnWidth = '*'; - grid.columns[2].drawnWidth = 100; - grid.columns[3].drawnWidth = 200; + grid.getOnlyDataColumns()[0].drawnWidth = 50; + grid.getOnlyDataColumns()[1].drawnWidth = '*'; + grid.getOnlyDataColumns()[2].drawnWidth = 100; + grid.getOnlyDataColumns()[3].drawnWidth = 200; })); - + describe('defaultGridOptions', function() { var options; beforeEach(function() { options = {}; }); - + it('set all options to default', function() { uiGridSaveStateService.defaultGridOptions(options); expect( options ).toEqual({ @@ -71,7 +87,11 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { saveVisible: true, saveSort: true, saveFilter: true, - saveSelection: true + saveSelection: true, + saveGrouping: true, + saveGroupingExpandedStates: false, + saveTreeView: true, + savePinning: true }); }); @@ -85,7 +105,11 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { saveVisible: false, saveSort: false, saveFilter: false, - saveSelection: false + saveSelection: false, + saveGrouping: false, + saveGroupingExpandedStates: true, + saveTreeView: false, + savePinning: false }; uiGridSaveStateService.defaultGridOptions(options); expect( options ).toEqual({ @@ -96,59 +120,131 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { saveVisible: false, saveSort: false, saveFilter: false, - saveSelection: false + saveSelection: false, + saveGrouping: false, + saveGroupingExpandedStates: true, + saveTreeView: false, + savePinning: false }); - }); + }); }); describe('saveColumns', function() { it('save columns', function() { expect( uiGridSaveStateService.saveColumns( grid ) ).toEqual([ - { name: 'col1', visible: true, width: 50, sort: [], filters: [] }, - { name: 'col2', visible: true, width: '*', sort: [], filters: [] }, - { name: 'col3', visible: false, width: 100, sort: [], filters: [] }, - { name: 'col4', visible: true, width: 200, sort: [], filters: [] } + { name: 'col1', visible: true, width: 50, sort: [], filters: [ {} ] }, + { name: 'col2', visible: true, width: '*', sort: [], filters: [ {} ] }, + { name: 'col3', visible: false, width: 100, sort: [], filters: [ {} ] }, + { name: 'col4', visible: true, width: 200, sort: [], filters: [ {} ] } ]); }); + + it('save columns with most options turned off', function() { + grid.options.saveWidths = false; + grid.options.saveVisible = false; + grid.options.saveSort = false; + grid.options.saveFilter = false; + + expect( uiGridSaveStateService.saveColumns( grid ) ).toEqual([ + { name: 'col1' }, + { name: 'col2' }, + { name: 'col3' }, + { name: 'col4' } + ]); + }); + + describe('pinning enabled', function() { + + beforeEach(function(){ + uiGridPinningService.initializeGrid(grid); + grid.buildColumns(); + grid.getOnlyDataColumns()[2].visible = false; + grid.setVisibleColumns(grid.columns); + }); + + it('save columns', function() { + expect( uiGridSaveStateService.saveColumns( grid ) ).toEqual([ + { name: 'col1', visible: true, width: 50, sort: [], filters: [ {} ], pinned: 'left' }, + { name: 'col2', visible: true, width: '*', sort: [], filters: [ {} ], pinned: '' }, + { name: 'col3', visible: false, width: 100, sort: [], filters: [ {} ], pinned: '' }, + { name: 'col4', visible: true, width: 200, sort: [], filters: [ {} ], pinned: 'right' } + ]); + }); + + it('save columns with most options turned off', function() { + grid.options.saveWidths = false; + grid.options.saveVisible = false; + grid.options.saveSort = false; + grid.options.saveFilter = false; + grid.options.savePinning = false; + + expect( uiGridSaveStateService.saveColumns( grid ) ).toEqual([ + { name: 'col1' }, + { name: 'col2' }, + { name: 'col3' }, + { name: 'col4' } + ]); + }); + }); }); - - + + describe('saveScrollFocus', function() { it('does nothing when no cellNav module initialized', function() { expect( uiGridSaveStateService.saveScrollFocus( grid ) ).toEqual( {} ); }); - it('save focus, no focus present', function() { + it('save focus, no focus present, tries to save scroll instead', function() { uiGridCellNavService.initializeGrid(grid); - - expect( uiGridSaveStateService.saveScrollFocus( grid ) ).toEqual( { focus: true } ); + + expect( uiGridSaveStateService.saveScrollFocus( grid ) ).toEqual( { focus: false } ); }); it('save focus, focus present, no row identity', function() { uiGridCellNavService.initializeGrid(grid); - + spyOn( grid.api.cellNav, 'getFocusedCell' ).andCallFake( function() { - return { row: grid.rows[2], col: grid.columns[3] }; + return { row: grid.rows[2], col: grid.getOnlyDataColumns()[3] }; }); - + expect( uiGridSaveStateService.saveScrollFocus( grid ) ).toEqual( { focus: true, colName: 'col4', rowVal: { identity: false, row: 1 } } ); }); + it('save focus, focus present, no col', function() { + uiGridCellNavService.initializeGrid(grid); + + spyOn( grid.api.cellNav, 'getFocusedCell' ).andCallFake( function() { + return { row: grid.rows[2], col: null }; + }); + + expect( uiGridSaveStateService.saveScrollFocus( grid ) ).toEqual( { focus: true, rowVal: { identity: false, row: 1 } } ); + }); + + it('save focus, focus present, no row', function() { + uiGridCellNavService.initializeGrid(grid); + + spyOn( grid.api.cellNav, 'getFocusedCell' ).andCallFake( function() { + return { row: null, col: grid.getOnlyDataColumns()[3] }; + }); + + expect( uiGridSaveStateService.saveScrollFocus( grid ) ).toEqual( { focus: true, colName: 'col4' } ); + }); + it('save focus, focus present, row identity present', function() { uiGridCellNavService.initializeGrid(grid); - + grid.options.saveRowIdentity = function ( rowEntity ){ return rowEntity.col1; }; - + spyOn( grid.api.cellNav, 'getFocusedCell' ).andCallFake( function() { - return { row: grid.rows[2], col: grid.columns[3] }; + return { row: grid.rows[2], col: grid.getOnlyDataColumns()[3] }; }); - + expect( uiGridSaveStateService.saveScrollFocus( grid ) ).toEqual( { focus: true, colName: 'col4', rowVal: { identity: true, row: 'a_2' } } ); }); - + it('save scroll, no prevscroll', function() { uiGridCellNavService.initializeGrid(grid); grid.options.saveFocus = false; @@ -156,10 +252,10 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { grid.renderContainers.body.grid.renderContainers.body.prevColScrollIndex = undefined; grid.renderContainers.body.grid.renderContainers.body.prevRowScrollIndex = undefined; - + expect( uiGridSaveStateService.saveScrollFocus( grid ) ).toEqual( { focus: false } ); }); - + it('save scroll, no row identity', function() { uiGridCellNavService.initializeGrid(grid); grid.options.saveFocus = false; @@ -167,10 +263,10 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { grid.renderContainers.body.grid.renderContainers.body.prevColScrollIndex = 2; grid.renderContainers.body.grid.renderContainers.body.prevRowScrollIndex = 2; - + expect( uiGridSaveStateService.saveScrollFocus( grid ) ).toEqual( { focus: false, colName: 'col4', rowVal: { identity: false, row: 2 } } ); }); - + it('save scroll, row identity present', function() { uiGridCellNavService.initializeGrid(grid); grid.options.saveFocus = false; @@ -178,10 +274,10 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { grid.options.saveRowIdentity = function ( rowEntity ){ return rowEntity.col1; }; - + grid.renderContainers.body.grid.renderContainers.body.prevColScrollIndex = 2; grid.renderContainers.body.grid.renderContainers.body.prevRowScrollIndex = 2; - + expect( uiGridSaveStateService.saveScrollFocus( grid ) ).toEqual( { focus: false, colName: 'col4', rowVal: { identity: true, row: 'a_3' } } ); }); }); @@ -191,10 +287,10 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { it('does nothing when no selection module initialized', function() { expect( uiGridSaveStateService.saveSelection( grid ) ).toEqual( {} ); }); - + it('saves no selection, without identity function', function() { uiGridSelectionService.initializeGrid(grid); - + expect( uiGridSaveStateService.saveSelection( grid ) ).toEqual( [] ); }); @@ -204,16 +300,16 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { grid.options.saveRowIdentity = function( rowEntity ){ return rowEntity.col1; }; - + expect( uiGridSaveStateService.saveSelection( grid ) ).toEqual( [] ); }); - + it('saves selected rows, without identity function', function() { uiGridSelectionService.initializeGrid(grid); - + grid.api.selection.selectRow(grid.options.data[0]); grid.api.selection.selectRow(grid.options.data[3]); // note that row 1 is not visible, so this will be visible row 2 - + expect( uiGridSaveStateService.saveSelection( grid ) ).toEqual( [ { identity: false, row: 0 }, { identity: false, row: 2 } ] ); }); @@ -226,7 +322,7 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { grid.api.selection.selectRow(grid.options.data[0]); grid.api.selection.selectRow(grid.options.data[3]); - + expect( uiGridSaveStateService.saveSelection( grid ) ).toEqual( [ { identity: true, row: 'a_0' }, { identity: true, row: 'a_3' } ] ); }); }); @@ -240,75 +336,146 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { it( 'gridRow, not visible', function() { expect( uiGridSaveStateService.getRowVal( grid, grid.rows[1] )).toEqual( { identity: false, row: -1 }); }); - + it( 'gridRow, visible', function() { expect( uiGridSaveStateService.getRowVal( grid, grid.rows[2] )).toEqual( { identity: false, row: 1 }); }); - + }); describe('restoreColumns', function() { - it('restore columns', function() { + it('restore columns, all options turned on', function() { + grid.options.saveWidths = true; + grid.options.saveOrder = true; + grid.options.saveVisible = true; + grid.options.saveSort = true; + grid.options.saveFilter = true; + var colVisChangeCount = 0; var colFilterChangeCount = 0; var colSortChangeCount = 0; - + grid.api.core.on.columnVisibilityChanged( $scope, function( column ) { - colVisChangeCount++; + colVisChangeCount++; }); grid.api.core.on.filterChanged( $scope, function() { - colFilterChangeCount++; + colFilterChangeCount++; }); grid.api.core.on.sortChanged( $scope, function() { - colSortChangeCount++; + colSortChangeCount++; }); - + uiGridSaveStateService.restoreColumns( grid, [ - { name: 'col2', visible: false, width: 90, sort: [ {blah: 'blah'} ], filters: [] }, + { name: 'col2', visible: false, width: 90, sort: [ {blah: 'blah'} ], filters: [ {} ] }, { name: 'col1', visible: true, width: '*', sort: [], filters: [ {'blah': 'blah'} ] }, - { name: 'col4', visible: false, width: 120, sort: [], filters: [] }, - { name: 'col3', visible: true, width: 220, sort: [], filters: [] } + { name: 'col4', visible: false, width: 120, sort: [], filters: [ {} ] }, + { name: 'col3', visible: true, width: 220, sort: [], filters: [ {} ] } ]); - - expect( grid.columns[0].name ).toEqual('col2', 'column 0 name should be col2'); - expect( grid.columns[1].name ).toEqual('col1', 'column 1 name should be col1'); - expect( grid.columns[2].name ).toEqual('col4', 'column 2 name should be col4'); - expect( grid.columns[3].name ).toEqual('col3', 'column 3 name should be col3'); - - expect( grid.columns[0].visible ).toEqual(false, 'column 0 visible should be false'); - expect( grid.columns[1].visible ).toEqual(true, 'column 1 visible should be true'); - expect( grid.columns[2].visible ).toEqual(false, 'column 2 visible should be false'); - expect( grid.columns[3].visible ).toEqual(true, 'column 3 visible should be true'); - - expect( grid.columns[0].colDef.visible ).toEqual(false, 'coldef 0 visible should be false'); - expect( grid.columns[1].colDef.visible ).toEqual(true, 'coldef 1 visible should be true'); - expect( grid.columns[2].colDef.visible ).toEqual(false, 'coldef 2 visible should be false'); - expect( grid.columns[3].colDef.visible ).toEqual(true, 'coldef 3 visible should be true'); - - expect( grid.columns[0].width ).toEqual(90); - expect( grid.columns[1].width ).toEqual('*'); - expect( grid.columns[2].width ).toEqual(120); - expect( grid.columns[3].width ).toEqual(220); - - expect( grid.columns[0].sort ).toEqual([ { blah: 'blah' } ]); - expect( grid.columns[1].sort ).toEqual([]); - expect( grid.columns[2].sort ).toEqual([]); - expect( grid.columns[3].sort ).toEqual([]); - - expect( grid.columns[0].filters ).toEqual([]); - expect( grid.columns[1].filters ).toEqual([ { blah: 'blah' } ]); - expect( grid.columns[2].filters ).toEqual([]); - expect( grid.columns[3].filters ).toEqual([]); - + + expect( grid.getOnlyDataColumns()[0].name ).toEqual('col2', 'column 0 name should be col2'); + expect( grid.getOnlyDataColumns()[1].name ).toEqual('col1', 'column 1 name should be col1'); + expect( grid.getOnlyDataColumns()[2].name ).toEqual('col4', 'column 2 name should be col4'); + expect( grid.getOnlyDataColumns()[3].name ).toEqual('col3', 'column 3 name should be col3'); + + expect( grid.getOnlyDataColumns()[0].visible ).toEqual(false, 'column 0 visible should be false'); + expect( grid.getOnlyDataColumns()[1].visible ).toEqual(true, 'column 1 visible should be true'); + expect( grid.getOnlyDataColumns()[2].visible ).toEqual(false, 'column 2 visible should be false'); + expect( grid.getOnlyDataColumns()[3].visible ).toEqual(true, 'column 3 visible should be true'); + + expect( grid.getOnlyDataColumns()[0].colDef.visible ).toEqual(false, 'coldef 0 visible should be false'); + expect( grid.getOnlyDataColumns()[1].colDef.visible ).toEqual(true, 'coldef 1 visible should be true'); + expect( grid.getOnlyDataColumns()[2].colDef.visible ).toEqual(false, 'coldef 2 visible should be false'); + expect( grid.getOnlyDataColumns()[3].colDef.visible ).toEqual(true, 'coldef 3 visible should be true'); + + expect( grid.getOnlyDataColumns()[0].width ).toEqual(90); + expect( grid.getOnlyDataColumns()[1].width ).toEqual('*'); + expect( grid.getOnlyDataColumns()[2].width ).toEqual(120); + expect( grid.getOnlyDataColumns()[3].width ).toEqual(220); + + expect( grid.getOnlyDataColumns()[0].sort ).toEqual([ { blah: 'blah' } ]); + expect( grid.getOnlyDataColumns()[1].sort ).toEqual([]); + expect( grid.getOnlyDataColumns()[2].sort ).toEqual([]); + expect( grid.getOnlyDataColumns()[3].sort ).toEqual([]); + + expect( grid.getOnlyDataColumns()[0].filters ).toEqual([ {} ]); + expect( grid.getOnlyDataColumns()[1].filters ).toEqual([ { blah: 'blah' } ]); + expect( grid.getOnlyDataColumns()[2].filters ).toEqual([ {} ]); + expect( grid.getOnlyDataColumns()[3].filters ).toEqual([ {} ]); + expect( colVisChangeCount ).toEqual( 4, '4 columns changed visibility'); expect( colFilterChangeCount ).toEqual( 1, '1 columns changed filter'); expect( colSortChangeCount ).toEqual( 4, '4 columns changed sort'); }); + + it('restore columns, all options turned off', function() { + grid.options.saveWidths = false; + grid.options.saveOrder = false; + grid.options.saveVisible = false; + grid.options.saveSort = false; + grid.options.saveFilter = false; + + var colVisChangeCount = 0; + var colFilterChangeCount = 0; + var colSortChangeCount = 0; + + grid.api.core.on.columnVisibilityChanged( $scope, function( column ) { + colVisChangeCount++; + }); + + grid.api.core.on.filterChanged( $scope, function() { + colFilterChangeCount++; + }); + + grid.api.core.on.sortChanged( $scope, function() { + colSortChangeCount++; + }); + + uiGridSaveStateService.restoreColumns( grid, [ + { name: 'col2', visible: false, width: 90, sort: [ {blah: 'blah'} ], filters: [ {} ] }, + { name: 'col1', visible: true, width: '*', sort: [], filters: [ {'blah': 'blah'} ] }, + { name: 'col4', visible: false, width: 120, sort: [], filters: [ {} ] }, + { name: 'col3', visible: true, width: 220, sort: [], filters: [ {} ] } + ]); + + expect( grid.getOnlyDataColumns()[0].name ).toEqual('col1', 'column 0 name should be col1'); + expect( grid.getOnlyDataColumns()[1].name ).toEqual('col2', 'column 1 name should be col2'); + expect( grid.getOnlyDataColumns()[2].name ).toEqual('col3', 'column 2 name should be col3'); + expect( grid.getOnlyDataColumns()[3].name ).toEqual('col4', 'column 3 name should be col4'); + + expect( grid.getOnlyDataColumns()[0].visible ).toEqual(true, 'column 0 visible should be true'); + expect( grid.getOnlyDataColumns()[1].visible ).toEqual(true, 'column 1 visible should be true'); + expect( grid.getOnlyDataColumns()[2].visible ).toEqual(false, 'column 2 visible should be false'); + expect( grid.getOnlyDataColumns()[3].visible ).toEqual(true, 'column 3 visible should be true'); + + expect( grid.getOnlyDataColumns()[0].colDef.visible ).toEqual(undefined, 'coldef 0 visible should be undefined'); + expect( grid.getOnlyDataColumns()[1].colDef.visible ).toEqual(undefined, 'coldef 1 visible should be undefined'); + expect( grid.getOnlyDataColumns()[2].colDef.visible ).toEqual(undefined, 'coldef 2 visible should be undefined'); + expect( grid.getOnlyDataColumns()[3].colDef.visible ).toEqual(undefined, 'coldef 3 visible should be undefined'); + + expect( grid.getOnlyDataColumns()[0].width ).toEqual(50); + expect( grid.getOnlyDataColumns()[1].width ).toEqual('*'); + expect( grid.getOnlyDataColumns()[2].width ).toEqual(100); + expect( grid.getOnlyDataColumns()[3].width ).toEqual(200); + + expect( grid.getOnlyDataColumns()[0].sort ).toEqual([]); + expect( grid.getOnlyDataColumns()[1].sort ).toEqual([]); + expect( grid.getOnlyDataColumns()[2].sort ).toEqual([]); + expect( grid.getOnlyDataColumns()[3].sort ).toEqual([]); + + expect( grid.getOnlyDataColumns()[0].filters ).toEqual([ {} ]); + expect( grid.getOnlyDataColumns()[1].filters ).toEqual([ {} ]); + expect( grid.getOnlyDataColumns()[2].filters ).toEqual([ {} ]); + expect( grid.getOnlyDataColumns()[3].filters ).toEqual([ {} ]); + + expect( colVisChangeCount ).toEqual( 0, '0 columns changed visibility'); + expect( colFilterChangeCount ).toEqual( 0, '0 columns changed filter'); + expect( colSortChangeCount ).toEqual( 0, '0 columns changed sort'); + }); }); - + describe('restoreScrollFocus', function() { it('does nothing when no cellNav module initialized', function() { @@ -317,23 +484,23 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { it('restores no row/col, without identity function', function() { uiGridCellNavService.initializeGrid(grid); - spyOn( grid.api.cellNav, 'scrollTo' ); + spyOn( grid.api.core, 'scrollTo' ); spyOn( grid.api.cellNav, 'scrollToFocus' ); - + uiGridSaveStateService.restoreScrollFocus( grid, $scope, {} ); - - expect( grid.api.cellNav.scrollTo ).not.toHaveBeenCalled(); + + expect( grid.api.core.scrollTo ).not.toHaveBeenCalled(); expect( grid.api.cellNav.scrollToFocus ).not.toHaveBeenCalled(); }); it('restores focus row only, without identity function', function() { uiGridCellNavService.initializeGrid(grid); - spyOn( grid.api.cellNav, 'scrollTo' ); + spyOn( grid.api.core, 'scrollTo' ); spyOn( grid.api.cellNav, 'scrollToFocus' ); - + uiGridSaveStateService.restoreScrollFocus( grid, $scope, { focus: true, rowVal: { identity: false, row: 2 } } ); - - expect( grid.api.cellNav.scrollTo ).not.toHaveBeenCalled(); + + expect( grid.api.core.scrollTo ).not.toHaveBeenCalled(); expect( grid.api.cellNav.scrollToFocus ).toHaveBeenCalledWith( grid.rows[3].entity, undefined ); }); @@ -343,24 +510,24 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { grid.options.saveRowIdentity = function( rowEntity ){ return rowEntity.col1; }; - - spyOn( grid.api.cellNav, 'scrollTo' ); + + spyOn( grid.api.core, 'scrollTo' ); spyOn( grid.api.cellNav, 'scrollToFocus' ); - + uiGridSaveStateService.restoreScrollFocus( grid, $scope, { focus: true, rowVal: { identity: true, row: 'a_3' } } ); - - expect( grid.api.cellNav.scrollTo ).not.toHaveBeenCalled(); + + expect( grid.api.core.scrollTo ).not.toHaveBeenCalled(); expect( grid.api.cellNav.scrollToFocus ).toHaveBeenCalledWith( grid.rows[3].entity, undefined ); }); it('restores focus col only, without identity function', function() { uiGridCellNavService.initializeGrid(grid); - spyOn( grid.api.cellNav, 'scrollTo' ); + spyOn( grid.api.core, 'scrollTo' ); spyOn( grid.api.cellNav, 'scrollToFocus' ); - + uiGridSaveStateService.restoreScrollFocus( grid, $scope, { focus: true, colName: 'col2' } ); - - expect( grid.api.cellNav.scrollTo ).not.toHaveBeenCalled(); + + expect( grid.api.core.scrollTo ).not.toHaveBeenCalled(); expect( grid.api.cellNav.scrollToFocus ).toHaveBeenCalledWith( null, grid.options.columnDefs[1] ); }); @@ -370,24 +537,24 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { grid.options.saveRowIdentity = function( rowEntity ){ return rowEntity.col1; }; - - spyOn( grid.api.cellNav, 'scrollTo' ); + + spyOn( grid.api.core, 'scrollTo' ); spyOn( grid.api.cellNav, 'scrollToFocus' ); - + uiGridSaveStateService.restoreScrollFocus( grid, $scope, { focus: true, colName: 'col2' } ); - - expect( grid.api.cellNav.scrollTo ).not.toHaveBeenCalled(); + + expect( grid.api.core.scrollTo ).not.toHaveBeenCalled(); expect( grid.api.cellNav.scrollToFocus ).toHaveBeenCalledWith( null, grid.options.columnDefs[1] ); }); it('restores focus col and row, without identity function', function() { uiGridCellNavService.initializeGrid(grid); - spyOn( grid.api.cellNav, 'scrollTo' ); + spyOn( grid.api.core, 'scrollTo' ); spyOn( grid.api.cellNav, 'scrollToFocus' ); - + uiGridSaveStateService.restoreScrollFocus( grid, $scope, { focus: true, colName: 'col2', rowVal: { identity: false, row: 2 } } ); - - expect( grid.api.cellNav.scrollTo ).not.toHaveBeenCalled(); + + expect( grid.api.core.scrollTo ).not.toHaveBeenCalled(); expect( grid.api.cellNav.scrollToFocus ).toHaveBeenCalledWith( grid.rows[3].entity, grid.options.columnDefs[1] ); }); @@ -397,29 +564,29 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { grid.options.saveRowIdentity = function( rowEntity ){ return rowEntity.col1; }; - - spyOn( grid.api.cellNav, 'scrollTo' ); + + spyOn( grid.api.core, 'scrollTo' ); spyOn( grid.api.cellNav, 'scrollToFocus' ); - + uiGridSaveStateService.restoreScrollFocus( grid, $scope, { focus: true, colName: 'col2', rowVal: { identity: true, row: 'a_3' } } ); - - expect( grid.api.cellNav.scrollTo ).not.toHaveBeenCalled(); + + expect( grid.api.core.scrollTo ).not.toHaveBeenCalled(); expect( grid.api.cellNav.scrollToFocus ).toHaveBeenCalledWith( grid.rows[3].entity, grid.options.columnDefs[1] ); }); }); - + describe('restoreSelection', function() { it('does nothing when no selection module initialized', function() { uiGridSaveStateService.restoreSelection( grid, [ { identity: false, row: 0 } ] ); }); - + it('restores no selection, without identity function', function() { uiGridSelectionService.initializeGrid(grid); - + uiGridSaveStateService.restoreSelection( grid, [] ); - + expect( grid.api.selection.getSelectedGridRows.length ).toEqual( 0 ); }); @@ -429,17 +596,17 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { grid.options.saveRowIdentity = function( rowEntity ){ return rowEntity.col1; }; - + uiGridSaveStateService.restoreSelection( grid, [ ] ); - + expect( grid.api.selection.getSelectedGridRows.length ).toEqual( 0 ); }); - + it('restores selected rows, without identity function', function() { uiGridSelectionService.initializeGrid(grid); - + uiGridSaveStateService.restoreSelection( grid, [ { identity: false, row: 0 }, { identity: false, row: 2 } ] ); - + expect( grid.api.selection.getSelectedGridRows().length ).toEqual( 2 ); // row 1 is not visible, so visible row 2 is actually grid row 3 @@ -462,9 +629,9 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { it('restores invisible row, without identity function', function() { uiGridSelectionService.initializeGrid(grid); - + uiGridSaveStateService.restoreSelection( grid, [ { identity: false, row: -1 } ] ); - + expect( grid.api.selection.getSelectedGridRows().length ).toEqual( 0 ); }); @@ -481,6 +648,44 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { }); }); + describe('restoreGrouping', function() { + beforeEach( function() { + grid.api.grouping = { setGrouping: function() {}}; + spyOn( grid.api.grouping, 'setGrouping' ).andCallFake(function() {}); + }); + + it( 'calls setGrouping with config', function() { + uiGridSaveStateService.restoreGrouping( grid, { grouping: [], aggregations: [] }); + + expect(grid.api.grouping.setGrouping).toHaveBeenCalledWith( { grouping: [], aggregations: [] }); + }); + + it( 'doesn\'t call setGrouping when config missing', function() { + uiGridSaveStateService.restoreGrouping( grid, undefined); + + expect(grid.api.grouping.setGrouping).not.toHaveBeenCalled(); + }); + }); + + describe('restoreTreeView', function() { + beforeEach( function() { + grid.api.treeView = { setTreeView: function() {}}; + spyOn( grid.api.treeView, 'setTreeView' ).andCallFake(function() {}); + }); + + it( 'calls setTreeView with config', function() { + uiGridSaveStateService.restoreTreeView( grid, { test: 'test' }); + + expect(grid.api.treeView.setTreeView).toHaveBeenCalledWith( { test: 'test' }); + }); + + it( 'doesn\'t call setTreeView when config missing', function() { + uiGridSaveStateService.restoreTreeView( grid, undefined); + + expect(grid.api.treeView.setTreeView).not.toHaveBeenCalled(); + }); + }); + describe('findRowByIdentity', function() { it('no row identity', function() { @@ -491,7 +696,7 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { grid.options.saveRowIdentity = function( rowEntity ){ return rowEntity.col1; }; - + expect( uiGridSaveStateService.findRowByIdentity( grid, { identity: true, row: 'a_2' } ) ).toEqual(grid.rows[2]); }); @@ -499,7 +704,7 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { grid.options.saveRowIdentity = function( rowEntity ){ return rowEntity.col1; }; - + expect( uiGridSaveStateService.findRowByIdentity( grid, { identity: true, row: 'a_9' } ) ).toEqual(null); }); }); @@ -510,29 +715,29 @@ describe('ui.grid.saveState uiGridSaveStateService', function () { uiGridCellNavService.initializeGrid(grid); spyOn( grid.api.cellNav, 'getFocusedCell' ).andCallFake( function() { - return { row: grid.rows[2], col: grid.columns[3] }; + return { row: grid.rows[2], col: grid.getOnlyDataColumns()[3] }; }); - spyOn( grid.api.cellNav, 'scrollTo' ); + spyOn( grid.api.core, 'scrollTo' ); spyOn( grid.api.cellNav, 'scrollToFocus' ); - + grid.options.saveRowIdentity = function( rowEntity ){ return rowEntity.col1; }; grid.api.selection.selectRow(grid.options.data[0]); grid.api.selection.selectRow(grid.options.data[3]); - + var state = grid.api.saveState.save(); - + grid.api.selection.clearSelectedRows(); grid.api.selection.selectRow(grid.options.data[2]); - + grid.api.saveState.restore( $scope, state ); - + expect( grid.api.selection.getSelectedGridRows() ).toEqual( [ grid.rows[0], grid.rows[3] ] ); - expect( grid.api.cellNav.scrollTo ).not.toHaveBeenCalled(); + expect( grid.api.core.scrollTo ).not.toHaveBeenCalled(); expect( grid.api.cellNav.scrollToFocus ).toHaveBeenCalledWith( grid.rows[2].entity, grid.options.columnDefs[3] ); }); - }); -}); \ No newline at end of file + }); +}); diff --git a/src/features/selection/js/selection.js b/src/features/selection/js/selection.js index 87ed99cc8a..e15da9be7e 100644 --- a/src/features/selection/js/selection.js +++ b/src/features/selection/js/selection.js @@ -63,7 +63,7 @@ * @methodOf ui.grid.selection.api:GridRow * @description Sets the isSelected property and updates the selectedCount * Changes to isSelected state should only be made via this function - * @param {bool} selelected value to set + * @param {bool} selected value to set */ $delegate.prototype.setSelected = function(selected) { this.isSelected = selected; @@ -422,7 +422,7 @@ * @propertyOf ui.grid.selection.api:GridOptions * @description Shows the total number of selected items in footer if true. *
Defaults to true. - *
GridOptions.showFooter must also be set to true. + *
GridOptions.showGridFooter must also be set to true. */ gridOptions.enableFooterTotalSelected = gridOptions.enableFooterTotalSelected !== false; @@ -430,7 +430,7 @@ * @ngdoc object * @name isRowSelectable * @propertyOf ui.grid.selection.api:GridOptions - * @description Makes it possible to specify a method that evaluates for each and sets its "enableSelection" property. + * @description Makes it possible to specify a method that evaluates for each row and sets its "enableSelection" property. */ gridOptions.isRowSelectable = angular.isDefined(gridOptions.isRowSelectable) ? gridOptions.isRowSelectable : angular.noop; @@ -615,8 +615,8 @@ */ - module.directive('uiGridSelection', ['uiGridSelectionConstants', 'uiGridSelectionService', '$templateCache', - function (uiGridSelectionConstants, uiGridSelectionService, $templateCache) { + module.directive('uiGridSelection', ['uiGridSelectionConstants', 'uiGridSelectionService', '$templateCache', 'uiGridConstants', + function (uiGridSelectionConstants, uiGridSelectionService, $templateCache, uiGridConstants) { return { replace: true, priority: 0, @@ -643,11 +643,27 @@ uiGridCtrl.grid.addRowHeaderColumn(selectionRowHeaderDef); } - if (uiGridCtrl.grid.options.isRowSelectable !== angular.noop) { - uiGridCtrl.grid.registerRowBuilder(function(row, options) { + var processorSet = false; + + var processSelectableRows = function( rows ){ + rows.forEach(function(row){ row.enableSelection = uiGridCtrl.grid.options.isRowSelectable(row); }); - } + return rows; + }; + + var updateOptions = function(){ + if (uiGridCtrl.grid.options.isRowSelectable !== angular.noop && processorSet !== true) { + uiGridCtrl.grid.registerRowsProcessor(processSelectableRows, 500); + processorSet = true; + } + }; + + updateOptions(); + + var dataChangeDereg = uiGridCtrl.grid.registerDataChangeCallback( updateOptions, [uiGridConstants.dataChange.OPTIONS] ); + + $scope.$on( '$destroy', dataChangeDereg); }, post: function ($scope, $elm, $attrs, uiGridCtrl) { @@ -668,6 +684,7 @@ link: function($scope, $elm, $attrs, uiGridCtrl) { var self = uiGridCtrl.grid; $scope.selectButtonClick = function(row, evt) { + evt.stopPropagation(); if (evt.shiftKey) { uiGridSelectionService.shiftSelect(self, row, evt, self.options.multiSelect); } @@ -756,25 +773,48 @@ * @description Stacks on top of ui.grid.uiGridCell to provide selection feature */ module.directive('uiGridCell', - ['$compile', 'uiGridConstants', 'uiGridSelectionConstants', 'gridUtil', '$parse', 'uiGridSelectionService', - function ($compile, uiGridConstants, uiGridSelectionConstants, gridUtil, $parse, uiGridSelectionService) { + ['$compile', 'uiGridConstants', 'uiGridSelectionConstants', 'gridUtil', '$parse', 'uiGridSelectionService', '$timeout', + function ($compile, uiGridConstants, uiGridSelectionConstants, gridUtil, $parse, uiGridSelectionService, $timeout) { return { priority: -200, // run after default uiGridCell directive restrict: 'A', + require: '?^uiGrid', scope: false, - link: function ($scope, $elm, $attrs) { + link: function ($scope, $elm, $attrs, uiGridCtrl) { var touchStartTime = 0; var touchTimeout = 300; - $elm.bind('keydown', function (evt) { - if (evt.keyCode === 32 && $scope.col.colDef.name === "selectionRowHeaderCol") { - uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect), $scope.grid.options.noUnselect); - $scope.$apply(); - } - }); + // Bind to keydown events in the render container + if (uiGridCtrl.grid.api.cellNav) { + + uiGridCtrl.grid.api.cellNav.on.viewPortKeyDown($scope, function (evt, rowCol) { + if (rowCol === null || + rowCol.row !== $scope.row || + rowCol.col !== $scope.col) { + return; + } + + if (evt.keyCode === 32 && $scope.col.colDef.name === "selectionRowHeaderCol") { + uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect), $scope.grid.options.noUnselect); + $scope.$apply(); + } + + // uiGridCellNavService.scrollToIfNecessary(uiGridCtrl.grid, rowCol.row, rowCol.col); + }); + } + + //$elm.bind('keydown', function (evt) { + // if (evt.keyCode === 32 && $scope.col.colDef.name === "selectionRowHeaderCol") { + // uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect), $scope.grid.options.noUnselect); + // $scope.$apply(); + // } + //}); var selectCells = function(evt){ + // if we get a click, then stop listening for touchend + $elm.off('touchend', touchEnd); + if (evt.shiftKey) { uiGridSelectionService.shiftSelect($scope.grid, $scope.row, evt, $scope.grid.options.multiSelect); } @@ -785,10 +825,19 @@ uiGridSelectionService.toggleRowSelection($scope.grid, $scope.row, evt, ($scope.grid.options.multiSelect && !$scope.grid.options.modifierKeysToMultiSelect), $scope.grid.options.noUnselect); } $scope.$apply(); + + // don't re-enable the touchend handler for a little while - some devices generate both, and it will + // take a little while to move your hand from the mouse to the screen if you have both modes of input + $timeout(function() { + $elm.on('touchend', touchEnd); + }, touchTimeout); }; var touchStart = function(evt){ touchStartTime = (new Date()).getTime(); + + // if we get a touch event, then stop listening for click + $elm.off('click', selectCells); }; var touchEnd = function(evt) { @@ -799,6 +848,12 @@ // short touch selectCells(evt); } + + // don't re-enable the click handler for a little while - some devices generate both, and it will + // take a little while to move your hand from the screen to the mouse if you have both modes of input + $timeout(function() { + $elm.on('click', selectCells); + }, touchTimeout); }; function registerRowSelectionEvents() { diff --git a/src/features/selection/less/selection.less b/src/features/selection/less/selection.less index 1aaba18d65..aff0043d66 100644 --- a/src/features/selection/less/selection.less +++ b/src/features/selection/less/selection.less @@ -1,7 +1,7 @@ @import '../../../less/variables'; -.ui-grid-row-selected > [ui-grid-row] > .ui-grid-cell { - background-color: @rowSelected !important; +.ui-grid-row.ui-grid-row-selected > [ui-grid-row] > .ui-grid-cell { + background-color: @rowSelected; } .ui-grid-disable-selection { diff --git a/src/features/selection/test/uiGridSelectionDirective.spec.js b/src/features/selection/test/uiGridSelectionDirective.spec.js index 84b87d1b40..89e3619be1 100644 --- a/src/features/selection/test/uiGridSelectionDirective.spec.js +++ b/src/features/selection/test/uiGridSelectionDirective.spec.js @@ -2,21 +2,19 @@ describe('ui.grid.selection uiGridSelectionDirective', function() { var parentScope, elm, scope, - gridCtrl; + gridCtrl, + $compile, + $rootScope, + uiGridConstants; beforeEach(module('ui.grid.selection')); - beforeEach(function() { - var rootScope; + beforeEach(inject(function(_$rootScope_, _$compile_, _uiGridConstants_) { + $compile = _$compile_; + $rootScope = _$rootScope_; + uiGridConstants = _uiGridConstants_; - inject([ - '$rootScope', - function (rootScopeInj) { - rootScope = rootScopeInj; - } - ]); - - parentScope = rootScope.$new(); + parentScope = $rootScope.$new(); parentScope.options = { columnDefs : [{field: 'id'}] @@ -32,19 +30,14 @@ describe('ui.grid.selection uiGridSelectionDirective', function() { } var tpl = '
'; - - inject([ - '$compile', - function ($compile) { - elm = $compile(tpl)(parentScope); - }]); + elm = $compile(tpl)(parentScope); parentScope.$digest(); scope = elm.scope(); gridCtrl = elm.controller('uiGrid'); - }); + })); it('should set the "enableSelection" field of the row using the function specified in "isRowSelectable"', function() { for (var i = 0; i < gridCtrl.grid.rows.length; i++) { @@ -61,4 +54,45 @@ describe('ui.grid.selection uiGridSelectionDirective', function() { } } }); + + describe('with filtering turned on', function () { + var elm, $timeout; + + /* + NOTES + - We have to flush $timeout because the header calculations are done post-$timeout, as that's when the header has been fully rendered. + - We have to actually attach the grid element to the document body, otherwise it will not have a rendered height. + */ + + beforeEach(inject(function (_$timeout_) { + $timeout = _$timeout_; + + parentScope.options.enableFiltering = true; + + elm = angular.element('
'); + document.body.appendChild(elm[0]); + $compile(elm)(parentScope); + $timeout.flush(); + parentScope.$digest(); + })); + + afterEach(function () { + $(elm).remove(); + }); + + it("doesn't prevent headers from shrinking when filtering gets turned off", function () { + // Header height with filtering on + var filteringHeight = $(elm).find('.ui-grid-header').height(); + + parentScope.options.enableFiltering = false; + elm.controller('uiGrid').grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN ); + $timeout.flush(); + parentScope.$digest(); + + var noFilteringHeight = $(elm).find('.ui-grid-header').height(); + + expect(noFilteringHeight).not.toEqual(filteringHeight); + expect(noFilteringHeight < filteringHeight).toBe(true); + }); + }); }); diff --git a/src/features/tree-view/js/tree-view.js b/src/features/tree-view/js/tree-view.js new file mode 100644 index 0000000000..17849d8875 --- /dev/null +++ b/src/features/tree-view/js/tree-view.js @@ -0,0 +1,837 @@ +(function () { + 'use strict'; + + /** + * @ngdoc overview + * @name ui.grid.treeView + * @description + * + * # ui.grid.treeView + * This module provides a tree view of the data that it is provided, with nodes in that + * tree and leaves. Unlike grouping, the tree is an inherent property of the data and must + * be provided with your data array. If you are using treeView you probably should disable sorting. + * + * Filtering is plausible, but requires some reworking to work with treeView - ideally the + * parent nodes would be shown whenever a child node or leaf node under them matched the filter + * + * Design information: + * ------------------- + * + * The raw data that is provided must come with a $$treeLevel on any non-leaf node. TreeView + * will run a rowsProcessor to set expand buttons alongside these nodes, and will maintain the + * expand/collapse state of each node. + * + * In future a count of the direct children of each node could optionally be calculated and displayed + * alongside the node - the current issue is deciding where to display that. For now we calculate it + * but don't display it. + * + * In future the count could be used to remove the + from a row that doesn't actually have any children. + * + * Optionally the treeView can be populated only when nodes are clicked on. This will provide callbacks when + * nodes are expanded, requesting the additional data. The node will be set to expanded, and when the data + * is added to the grid then it will automatically be displayed by the rowsProcessor. + * + * Treeview adds information to the rows + * - treeLevel - if present and > -1 tells us the level (level 0 is the top level) + * - expandedState = object: pointer to the node in the grid.treeView.rowExpandedStates that refers + * to this row, allowing us to manipulate the state + * + * Since the logic is baked into the rowsProcessors, it should get triggered whenever + * row order or filtering or anything like that is changed. We recall the expanded state + * across invocations of the rowsProcessors by putting it into the grid.treeView.rowExpandedStates hash. + * + * By default rows are collapsed, which means all data rows have their visible property + * set to false, and only level 0 group rows are set to visible. + * + * We rely on the rowsProcessors to do the actual expanding and collapsing, so we set the flags we want into + * grid.treeView.rowExpandedStates, then call refresh. This is because we can't easily change the visible + * row cache without calling the processors, and once we've built the logic into the rowProcessors we may as + * well use it all the time. + * + *
+ *
+ * + *
+ */ + + var module = angular.module('ui.grid.treeView', ['ui.grid']); + + /** + * @ngdoc object + * @name ui.grid.treeView.constant:uiGridTreeViewConstants + * + * @description constants available in treeView module + * + */ + module.constant('uiGridTreeViewConstants', { + featureName: "treeView", + treeViewRowHeaderColName: 'treeViewRowHeaderCol', + EXPANDED: 'expanded', + COLLAPSED: 'collapsed' + }); + + /** + * @ngdoc service + * @name ui.grid.treeView.service:uiGridTreeViewService + * + * @description Services for treeView features + */ + module.service('uiGridTreeViewService', ['$q', 'uiGridTreeViewConstants', 'gridUtil', 'GridRow', 'gridClassFactory', 'i18nService', 'uiGridConstants', + function ($q, uiGridTreeViewConstants, gridUtil, GridRow, gridClassFactory, i18nService, uiGridConstants) { + + var service = { + + initializeGrid: function (grid, $scope) { + + //add feature namespace and any properties to grid for needed + /** + * @ngdoc object + * @name ui.grid.treeView.grid:treeView + * + * @description Grid properties and functions added for treeView + */ + grid.treeView = {}; + + /** + * @ngdoc property + * @propertyOf ui.grid.treeView.grid:treeView + * @name numberLevels + * + * @description Total number of tree levels currently used, calculated by the rowsProcessor by + * retaining the highest tree level it sees + */ + grid.treeView.numberLevels = 0; + + /** + * @ngdoc property + * @propertyOf ui.grid.treeView.grid:treeView + * @name expandAll + * + * @description Whether or not the expandAll box is selected + */ + grid.treeView.expandAll = false; + + /** + * @ngdoc property + * @propertyOf ui.grid.treeView.grid:treeView + * @name rowExpandedStates + * + * @description Nested hash that holds all the expanded states based on the nodes. + * We use the row.uid as the key into the hash, only because we need a key. + * + * ``` + * { + * uiGrid-DXNP: { + * state: 'expanded', + * uiGrid-DAP: { state: 'expanded' }, + * uiGrid-BBB: { state: 'collapsed' }, + * uiGrid-AAA: { state: 'expanded' }, + * uiGrid-CCC: { state: 'collapsed' } + * }, + * uiGrid-DXNG: { + * state: 'collapsed', + * uiGrid-DDD: { state: 'expanded' }, + * uiGrid-XXX: { state: 'collapsed' }, + * uiGrid-YYY: { state: 'expanded' } + * } + * } + * ``` + * Missing values are false - meaning they aren't expanded. + * + * This is used because the rowProcessors run every time the grid is refreshed, so + * we'd lose the expanded state every time the grid was refreshed. This instead gives + * us a reliable lookup that persists across rowProcessors. + * + */ + grid.treeView.rowExpandedStates = {}; + + service.defaultGridOptions(grid.options); + + grid.registerRowsProcessor(service.treeRows, 410); + + /** + * @ngdoc object + * @name ui.grid.treeView.api:PublicApi + * + * @description Public Api for treeView feature + */ + var publicApi = { + events: { + treeView: { + /** + * @ngdoc event + * @eventOf ui.grid.treeView.api:PublicApi + * @name rowExpanded + * @description raised whenever a row is expanded. If you are dynamically + * rendering your tree you can listen to this event, and then retrieve + * the children of this row and load them into the grid data. + * + * When the data is loaded the grid will automatically refresh to show these new rows + * + *
+                 *      gridApi.treeView.on.rowExpanded(scope,function(row){})
+                 * 
+ * @param {gridRow} row the row that was expanded. You can also + * retrieve the grid from this row with row.grid + */ + rowExpanded: {}, + + /** + * @ngdoc event + * @eventOf ui.grid.treeView.api:PublicApi + * @name rowCollapsed + * @description raised whenever a row is collapsed. Doesn't really have + * a purpose at the moment, included for symmetry + * + *
+                 *      gridApi.treeView.on.rowCollapsed(scope,function(row){})
+                 * 
+ * @param {gridRow} row the row that was collapsed. You can also + * retrieve the grid from this row with row.grid + */ + rowCollapsed: {} + } + }, + methods: { + treeView: { + /** + * @ngdoc function + * @name expandAllRows + * @methodOf ui.grid.treeView.api:PublicApi + * @description Expands all tree rows + */ + expandAllRows: function () { + service.expandAllRows(grid); + }, + + /** + * @ngdoc function + * @name collapseAllRows + * @methodOf ui.grid.treeView.api:PublicApi + * @description collapse all tree rows + */ + collapseAllRows: function () { + service.collapseAllRows(grid); + }, + + /** + * @ngdoc function + * @name toggleRowTreeViewState + * @methodOf ui.grid.treeView.api:PublicApi + * @description call expand if the row is collapsed, collapse if it is expanded + * @param {gridRow} row the row you wish to toggle + */ + toggleRowTreeViewState: function (row) { + service.toggleRowTreeViewState(grid, row); + }, + + /** + * @ngdoc function + * @name expandRow + * @methodOf ui.grid.treeView.api:PublicApi + * @description expand the immediate children of the specified row + * @param {gridRow} row the row you wish to expand + */ + expandRow: function (row) { + service.expandRow(grid, row); + }, + + /** + * @ngdoc function + * @name expandRowChildren + * @methodOf ui.grid.treeView.api:PublicApi + * @description expand all children of the specified row + * @param {gridRow} row the row you wish to expand + */ + expandRowChildren: function (row) { + service.expandRowChildren(grid, row); + }, + + /** + * @ngdoc function + * @name collapseRow + * @methodOf ui.grid.treeView.api:PublicApi + * @description collapse the specified row. When + * you expand the row again, all grandchildren will retain their state + * @param {gridRow} row the row you wish to collapse + */ + collapseRow: function ( row ) { + service.collapseRow(grid, row); + }, + + /** + * @ngdoc function + * @name collapseRowChildren + * @methodOf ui.grid.treeView.api:PublicApi + * @description collapse all children of the specified row. When + * you expand the row again, all grandchildren will be collapsed + * @param {gridRow} row the row you wish to collapse children for + */ + collapseRowChildren: function ( row ) { + service.collapseRowChildren(grid, row); + }, + + /** + * @ngdoc function + * @name getGrouping + * @methodOf ui.grid.treeView.api:PublicApi + * @description Get the tree state for this grid, + * used by the saveState feature + * Returned treeView is an object + * `{ expandedState: hash }` + * where expandedState is a hash of the currently expanded nodes + * + * @returns {object} treeView state + */ + getTreeView: function () { + return { expandedState: grid.treeView.rowExpandedStates }; + }, + + /** + * @ngdoc function + * @name setTreeView + * @methodOf ui.grid.treeView.api:PublicApi + * @description Set the expanded states of the tree + * @param {object} config the config you want to apply, in the format + * provided out by getTreeView + */ + setTreeView: function ( config ) { + if ( typeof(config.expandedState) !== 'undefined' ){ + grid.treeView.rowExpandedStates = config.expandedState; + } + } + } + } + }; + + grid.api.registerEventsFromObject(publicApi.events); + + grid.api.registerMethodsFromObject(publicApi.methods); + + }, + + defaultGridOptions: function (gridOptions) { + //default option to true unless it was explicitly set to false + /** + * @ngdoc object + * @name ui.grid.treeView.api:GridOptions + * + * @description GridOptions for treeView feature, these are available to be + * set using the ui-grid {@link ui.grid.class:GridOptions gridOptions} + */ + + /** + * @ngdoc object + * @name enableTreeView + * @propertyOf ui.grid.treeView.api:GridOptions + * @description Enable row tree view for entire grid. + *
Defaults to true + */ + gridOptions.enableTreeView = gridOptions.enableTreeView !== false; + + /** + * @ngdoc object + * @name treeViewRowHeaderBaseWidth + * @propertyOf ui.grid.treeView.api:GridOptions + * @description Base width of the treeView header, provides for a single level of tree. This + * is incremented by `treeViewIndent` for each extra level + *
Defaults to 30 + */ + gridOptions.treeViewRowHeaderBaseWidth = gridOptions.treeViewRowHeaderBaseWidth || 30; + + /** + * @ngdoc object + * @name treeViewIndent + * @propertyOf ui.grid.treeView.api:GridOptions + * @description Number of pixels of indent for the icon at each treeView level, wider indents are visually more pleasing, + * but will make the tree view row header wider + *
Defaults to 10 + */ + gridOptions.treeViewIndent = gridOptions.treeViewIndent || 10; + + /** + * @ngdoc object + * @name showTreeViewRowHeader + * @propertyOf ui.grid.treeView.api:GridOptions + * @description If set to false, don't create the row header. Youll need to programatically control the expand + * states + *
Defaults to true + */ + gridOptions.showTreeViewRowHeader = gridOptions.showTreeViewRowHeader !== false; + }, + + + /** + * @ngdoc function + * @name expandAllRows + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description Expands all nodes in the tree + * + * @param {Grid} grid grid object + */ + expandAllRows: function (grid) { + service.setAllNodes( grid, grid.treeView.rowExpandedStates, uiGridTreeViewConstants.EXPANDED ); + grid.queueGridRefresh(); + }, + + + /** + * @ngdoc function + * @name collapseAllRows + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description Collapses all nodes in the tree + * + * @param {Grid} grid grid object + */ + collapseAllRows: function (grid) { + service.setAllNodes( grid, grid.treeView.rowExpandedStates, uiGridTreeViewConstants.COLLAPSED ); + grid.queueGridRefresh(); + }, + + + /** + * @ngdoc function + * @name setAllNodes + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description Works through a subset of grid.treeView.rowExpandedStates, setting + * all child nodes (and their descendents) of the provided node to the given state. + * + * Calls itself recursively on all nodes so as to achieve this. + * + * @param {Grid} grid the grid we're operating on (so we can raise events) + * @param {object} expandedStatesSubset the portion of the tree that we want to update + * @param {string} targetState the state we want to set it to + */ + setAllNodes: function (grid, expandedStatesSubset, targetState) { + // set this node - if this is a node (first invocation in the recursion doesn't have a root node) + if ( typeof(expandedStatesSubset.state) !== 'undefined' && expandedStatesSubset.state !== targetState ){ + expandedStatesSubset.state = targetState; + if ( targetState === uiGridTreeViewConstants.EXPANDED ){ + grid.api.treeView.raise.rowExpanded(expandedStatesSubset.row); + } else { + grid.api.treeView.raise.rowCollapsed(expandedStatesSubset.row); + } + } + + // set all child nodes + angular.forEach(expandedStatesSubset, function( childNode, key){ + if (key !== 'state' && key !== 'row'){ + service.setAllNodes(grid, childNode, targetState); + } + }); + }, + + + /** + * @ngdoc function + * @name toggleRowTreeViewState + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description Toggles the expand or collapse state of this grouped row. + * If the row isn't a groupHeader, does nothing. + * + * @param {Grid} grid grid object + * @param {GridRow} row the row we want to toggle + */ + toggleRowTreeViewState: function ( grid, row ){ + if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){ + return; + } + + if (row.treeExpandedState.state === uiGridTreeViewConstants.EXPANDED){ + service.collapseRow(grid, row); + } else { + service.expandRow(grid, row); + } + + grid.queueGridRefresh(); + }, + + + /** + * @ngdoc function + * @name expandRow + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description Expands this specific row, showing only immediate children. + * + * @param {Grid} grid grid object + * @param {GridRow} row the row we want to expand + */ + expandRow: function ( grid, row ){ + if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){ + return; + } + + if ( row.treeExpandedState.state !== uiGridTreeViewConstants.EXPANDED ){ + row.treeExpandedState.state = uiGridTreeViewConstants.EXPANDED; + grid.api.treeView.raise.rowExpanded(row); + grid.queueGridRefresh(); + } + }, + + + /** + * @ngdoc function + * @name expandRowChildren + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description Expands this specific row, showing all children. + * + * @param {Grid} grid grid object + * @param {GridRow} row the row we want to expand + */ + expandRowChildren: function ( grid, row ){ + if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){ + return; + } + + service.setAllNodes(grid, row.treeExpandedState, uiGridTreeViewConstants.EXPANDED); + grid.queueGridRefresh(); + }, + + + /** + * @ngdoc function + * @name collapseRow + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description Collapses this specific row + * + * @param {Grid} grid grid object + * @param {GridRow} row the row we want to collapse + */ + collapseRow: function( grid, row ){ + if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){ + return; + } + + if ( row.treeExpandedState.state !== uiGridTreeViewConstants.COLLAPSED ){ + row.treeExpandedState.state = uiGridTreeViewConstants.COLLAPSED; + grid.api.treeView.raise.rowCollapsed(row); + grid.queueGridRefresh(); + } + }, + + + /** + * @ngdoc function + * @name collapseRowChildren + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description Collapses this specific row and all children + * + * @param {Grid} grid grid object + * @param {GridRow} row the row we want to collapse + */ + collapseRowChildren: function( grid, row ){ + if ( typeof(row.treeLevel) === 'undefined' || row.treeLevel === null || row.treeLevel < 0 ){ + return; + } + + service.setAllNodes(grid, row.treeExpandedState, uiGridTreeViewConstants.COLLAPSED); + grid.queueGridRefresh(); + }, + + + /** + * @ngdoc function + * @name treeRows + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description The rowProcessor that adds the nodes to the tree, and sets the visible + * state of each row based on it's parent state + * + * Assumes it is always called after the sorting processor + * + * Processes all the rows in order, setting the group level based on the $$treeLevel in the associated + * entity, and setting the visible state based on the parent's state. + * + * Calculates the deepest level of tree whilst it goes, and updates that so that the header column can be correctly + * sized. + * + * @param {array} renderableRows the rows we want to process, usually the output from the previous rowProcessor + * @returns {array} the updated rows, including our new group rows + */ + treeRows: function( renderableRows ) { + if (renderableRows.length === 0){ + return renderableRows; + } + + var grid = this; + var currentLevel = 0; + var currentState = uiGridTreeViewConstants.EXPANDED; + var parents = []; + + var updateState = function( row ) { + row.treeLevel = row.entity.$$treeLevel; + + if ( !row.visible ){ + return; + } + + if ( row.treeLevel <= currentLevel ){ + // pop any levels that aren't parents of this level + while ( row.treeLevel <= currentLevel ){ + parents.pop(); + currentLevel--; + } + + // reset our current state based on the new parent, set to expanded if this is a root node + if ( parents.length > 0 ){ + currentState = service.setCurrentState(parents); + } else { + currentState = uiGridTreeViewConstants.EXPANDED; + } + } + + // set visibility based on the parent's state + if ( currentState === uiGridTreeViewConstants.COLLAPSED ){ + row.visible = false; + } else { + row.visible = true; + } + + // if this row is a node, then add it to the parents array + if ( typeof(row.treeLevel) !== 'undefined' && row.treeLevel > -1 ){ + service.addOrUseState(grid, row, parents); + currentLevel++; + currentState = service.setCurrentState(parents); + } + + + // update the tree number of levels, so we can set header width if we need to + if ( grid.treeView.numberLevels < row.treeLevel ){ + grid.treeView.numberLevels = row.treeLevel; + } + }; + + renderableRows.forEach(updateState); + + var newWidth = grid.options.treeViewRowHeaderBaseWidth + grid.options.treeViewIndent * grid.treeView.numberLevels; + var rowHeader = grid.getColumn(uiGridTreeViewConstants.treeViewRowHeaderColName); + if ( rowHeader && newWidth !== rowHeader.width ){ + rowHeader.width = newWidth; + grid.queueRefresh(); + } + return renderableRows.filter(function (row) { return row.visible; }); + }, + + /** + * @ngdoc function + * @name addOrUseState + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description If a state already exists for this row with the right parents, use that state, + * otherwise create a new state for this row and set it's expand/collapse to the same as it's parent. + * + * @param {grid} grid the grid we're operating on + * @param {gridRow} row the row we want to set + * @param {array} parents an array of the parents this row should have + * @returns {undefined} updates the parents array, updates the row to have a treeExpandedState, and updates the + * grid.treeView.expandedStates + */ + addOrUseState: function( grid, row, parents ){ + if ( row.entity.$$treeLevel === 0 ){ + if ( typeof(grid.treeView.rowExpandedStates[row.uid]) === 'undefined' ) { + grid.treeView.rowExpandedStates[row.uid] = { state: uiGridTreeViewConstants.COLLAPSED, row: row }; + } + row.treeExpandedState = grid.treeView.rowExpandedStates[row.uid]; + } else { + var parentState = parents[parents.length - 1].treeExpandedState; + if ( typeof(parentState[row.uid]) === 'undefined') { + parentState[row.uid] = { state: parentState.state, row: row }; + } + row.treeExpandedState = parentState[row.uid]; + } + parents.push(row); + }, + + + /** + * @ngdoc function + * @name setCurrentState + * @methodOf ui.grid.treeView.service:uiGridTreeViewService + * @description Looks at the parents array to determine our current state. + * If any node in the hierarchy is collapsed, then return collapsed, otherwise return + * expanded. + * + * @param {array} parents an array of the parents this row should have + * @returns {string} the state we should be setting to any nodes we see + */ + setCurrentState: function( parents ){ + var currentState = uiGridTreeViewConstants.EXPANDED; + parents.forEach( function(parent){ + if ( parent.treeExpandedState.state === uiGridTreeViewConstants.COLLAPSED ){ + currentState = uiGridTreeViewConstants.COLLAPSED; + } + }); + + return currentState; + } + + }; + + return service; + + }]); + + /** + * @ngdoc directive + * @name ui.grid.treeView.directive:uiGridTreeView + * @element div + * @restrict A + * + * @description Adds treeView features to grid + * + * @example + + + var app = angular.module('app', ['ui.grid', 'ui.grid.treeView']); + + app.controller('MainCtrl', ['$scope', function ($scope) { + $scope.data = [ + { name: 'Bob', title: 'CEO' }, + { name: 'Frank', title: 'Lowly Developer' } + ]; + + $scope.columnDefs = [ + {name: 'name', enableCellEdit: true}, + {name: 'title', enableCellEdit: true} + ]; + + $scope.gridOptions = { columnDefs: $scope.columnDefs, data: $scope.data }; + }]); + + +
+
+
+
+
+ */ + module.directive('uiGridTreeView', ['uiGridTreeViewConstants', 'uiGridTreeViewService', '$templateCache', + function (uiGridTreeViewConstants, uiGridTreeViewService, $templateCache) { + return { + replace: true, + priority: 0, + require: '^uiGrid', + scope: false, + compile: function () { + return { + pre: function ($scope, $elm, $attrs, uiGridCtrl) { + if (uiGridCtrl.grid.options.enableTreeView !== false){ + uiGridTreeViewService.initializeGrid(uiGridCtrl.grid, $scope); + + if ( uiGridCtrl.grid.options.showTreeViewRowHeader ){ + var treeViewRowHeaderDef = { + name: uiGridTreeViewConstants.treeViewRowHeaderColName, + displayName: '', + width: uiGridCtrl.grid.options.treeViewRowHeaderBaseWidth, + minWidth: 10, + cellTemplate: 'ui-grid/treeViewRowHeader', + headerCellTemplate: 'ui-grid/treeViewHeaderCell', + enableColumnResizing: false, + enableColumnMenu: false, + exporterSuppressExport: true, + allowCellFocus: true + }; + + uiGridCtrl.grid.addRowHeaderColumn(treeViewRowHeaderDef); + } + } + }, + post: function ($scope, $elm, $attrs, uiGridCtrl) { + + } + }; + } + }; + }]); + + + /** + * @ngdoc directive + * @name ui.grid.treeView.directive:uiGridTreeViewRowHeaderButtons + * @element div + * + * @description Provides the expand/collapse button on groupHeader rows + */ + module.directive('uiGridTreeViewRowHeaderButtons', ['$templateCache', 'uiGridTreeViewService', + function ($templateCache, uiGridTreeViewService) { + return { + replace: true, + restrict: 'E', + template: $templateCache.get('ui-grid/treeViewRowHeaderButtons'), + scope: true, + require: '^uiGrid', + link: function($scope, $elm, $attrs, uiGridCtrl) { + var self = uiGridCtrl.grid; + $scope.treeViewButtonClick = function(row, evt) { + uiGridTreeViewService.toggleRowTreeViewState(self, row, evt); + }; + } + }; + }]); + + + /** + * @ngdoc directive + * @name ui.grid.treeView.directive:uiGridTreeViewExpandAllButtons + * @element div + * + * @description Provides the expand/collapse all button + */ + module.directive('uiGridTreeViewExpandAllButtons', ['$templateCache', 'uiGridTreeViewService', + function ($templateCache, uiGridTreeViewService) { + return { + replace: true, + restrict: 'E', + template: $templateCache.get('ui-grid/treeViewExpandAllButtons'), + scope: false, + link: function($scope, $elm, $attrs, uiGridCtrl) { + var self = $scope.col.grid; + + $scope.headerButtonClick = function(row, evt) { + if ( self.treeView.expandAll ){ + uiGridTreeViewService.collapseAllRows(self, evt); + self.treeView.expandAll = false; + } else { + uiGridTreeViewService.expandAllRows(self, evt); + self.treeView.expandAll = true; + } + }; + } + }; + }]); + + /** + * @ngdoc directive + * @name ui.grid.treeView.directive:uiGridViewport + * @element div + * + * @description Stacks on top of ui.grid.uiGridViewport to set formatting on a tree view header row + */ + module.directive('uiGridViewport', + ['$compile', 'uiGridConstants', 'uiGridTreeViewConstants', 'gridUtil', '$parse', 'uiGridTreeViewService', + function ($compile, uiGridConstants, uiGridTreeViewConstants, gridUtil, $parse, uiGridTreeViewService) { + return { + priority: -200, // run after default directive + scope: false, + compile: function ($elm, $attrs) { + var rowRepeatDiv = angular.element($elm.children().children()[0]); + + var existingNgClass = rowRepeatDiv.attr("ng-class"); + var newNgClass = ''; + if ( existingNgClass ) { + newNgClass = existingNgClass.slice(0, -1) + ",'ui-grid-tree-view-header-row': row.treeLevel > -1}"; + } else { + newNgClass = "{'ui-grid-tree-view-header-row': row.treeLevel > -1}"; + } + rowRepeatDiv.attr("ng-class", newNgClass); + + return { + pre: function ($scope, $elm, $attrs, controllers) { + + }, + post: function ($scope, $elm, $attrs, controllers) { + } + }; + } + }; + }]); + +})(); diff --git a/src/features/tree-view/less/tree-view.less b/src/features/tree-view/less/tree-view.less new file mode 100644 index 0000000000..ab94c729f8 --- /dev/null +++ b/src/features/tree-view/less/tree-view.less @@ -0,0 +1,10 @@ +@import '../../../less/variables'; + +.ui-grid-group-header-row { + font-weight: bold !important; +} + +.ui-grid-grouping-row-header-buttons.ui-grid-group-header { + cursor: pointer; + opacity: 1; +} diff --git a/src/features/tree-view/templates/treeViewExpandAllButtons.html b/src/features/tree-view/templates/treeViewExpandAllButtons.html new file mode 100644 index 0000000000..f6f126a871 --- /dev/null +++ b/src/features/tree-view/templates/treeViewExpandAllButtons.html @@ -0,0 +1,2 @@ +
+
\ No newline at end of file diff --git a/src/features/tree-view/templates/treeViewHeaderCell.html b/src/features/tree-view/templates/treeViewHeaderCell.html new file mode 100644 index 0000000000..4c4b02e8ff --- /dev/null +++ b/src/features/tree-view/templates/treeViewHeaderCell.html @@ -0,0 +1,5 @@ +
+
+ +
+
diff --git a/src/features/tree-view/templates/treeViewRowHeader.html b/src/features/tree-view/templates/treeViewRowHeader.html new file mode 100644 index 0000000000..5a0475c579 --- /dev/null +++ b/src/features/tree-view/templates/treeViewRowHeader.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/features/tree-view/templates/treeViewRowHeaderButtons.html b/src/features/tree-view/templates/treeViewRowHeaderButtons.html new file mode 100644 index 0000000000..30cda00931 --- /dev/null +++ b/src/features/tree-view/templates/treeViewRowHeaderButtons.html @@ -0,0 +1,4 @@ +
+ +   +
\ No newline at end of file diff --git a/src/features/tree-view/test/tree-view.spec.js b/src/features/tree-view/test/tree-view.spec.js new file mode 100644 index 0000000000..4e4b299e3e --- /dev/null +++ b/src/features/tree-view/test/tree-view.spec.js @@ -0,0 +1,194 @@ +describe('ui.grid.treeView uiGridTreeViewService', function () { + var uiGridTreeViewService; + var uiGridTreeViewConstants; + var gridClassFactory; + var grid; + var $rootScope; + var $scope; + var GridRow; + + beforeEach(module('ui.grid.treeView')); + + beforeEach(inject(function (_uiGridTreeViewService_,_gridClassFactory_, $templateCache, _uiGridTreeViewConstants_, + _$rootScope_, _GridRow_) { + uiGridTreeViewService = _uiGridTreeViewService_; + uiGridTreeViewConstants = _uiGridTreeViewConstants_; + gridClassFactory = _gridClassFactory_; + $rootScope = _$rootScope_; + $scope = $rootScope.$new(); + GridRow = _GridRow_; + + $templateCache.put('ui-grid/uiGridCell', '
'); + $templateCache.put('ui-grid/editableCell', '
'); + + grid = gridClassFactory.createGrid({}); + grid.options.columnDefs = [ + {field: 'col0'}, + {field: 'col1'}, + {field: 'col2'}, + {field: 'col3'} + ]; + + _uiGridTreeViewService_.initializeGrid(grid, $scope); + var data = []; + for (var i = 0; i < 10; i++) { + data.push({col0: 'a_' + Math.floor(i/4), col1: 'b_' + Math.floor(i/2), col2: 'c_' + i, col3: 'd_' + i}); + } + data[0].$$treeLevel = 0; + data[1].$$treeLevel = 1; + data[3].$$treeLevel = 1; + data[4].$$treeLevel = 2; + data[7].$$treeLevel = 0; + data[9].$$treeLevel = 1; + + grid.options.data = data; + + grid.buildColumns(); + grid.modifyRows(grid.options.data); + })); + + describe( 'treeRows', function() { + it( 'tree the rows', function() { + spyOn(gridClassFactory, 'rowTemplateAssigner').andCallFake( function() {}); + + var treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 2, 'only the level 0 rows are visible' ); + + expect( grid.treeView.numberLevels).toEqual(2); + }); + + it( 'expandAll', function() { + spyOn(gridClassFactory, 'rowTemplateAssigner').andCallFake( function() {}); + + var expandCount = 0; + grid.api.treeView.on.rowExpanded( $scope, function(row){ + expandCount++; + }); + + var treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 2, 'only the level 0 rows are visible' ); + + grid.api.treeView.expandAllRows(); + grid.rows.forEach(function( row ){ + row.visible = true; + }); + treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 10, 'all rows are visible' ); + expect( expandCount ).toEqual(6); + }); + + it( 'expandRow', function() { + spyOn(gridClassFactory, 'rowTemplateAssigner').andCallFake( function() {}); + + var expandCount = 0; + grid.api.treeView.on.rowExpanded( $scope, function(row){ + expandCount++; + }); + + var treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 2, 'only the level 0 rows are visible' ); + + grid.api.treeView.expandRow(grid.rows[0]); + grid.rows.forEach(function( row ){ + row.visible = true; + }); + treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 4, 'children of row 0 are also visible' ); + + expect( expandCount ).toEqual(1); + }); + + it( 'expandRowChildren', function() { + spyOn(gridClassFactory, 'rowTemplateAssigner').andCallFake( function() {}); + + var expandCount = 0; + grid.api.treeView.on.rowExpanded( $scope, function(row){ + expandCount++; + }); + + var treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 2, 'only the level 0 rows are visible' ); + + grid.api.treeView.expandRowChildren(grid.rows[0]); + grid.rows.forEach(function( row ){ + row.visible = true; + }); + treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 8, 'all children of row 0 are also visible' ); + + expect( expandCount ).toEqual(4, 'called for row 0, 1, 3 and 4'); + }); + + it( 'collapseRow', function() { + spyOn(gridClassFactory, 'rowTemplateAssigner').andCallFake( function() {}); + + var collapseCount = 0; + grid.api.treeView.on.rowCollapsed( $scope, function(row){ + collapseCount++; + }); + + var treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 2, 'only the level 0 rows are visible' ); + + grid.api.treeView.expandAllRows(); + grid.api.treeView.collapseRow(grid.rows[7]); + grid.rows.forEach(function( row ){ + row.visible = true; + }); + treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 8, 'children of row 7 are hidden' ); + expect( collapseCount ).toEqual( 1 ); + }); + + it( 'collapseRowChildren', function() { + spyOn(gridClassFactory, 'rowTemplateAssigner').andCallFake( function() {}); + + var collapseCount = 0; + grid.api.treeView.on.rowCollapsed( $scope, function(row){ + collapseCount++; + }); + + var treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 2, 'only the level 0 rows are visible' ); + + grid.api.treeView.expandAllRows(); + grid.api.treeView.collapseRowChildren(grid.rows[0]); + grid.rows.forEach(function( row ){ + row.visible = true; + }); + treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 4, 'children of row 0 are hidden' ); + expect( collapseCount ).toEqual( 4 ); + }); + + it( 'collapseAllRows', function() { + spyOn(gridClassFactory, 'rowTemplateAssigner').andCallFake( function() {}); + + var collapseCount = 0; + grid.api.treeView.on.rowCollapsed( $scope, function(row){ + collapseCount++; + }); + + var treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 2, 'only the level 0 rows are visible' ); + + grid.api.treeView.expandAllRows(); + grid.rows.forEach(function( row ){ + row.visible = true; + }); + treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 10, 'all rows visible' ); + + grid.api.treeView.collapseAllRows(); + grid.rows.forEach(function( row ){ + row.visible = true; + }); + treeRows = uiGridTreeViewService.treeRows.call( grid, grid.rows.slice(0) ); + expect( treeRows.length ).toEqual( 2, 'only level 0 is visible' ); + expect( collapseCount ).toEqual( 6 ); + }); + }); + + + +}); \ No newline at end of file diff --git a/src/js/core/constants.js b/src/js/core/constants.js index 8905d33898..90c0b25d53 100644 --- a/src/js/core/constants.js +++ b/src/js/core/constants.js @@ -7,6 +7,7 @@ CUSTOM_FILTERS: /CUSTOM_FILTERS/g, COL_FIELD: /COL_FIELD/g, MODEL_COL_FIELD: /MODEL_COL_FIELD/g, + TOOLTIP: /title=\"TOOLTIP\"/g, DISPLAY_CELL_TEMPLATE: /DISPLAY_CELL_TEMPLATE/g, TEMPLATE_REGEXP: /<.+>/, FUNC_REGEXP: /(\([^)]*\))?$/, @@ -71,7 +72,9 @@ GREATER_THAN_OR_EQUAL: 64, LESS_THAN: 128, LESS_THAN_OR_EQUAL: 256, - NOT_EQUAL: 512 + NOT_EQUAL: 512, + SELECT: 'select', + INPUT: 'input' }, aggregationTypes: { diff --git a/src/js/core/directives/ui-grid-cell.js b/src/js/core/directives/ui-grid-cell.js index 5e3de7bc57..9b9af47f48 100644 --- a/src/js/core/directives/ui-grid-cell.js +++ b/src/js/core/directives/ui-grid-cell.js @@ -87,13 +87,15 @@ angular.module('ui.grid').directive('uiGridCell', ['$compile', '$parse', 'gridUt }; // TODO(c0bra): Turn this into a deep array watch +/* shouldn't be needed any more given track by col.name var colWatchDereg = $scope.$watch( 'col', cellChangeFunction ); +*/ var rowWatchDereg = $scope.$watch( 'row', cellChangeFunction ); var deregisterFunction = function() { dataChangeDereg(); - colWatchDereg(); +// colWatchDereg(); rowWatchDereg(); }; diff --git a/src/js/core/directives/ui-grid-column-menu.js b/src/js/core/directives/ui-grid-column-menu.js index edd01df6d5..344e345746 100644 --- a/src/js/core/directives/ui-grid-column-menu.js +++ b/src/js/core/directives/ui-grid-column-menu.js @@ -437,8 +437,9 @@ function ($timeout, gridUtil, uiGridConstants, uiGridColumnMenuService) { $scope.hideColumn = function () { $scope.col.colDef.visible = false; + $scope.col.visible = false; - $scope.grid.refresh(); + $scope.grid.queueGridRefresh(); $scope.hideMenu(); $scope.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN ); $scope.grid.api.core.raise.columnVisibilityChanged( $scope.col ); diff --git a/src/js/core/directives/ui-grid-filter.js b/src/js/core/directives/ui-grid-filter.js new file mode 100644 index 0000000000..4389f56e6c --- /dev/null +++ b/src/js/core/directives/ui-grid-filter.js @@ -0,0 +1,27 @@ +(function(){ + 'use strict'; + + angular.module('ui.grid').directive('uiGridFilter', ['$compile', '$templateCache', function ($compile, $templateCache) { + + return { + compile: function() { + return { + pre: function ($scope, $elm, $attrs, controllers) { + $scope.col.updateFilters = function( filterable ){ + $elm.children().remove(); + if ( filterable ){ + var template = $scope.col.filterHeaderTemplate; + + $elm.append($compile(template)($scope)); + } + }; + + $scope.$on( '$destroy', function() { + delete $scope.col.updateFilters; + }); + } + }; + } + }; + }]); +})(); diff --git a/src/js/core/directives/ui-grid-footer-cell.js b/src/js/core/directives/ui-grid-footer-cell.js index 7df6315a41..9bc2438725 100644 --- a/src/js/core/directives/ui-grid-footer-cell.js +++ b/src/js/core/directives/ui-grid-footer-cell.js @@ -22,7 +22,8 @@ //$elm.addClass($scope.col.getColClass(false)); $scope.grid = uiGridCtrl.grid; - $elm.addClass($scope.col.getColClass(false)); + var initColClass = $scope.col.getColClass(false); + $elm.addClass(initColClass); // apply any footerCellClass var classAdded; @@ -46,8 +47,26 @@ updateClass(); } + // Watch for column changes so we can alter the col cell class properly +/* shouldn't be needed any more, given track by col.name + $scope.$watch('col', function (n, o) { + if (n !== o) { + // See if the column's internal class has changed + var newColClass = $scope.col.getColClass(false); + if (newColClass !== initColClass) { + $elm.removeClass(initColClass); + $elm.addClass(newColClass); + initColClass = newColClass; + } + } + }); +*/ + + // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateClass, [uiGridConstants.dataChange.COLUMN]); + // listen for visible rows change and update aggregation values + $scope.grid.api.core.on.rowsRendered( $scope, $scope.col.updateAggregationValue ); $scope.$on( '$destroy', dataChangeDereg ); } diff --git a/src/js/core/directives/ui-grid-footer.js b/src/js/core/directives/ui-grid-footer.js index d6db5297c8..c3a42dad2a 100644 --- a/src/js/core/directives/ui-grid-footer.js +++ b/src/js/core/directives/ui-grid-footer.js @@ -2,7 +2,6 @@ 'use strict'; angular.module('ui.grid').directive('uiGridFooter', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', function ($templateCache, $compile, uiGridConstants, gridUtil, $timeout) { - var defaultTemplate = 'ui-grid/ui-grid-footer'; return { restrict: 'EA', @@ -21,7 +20,7 @@ containerCtrl.footer = $elm; - var footerTemplate = ($scope.grid.options.footerTemplate) ? $scope.grid.options.footerTemplate : defaultTemplate; + var footerTemplate = $scope.grid.options.footerTemplate; gridUtil.getTemplate(footerTemplate) .then(function (contents) { var template = angular.element(contents); diff --git a/src/js/core/directives/ui-grid-grid-footer.js b/src/js/core/directives/ui-grid-grid-footer.js index b38e3366c4..85dd89b875 100644 --- a/src/js/core/directives/ui-grid-grid-footer.js +++ b/src/js/core/directives/ui-grid-grid-footer.js @@ -2,7 +2,6 @@ 'use strict'; angular.module('ui.grid').directive('uiGridGridFooter', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', function ($templateCache, $compile, uiGridConstants, gridUtil, $timeout) { - var defaultTemplate = 'ui-grid/ui-grid-grid-footer'; return { restrict: 'EA', @@ -16,7 +15,9 @@ $scope.grid = uiGridCtrl.grid; - var footerTemplate = ($scope.grid.options.gridFooterTemplate) ? $scope.grid.options.gridFooterTemplate : defaultTemplate; + + + var footerTemplate = $scope.grid.options.gridFooterTemplate; gridUtil.getTemplate(footerTemplate) .then(function (contents) { var template = angular.element(contents); diff --git a/src/js/core/directives/ui-grid-header-cell.js b/src/js/core/directives/ui-grid-header-cell.js index 3697c99bf4..d1877f8594 100644 --- a/src/js/core/directives/ui-grid-header-cell.js +++ b/src/js/core/directives/ui-grid-header-cell.js @@ -5,6 +5,7 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants, ScrollEvent) { // Do stuff after mouse has been down this many ms on the header cell var mousedownTimeout = 500; + var changeModeTimeout = 500; // length of time between a touch event and a mouse event being recognised again, and vice versa var uiGridHeaderCell = { priority: 0, @@ -48,7 +49,143 @@ // apply any headerCellClass var classAdded; - var updateClass = function( grid ){ + + // filter watchers + var filterDeregisters = []; + + + /* + * Our basic approach here for event handlers is that we listen for a down event (mousedown or touchstart). + * Once we have a down event, we need to work out whether we have a click, a drag, or a + * hold. A click would sort the grid (if sortable). A drag would be used by moveable, so + * we ignore it. A hold would open the menu. + * + * So, on down event, we put in place handlers for move and up events, and a timer. If the + * timer expires before we see a move or up, then we have a long press and hence a column menu open. + * If the up happens before the timer, then we have a click, and we sort if the column is sortable. + * If a move happens before the timer, then we are doing column move, so we do nothing, the moveable feature + * will handle it. + * + * To deal with touch enabled devices that also have mice, we only create our handlers when + * we get the down event, and we create the corresponding handlers - if we're touchstart then + * we get touchmove and touchend, if we're mousedown then we get mousemove and mouseup. + * + * We also suppress the click action whilst this is happening - otherwise after the mouseup there + * will be a click event and that can cause the column menu to close + * + */ + + $scope.downFn = function( event ){ + event.stopPropagation(); + + if (typeof(event.originalEvent) !== 'undefined' && event.originalEvent !== undefined) { + event = event.originalEvent; + } + + // Don't show the menu if it's not the left button + if (event.button && event.button !== 0) { + return; + } + + $scope.mousedownStartTime = (new Date()).getTime(); + $scope.mousedownTimeout = $timeout(function() { }, mousedownTimeout); + + $scope.mousedownTimeout.then(function () { + if ( $scope.colMenu ) { + uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm, event); + } + }); + + uiGridCtrl.fireEvent(uiGridConstants.events.COLUMN_HEADER_CLICK, {event: event, columnName: $scope.col.colDef.name}); + + $scope.offAllEvents(); + if ( event.type === 'touchstart'){ + $document.on('touchend', $scope.upFn); + $document.on('touchmove', $scope.moveFn); + } else if ( event.type === 'mousedown' ){ + $document.on('mouseup', $scope.upFn); + $document.on('mousemove', $scope.moveFn); + } + }; + + $scope.upFn = function( event ){ + event.stopPropagation(); + $timeout.cancel($scope.mousedownTimeout); + $scope.offAllEvents(); + $scope.onDownEvents(event.type); + + var mousedownEndTime = (new Date()).getTime(); + var mousedownTime = mousedownEndTime - $scope.mousedownStartTime; + + if (mousedownTime > mousedownTimeout) { + // long click, handled above with mousedown + } + else { + // short click + if ( $scope.sortable ){ + $scope.handleClick(event); + } + } + }; + + $scope.moveFn = function( event ){ + // we're a move, so do nothing and leave for column move (if enabled) to take over + $timeout.cancel($scope.mousedownTimeout); + $scope.offAllEvents(); + $scope.onDownEvents(event.type); + }; + + $scope.clickFn = function ( event ){ + event.stopPropagation(); + $contentsElm.off('click', $scope.clickFn); + }; + + + $scope.offAllEvents = function(){ + $contentsElm.off('touchstart', $scope.downFn); + $contentsElm.off('mousedown', $scope.downFn); + + $document.off('touchend', $scope.upFn); + $document.off('mouseup', $scope.upFn); + + $document.off('touchmove', $scope.moveFn); + $document.off('mousemove', $scope.moveFn); + + $contentsElm.off('click', $scope.clickFn); + }; + + $scope.onDownEvents = function( type ){ + // If there is a previous event, then wait a while before + // activating the other mode - i.e. if the last event was a touch event then + // don't enable mouse events for a wee while (500ms or so) + // Avoids problems with devices that emulate mouse events when you have touch events + + switch (type){ + case 'touchmove': + case 'touchend': + $contentsElm.on('click', $scope.clickFn); + $contentsElm.on('touchstart', $scope.downFn); + $timeout(function(){ + $contentsElm.on('mousedown', $scope.downFn); + }, changeModeTimeout); + break; + case 'mousemove': + case 'mouseup': + $contentsElm.on('click', $scope.clickFn); + $contentsElm.on('mousedown', $scope.downFn); + $timeout(function(){ + $contentsElm.on('touchstart', $scope.downFn); + }, changeModeTimeout); + break; + default: + $contentsElm.on('click', $scope.clickFn); + $contentsElm.on('touchstart', $scope.downFn); + $contentsElm.on('mousedown', $scope.downFn); + } + }; + + + var updateHeaderOptions = function( grid ){ var contents = $elm; if ( classAdded ){ contents.removeClass( classAdded ); @@ -65,8 +202,92 @@ var rightMostContainer = $scope.grid.renderContainers['right'] ? $scope.grid.renderContainers['right'] : $scope.grid.renderContainers['body']; $scope.isLastCol = ( $scope.col === rightMostContainer.visibleColumnCache[ rightMostContainer.visibleColumnCache.length - 1 ] ); + + // Figure out whether this column is sortable or not + if (uiGridCtrl.grid.options.enableSorting && $scope.col.enableSorting) { + $scope.sortable = true; + } + else { + $scope.sortable = false; + } + + // Figure out whether this column is filterable or not + var oldFilterable = $scope.filterable; + if (uiGridCtrl.grid.options.enableFiltering && $scope.col.enableFiltering) { + $scope.filterable = true; + } + else { + $scope.filterable = false; + } + + if ( oldFilterable !== $scope.filterable){ + if ( typeof($scope.col.updateFilters) !== 'undefined' ){ + $scope.col.updateFilters($scope.filterable); + } + + // if column is filterable add a filter watcher + if ($scope.filterable) { + $scope.col.filters.forEach( function(filter, i) { + filterDeregisters.push($scope.$watch('col.filters[' + i + '].term', function(n, o) { + if (n !== o) { + uiGridCtrl.grid.api.core.raise.filterChanged(); + uiGridCtrl.grid.api.core.notifyDataChange( uiGridConstants.dataChange.COLUMN ); + uiGridCtrl.grid.queueGridRefresh(); + } + })); + }); + $scope.$on('$destroy', function() { + filterDeregisters.forEach( function(filterDeregister) { + filterDeregister(); + }); + }); + } else { + filterDeregisters.forEach( function(filterDeregister) { + filterDeregister(); + }); + } + + } + + // figure out whether we support column menus + if ($scope.col.grid.options && $scope.col.grid.options.enableColumnMenus !== false && + $scope.col.colDef && $scope.col.colDef.enableColumnMenu !== false){ + $scope.colMenu = true; + } else { + $scope.colMenu = false; + } + + /** + * @ngdoc property + * @name enableColumnMenu + * @propertyOf ui.grid.class:GridOptions.columnDef + * @description if column menus are enabled, controls the column menus for this specific + * column (i.e. if gridOptions.enableColumnMenus, then you can control column menus + * using this option. If gridOptions.enableColumnMenus === false then you get no column + * menus irrespective of the value of this option ). Defaults to true. + * + */ + /** + * @ngdoc property + * @name enableColumnMenus + * @propertyOf ui.grid.class:GridOptions.columnDef + * @description Override for column menus everywhere - if set to false then you get no + * column menus. Defaults to true. + * + */ + + $scope.offAllEvents(); + + if ($scope.sortable || $scope.colMenu) { + $scope.onDownEvents(); + + $scope.$on('$destroy', function () { + $scope.offAllEvents(); + }); + } }; +/* $scope.$watch('col', function (n, o) { if (n !== o) { // See if the column's internal class has changed @@ -78,40 +299,15 @@ } } }); - - updateClass(); +*/ + updateHeaderOptions(); // Register a data change watch that would get triggered whenever someone edits a cell or modifies column defs - var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateClass, [uiGridConstants.dataChange.COLUMN]); + var dataChangeDereg = $scope.grid.registerDataChangeCallback( updateHeaderOptions, [uiGridConstants.dataChange.COLUMN]); $scope.$on( '$destroy', dataChangeDereg ); - - // Figure out whether this column is sortable or not - if (uiGridCtrl.grid.options.enableSorting && $scope.col.enableSorting) { - $scope.sortable = true; - } - else { - $scope.sortable = false; - } - - // Figure out whether this column is filterable or not - if (uiGridCtrl.grid.options.enableFiltering && $scope.col.enableFiltering) { - $scope.filterable = true; - } - else { - $scope.filterable = false; - } - - // figure out whether we support column menus - if ($scope.col.grid.options && $scope.col.grid.options.enableColumnMenus !== false && - $scope.col.colDef && $scope.col.colDef.enableColumnMenu !== false){ - $scope.colMenu = true; - } else { - $scope.colMenu = false; - } - - function handleClick(event) { + $scope.handleClick = function(event) { // If the shift key is being held down, add this column to the sort var add = false; if (event.shiftKey) { @@ -124,68 +320,8 @@ if (uiGridCtrl.columnMenuScope) { uiGridCtrl.columnMenuScope.hideMenu(); } uiGridCtrl.grid.refresh(); }); - } + }; - /** - * @ngdoc property - * @name enableColumnMenu - * @propertyOf ui.grid.class:GridOptions.columnDef - * @description if column menus are enabled, controls the column menus for this specific - * column (i.e. if gridOptions.enableColumnMenus, then you can control column menus - * using this option. If gridOptions.enableColumnMenus === false then you get no column - * menus irrespective of the value of this option ). Defaults to true. - * - */ - /** - * @ngdoc property - * @name enableColumnMenus - * @propertyOf ui.grid.class:GridOptions.columnDef - * @description Override for column menus everywhere - if set to false then you get no - * column menus. Defaults to true. - * - */ - - if ($scope.sortable || $scope.colMenu) { - // Long-click (for mobile) - var cancelMousedownTimeout; - var mousedownStartTime = 0; - - var downEvent = gridUtil.isTouchEnabled() ? 'touchstart' : 'mousedown'; - $contentsElm.on(downEvent, function(event) { - event.stopPropagation(); - - if (typeof(event.originalEvent) !== 'undefined' && event.originalEvent !== undefined) { - event = event.originalEvent; - } - - // Don't show the menu if it's not the left button - if (event.button && event.button !== 0) { - return; - } - - mousedownStartTime = (new Date()).getTime(); - - cancelMousedownTimeout = $timeout(function() { }, mousedownTimeout); - - cancelMousedownTimeout.then(function () { - if ( $scope.colMenu ) { - uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm, event); - } - }); - - uiGridCtrl.fireEvent(uiGridConstants.events.COLUMN_HEADER_CLICK, {event: event, columnName: $scope.col.colDef.name}); - }); - - var upEvent = gridUtil.isTouchEnabled() ? 'touchend' : 'mouseup'; - $contentsElm.on(upEvent, function () { - $timeout.cancel(cancelMousedownTimeout); - }); - - $scope.$on('$destroy', function () { - $contentsElm.off('mousedown touchstart'); - }); - } - $scope.toggleMenu = function(event) { event.stopPropagation(); @@ -209,49 +345,6 @@ uiGridCtrl.columnMenuScope.showMenu($scope.col, $elm); } }; - - // If this column is sortable, add a click event handler - if ($scope.sortable) { - var clickEvent = gridUtil.isTouchEnabled() ? 'touchend' : 'click'; - $contentsElm.on(clickEvent, function(event) { - event.stopPropagation(); - - $timeout.cancel(cancelMousedownTimeout); - - var mousedownEndTime = (new Date()).getTime(); - var mousedownTime = mousedownEndTime - mousedownStartTime; - - if (mousedownTime > mousedownTimeout) { - // long click, handled above with mousedown - } - else { - // short click - handleClick(event); - } - }); - - $scope.$on('$destroy', function () { - // Cancel any pending long-click timeout - $timeout.cancel(cancelMousedownTimeout); - }); - } - - if ($scope.filterable) { - var filterDeregisters = []; - angular.forEach($scope.col.filters, function(filter, i) { - filterDeregisters.push($scope.$watch('col.filters[' + i + '].term', function(n, o) { - if (n !== o) { - uiGridCtrl.grid.api.core.raise.filterChanged(); - uiGridCtrl.grid.refresh(true); - } - })); - }); - $scope.$on('$destroy', function() { - angular.forEach(filterDeregisters, function(filterDeregister) { - filterDeregister(); - }); - }); - } } }; } diff --git a/src/js/core/directives/ui-grid-header.js b/src/js/core/directives/ui-grid-header.js index 149c646f3d..b72fad95e4 100644 --- a/src/js/core/directives/ui-grid-header.js +++ b/src/js/core/directives/ui-grid-header.js @@ -1,7 +1,8 @@ (function(){ 'use strict'; - angular.module('ui.grid').directive('uiGridHeader', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', function($templateCache, $compile, uiGridConstants, gridUtil, $timeout) { + angular.module('ui.grid').directive('uiGridHeader', ['$templateCache', '$compile', 'uiGridConstants', 'gridUtil', '$timeout', 'ScrollEvent', + function($templateCache, $compile, uiGridConstants, gridUtil, $timeout, ScrollEvent) { var defaultTemplate = 'ui-grid/ui-grid-header'; var emptyTemplate = 'ui-grid/ui-grid-no-header'; @@ -47,8 +48,13 @@ // Inject a reference to the header viewport (if it exists) into the grid controller for use in the horizontal scroll handler below var headerViewport = $elm[0].getElementsByClassName('ui-grid-header-viewport')[0]; + if (headerViewport) { containerCtrl.headerViewport = headerViewport; + angular.element(headerViewport).on('scroll', scrollHandler); + $scope.$on('$destroy', function () { + angular.element(headerViewport).off('scroll', scrollHandler); + }); } } @@ -67,6 +73,22 @@ containerCtrl.headerCanvas = null; } } + + function scrollHandler(evt) { + if (uiGridCtrl.grid.isScrollingHorizontally) { + return; + } + var newScrollLeft = gridUtil.normalizeScrollLeft(containerCtrl.headerViewport, uiGridCtrl.grid); + var horizScrollPercentage = containerCtrl.colContainer.scrollHorizontal(newScrollLeft); + + var scrollEvent = new ScrollEvent(uiGridCtrl.grid, null, containerCtrl.colContainer, ScrollEvent.Sources.ViewPortScroll); + scrollEvent.newScrollLeft = newScrollLeft; + if ( horizScrollPercentage > -1 ){ + scrollEvent.x = { percentage: horizScrollPercentage }; + } + + uiGridCtrl.grid.scrollContainers(null, scrollEvent); + } }, post: function ($scope, $elm, $attrs, controllers) { @@ -81,118 +103,22 @@ gridUtil.disableAnimations($elm); function updateColumnWidths() { - // Get the width of the viewport - var availableWidth = containerCtrl.colContainer.getViewportWidth() - grid.scrollbarWidth; - - //if (typeof(uiGridCtrl.grid.verticalScrollbarWidth) !== 'undefined' && uiGridCtrl.grid.verticalScrollbarWidth !== undefined && uiGridCtrl.grid.verticalScrollbarWidth > 0) { - // availableWidth = availableWidth + uiGridCtrl.grid.verticalScrollbarWidth; - //} - - // The total number of columns - // var equalWidthColumnCount = columnCount = uiGridCtrl.grid.options.columnDefs.length; - // var equalWidth = availableWidth / equalWidthColumnCount; - - var columnCache = containerCtrl.colContainer.visibleColumnCache, - canvasWidth = 0, - asteriskNum = 0, - oneAsterisk = 0, - leftoverWidth = availableWidth, - hasVariableWidth = false; - - var getColWidth = function(column){ - if (column.widthType === "manual"){ - return +column.width; - } - else if (column.widthType === "percent"){ - return parseInt(column.width.replace(/%/g, ''), 10) * availableWidth / 100; - } - else if (column.widthType === "auto"){ - // leftOverWidth is subtracted from after each call to this - // function so we need to calculate oneAsterisk size only once - if (oneAsterisk === 0) { - oneAsterisk = parseInt(leftoverWidth / asteriskNum, 10); - } - return column.width.length * oneAsterisk; - } - }; - - // Populate / determine column width types: - columnCache.forEach(function(column){ - column.widthType = null; - if (isFinite(+column.width)){ - column.widthType = "manual"; - } - else if (gridUtil.endsWith(column.width, "%")){ - column.widthType = "percent"; - hasVariableWidth = true; - } - else if (angular.isString(column.width) && column.width.indexOf('*') !== -1){ - column.widthType = "auto"; - asteriskNum += column.width.length; - hasVariableWidth = true; - } - }); - - // For sorting, calculate width from first to last: - var colWidthPriority = ["manual", "percent", "auto"]; - columnCache.filter(function(column){ - // Only draw visible items with a widthType - return (column.visible && column.widthType); - }).sort(function(a,b){ - // Calculate widths in order, so that manual comes first, etc. - return colWidthPriority.indexOf(a.widthType) - colWidthPriority.indexOf(b.widthType); - }).forEach(function(column){ - // Calculate widths: - var colWidth = getColWidth(column); - if (column.minWidth){ - colWidth = Math.max(colWidth, column.minWidth); - } - if (column.maxWidth){ - colWidth = Math.min(colWidth, column.maxWidth); - } - column.drawnWidth = Math.floor(colWidth); - canvasWidth += column.drawnWidth; - leftoverWidth -= column.drawnWidth; - }); - - // If the grid width didn't divide evenly into the column widths and we have pixels left over, dole them out to the columns one by one to make everything fit - if (hasVariableWidth && leftoverWidth > 0 && canvasWidth > 0 && canvasWidth < availableWidth) { - var remFn = function (column) { - if (leftoverWidth > 0 && (column.widthType === "auto" || column.widthType === "percent")) { - column.drawnWidth = column.drawnWidth + 1; - canvasWidth = canvasWidth + 1; - leftoverWidth--; - } - }; - var prevLeftover = 0; - do { - prevLeftover = leftoverWidth; - columnCache.forEach(remFn); - } while (leftoverWidth > 0 && leftoverWidth !== prevLeftover ); - } - canvasWidth = Math.max(canvasWidth, availableWidth); + // this styleBuilder always runs after the renderContainer, so we can rely on the column widths + // already being populated correctly + var columnCache = containerCtrl.colContainer.visibleColumnCache; + // Build the CSS // uiGridCtrl.grid.columns.forEach(function (column) { var ret = ''; + var canvasWidth = 0; columnCache.forEach(function (column) { ret = ret + column.getColClassDefinition(); + canvasWidth += column.drawnWidth; }); - // Add the vertical scrollbar width back in to the canvas width, it's taken out in getViewportWidth - //if (grid.verticalScrollbarWidth) { - // canvasWidth = canvasWidth + grid.verticalScrollbarWidth; - //} - // canvasWidth = canvasWidth + 1; - - // if we have a grid menu, then we prune the width of the last column header - // to allow room for the button whilst still getting to the column menu - if (columnCache.length > 0) { // && grid.options.enableGridMenu) { - columnCache[columnCache.length - 1].headerWidth = columnCache[columnCache.length - 1].drawnWidth - 30; - } - - containerCtrl.colContainer.canvasWidth = parseInt(canvasWidth, 10); - + containerCtrl.colContainer.canvasWidth = canvasWidth; + // Return the styles back to buildStyles which pops them into the `customStyles` scope variable return ret; } @@ -207,7 +133,7 @@ //todo: remove this if by injecting gridCtrl into unit tests if (uiGridCtrl) { uiGridCtrl.grid.registerStyleComputation({ - priority: 5, + priority: 15, func: updateColumnWidths }); } diff --git a/src/js/core/directives/ui-grid-menu-button.js b/src/js/core/directives/ui-grid-menu-button.js index 09e9741d94..f5c5a7a04b 100644 --- a/src/js/core/directives/ui-grid-menu-button.js +++ b/src/js/core/directives/ui-grid-menu-button.js @@ -195,6 +195,11 @@ angular.module('ui.grid') if ( $scope.grid.options.gridMenuShowHideColumns !== false ){ menuItems = menuItems.concat( service.showHideColumns( $scope ) ); } + + menuItems.sort(function(a, b){ + return a.order - b.order; + }); + return menuItems; }, @@ -235,7 +240,8 @@ angular.module('ui.grid') // add header for columns showHideColumns.push({ - title: i18nService.getSafeText('gridMenu.columns') + title: i18nService.getSafeText('gridMenu.columns'), + order: 300 }); $scope.grid.options.gridMenuTitleFilter = $scope.grid.options.gridMenuTitleFilter ? $scope.grid.options.gridMenuTitleFilter : function( title ) { return title; }; @@ -252,7 +258,9 @@ angular.module('ui.grid') shown: function() { return this.context.gridCol.colDef.visible === true || this.context.gridCol.colDef.visible === undefined; }, - context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) } + context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) }, + leaveOpen: true, + order: 301 + index * 2 }; service.setMenuItemTitle( menuItem, colDef, $scope.grid ); showHideColumns.push( menuItem ); @@ -267,15 +275,14 @@ angular.module('ui.grid') shown: function() { return !(this.context.gridCol.colDef.visible === true || this.context.gridCol.colDef.visible === undefined); }, - context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) } + context: { gridCol: $scope.grid.getColumn(colDef.name || colDef.field) }, + leaveOpen: true, + order: 301 + index * 2 + 1 }; service.setMenuItemTitle( menuItem, colDef, $scope.grid ); showHideColumns.push( menuItem ); } }); - showHideColumns.forEach( function (menuItem) { - menuItem.templateUrl = $scope.grid.options.menuItemTemplate; - }); return showHideColumns; }, @@ -335,52 +342,41 @@ angular.module('ui.grid') -.directive('uiGridMenuButton', ['$compile', 'gridUtil', 'uiGridConstants', 'uiGridGridMenuService', -function ($compile, gridUtil, uiGridConstants, uiGridGridMenuService) { - var defaultTemplate = 'ui-grid/ui-grid-menu-button'; +.directive('uiGridMenuButton', ['gridUtil', 'uiGridConstants', 'uiGridGridMenuService', +function (gridUtil, uiGridConstants, uiGridGridMenuService) { + return { priority: 0, scope: true, require: ['?^uiGrid'], + templateUrl: 'ui-grid/ui-grid-menu-button', replace: true, - compile: function ($elm, $attrs) { - return { - pre: function($scope, $elm, $attrs, controllers) { - var uiGridCtrl = controllers[0]; - var menuButtonTemplate = (uiGridCtrl.grid && uiGridCtrl.grid.options.menuButtonTemplate) ? uiGridCtrl.grid.options.menuButtonTemplate : defaultTemplate; - $scope.menuTemplate = uiGridCtrl.grid.options.menuTemplate; - gridUtil.getTemplate(menuButtonTemplate) - .then(function (contents) { - var template = angular.element(contents); - $elm.replaceWith(template); - $compile(template)($scope); - }); - }, - post: function($scope, $elm, $attrs, controllers) { - var uiGridCtrl = controllers[0]; - uiGridGridMenuService.initialize($scope, uiGridCtrl.grid); - $scope.shown = false; - $scope.toggleMenu = function () { - if ( $scope.shown ){ - $scope.$broadcast('hide-menu'); - $scope.shown = false; - } else { - $scope.menuItems = uiGridGridMenuService.getMenuItems( $scope ); - $scope.$broadcast('show-menu'); - $scope.shown = true; - } - }; - $scope.$on('menu-hidden', function() { - $scope.shown = false; - }); - } + link: function ($scope, $elm, $attrs, controllers) { + var uiGridCtrl = controllers[0]; - }; + uiGridGridMenuService.initialize($scope, uiGridCtrl.grid); + + $scope.shown = false; + + $scope.toggleMenu = function () { + if ( $scope.shown ){ + $scope.$broadcast('hide-menu'); + $scope.shown = false; + } else { + $scope.menuItems = uiGridGridMenuService.getMenuItems( $scope ); + $scope.$broadcast('show-menu'); + $scope.shown = true; + } + }; + + $scope.$on('menu-hidden', function() { + $scope.shown = false; + }); } }; }]); -})(); +})(); \ No newline at end of file diff --git a/src/js/core/directives/ui-grid-menu.js b/src/js/core/directives/ui-grid-menu.js index 9938da2bb1..3f1658e045 100644 --- a/src/js/core/directives/ui-grid-menu.js +++ b/src/js/core/directives/ui-grid-menu.js @@ -32,147 +32,129 @@ angular.module('ui.grid') .directive('uiGridMenu', ['$compile', '$timeout', '$window', '$document', 'gridUtil', 'uiGridConstants', function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { - var defaultTemplate = 'ui-grid/uiGridMenu'; var uiGridMenu = { priority: 0, scope: { // shown: '&', menuItems: '=', - autoHide: '=?', - templateUrl: '=' + autoHide: '=?' }, require: '?^uiGrid', + templateUrl: 'ui-grid/uiGridMenu', replace: false, - compile: function ($elm, $attrs) { - return { - pre: function ($scope, $elm, $attrs, uiGridCtrl) { - if ( uiGridCtrl ) { - $scope.grid = uiGridCtrl.grid; - } - - var menuTemplate = $scope.templateUrl || defaultTemplate; - gridUtil.getTemplate(menuTemplate) - .then(function (contents) { - var template = angular.element(contents); - $elm.append(template); - $compile(template)($scope); - }); - }, - - post: function ($scope, $elm, $attrs, uiGridCtrl) { - var self = this; - var menuMid; - var $animate; - - // *** Show/Hide functions ****** - self.showMenu = $scope.showMenu = function(event, args) { - if ( !$scope.shown ){ - - /* - * In order to animate cleanly we remove the ng-if, wait a digest cycle, then - * animate the removal of the ng-hide. We can't successfully (so far as I can tell) - * animate removal of the ng-if, as the menu items aren't there yet. And we don't want - * to rely on ng-show only, as that leaves elements in the DOM that are needlessly evaluated - * on scroll events. - * - * Note when testing animation that animations don't run on the tutorials. When debugging it looks - * like they do, but angular has a default $animate provider that is just a stub, and that's what's - * being called. ALso don't be fooled by the fact that your browser has actually loaded the - * angular-translate.js, it's not using it. You need to test animations in an external application. - */ - $scope.shown = true; - - $timeout( function() { - $scope.shownMid = true; - $scope.$emit('menu-shown'); - }); - } else if ( !$scope.shownMid ) { - // we're probably doing a hide then show, so we don't need to wait for ng-if - $scope.shownMid = true; - $scope.$emit('menu-shown'); - } + link: function ($scope, $elm, $attrs, uiGridCtrl) { + var self = this; + var menuMid; + var $animate; + + // *** Show/Hide functions ****** + self.showMenu = $scope.showMenu = function(event, args) { + if ( !$scope.shown ){ + + /* + * In order to animate cleanly we remove the ng-if, wait a digest cycle, then + * animate the removal of the ng-hide. We can't successfully (so far as I can tell) + * animate removal of the ng-if, as the menu items aren't there yet. And we don't want + * to rely on ng-show only, as that leaves elements in the DOM that are needlessly evaluated + * on scroll events. + * + * Note when testing animation that animations don't run on the tutorials. When debugging it looks + * like they do, but angular has a default $animate provider that is just a stub, and that's what's + * being called. ALso don't be fooled by the fact that your browser has actually loaded the + * angular-translate.js, it's not using it. You need to test animations in an external application. + */ + $scope.shown = true; + + $timeout( function() { + $scope.shownMid = true; + $scope.$emit('menu-shown'); + }); + } else if ( !$scope.shownMid ) { + // we're probably doing a hide then show, so we don't need to wait for ng-if + $scope.shownMid = true; + $scope.$emit('menu-shown'); + } - var docEventType = 'click'; - if (args && args.originalEvent && args.originalEvent.type && args.originalEvent.type === 'touchstart') { - docEventType = args.originalEvent.type; - } + var docEventType = 'click'; + if (args && args.originalEvent && args.originalEvent.type && args.originalEvent.type === 'touchstart') { + docEventType = args.originalEvent.type; + } - // Turn off an existing document click handler - angular.element(document).off('click touchstart', applyHideMenu); + // Turn off an existing document click handler + angular.element(document).off('click touchstart', applyHideMenu); - // Turn on the document click handler, but in a timeout so it doesn't apply to THIS click if there is one - $timeout(function() { - angular.element(document).on(docEventType, applyHideMenu); - }); - }; + // Turn on the document click handler, but in a timeout so it doesn't apply to THIS click if there is one + $timeout(function() { + angular.element(document).on(docEventType, applyHideMenu); + }); + }; - self.hideMenu = $scope.hideMenu = function(event, args) { - if ( $scope.shown ){ - /* - * In order to animate cleanly we animate the addition of ng-hide, then use a $timeout to - * set the ng-if (shown = false) after the animation runs. In theory we can cascade off the - * callback on the addClass method, but it is very unreliable with unit tests for no discernable reason. - * - * The user may have clicked on the menu again whilst - * we're waiting, so we check that the mid isn't shown before applying the ng-if. - */ - $scope.shownMid = false; - $timeout( function() { - if ( !$scope.shownMid ){ - $scope.shown = false; - $scope.$emit('menu-hidden'); - } - }, 200); + self.hideMenu = $scope.hideMenu = function(event, args) { + if ( $scope.shown ){ + /* + * In order to animate cleanly we animate the addition of ng-hide, then use a $timeout to + * set the ng-if (shown = false) after the animation runs. In theory we can cascade off the + * callback on the addClass method, but it is very unreliable with unit tests for no discernable reason. + * + * The user may have clicked on the menu again whilst + * we're waiting, so we check that the mid isn't shown before applying the ng-if. + */ + $scope.shownMid = false; + $timeout( function() { + if ( !$scope.shownMid ){ + $scope.shown = false; + $scope.$emit('menu-hidden'); } + }, 200); + } - angular.element(document).off('click touchstart', applyHideMenu); - }; - - $scope.$on('hide-menu', function (event, args) { - $scope.hideMenu(event, args); - }); - - $scope.$on('show-menu', function (event, args) { - $scope.showMenu(event, args); - }); + angular.element(document).off('click touchstart', applyHideMenu); + }; - - // *** Auto hide when click elsewhere ****** - var applyHideMenu = function(){ - if ($scope.shown) { - $scope.$apply(function () { - $scope.hideMenu(); - }); - } - }; - - if (typeof($scope.autoHide) === 'undefined' || $scope.autoHide === undefined) { - $scope.autoHide = true; - } + $scope.$on('hide-menu', function (event, args) { + $scope.hideMenu(event, args); + }); - if ($scope.autoHide) { - angular.element($window).on('resize', applyHideMenu); - } + $scope.$on('show-menu', function (event, args) { + $scope.showMenu(event, args); + }); - $scope.$on('$destroy', function () { - angular.element(document).off('click touchstart', applyHideMenu); + + // *** Auto hide when click elsewhere ****** + var applyHideMenu = function(){ + if ($scope.shown) { + $scope.$apply(function () { + $scope.hideMenu(); }); + } + }; + + if (typeof($scope.autoHide) === 'undefined' || $scope.autoHide === undefined) { + $scope.autoHide = true; + } + if ($scope.autoHide) { + angular.element($window).on('resize', applyHideMenu); + } - $scope.$on('$destroy', function() { - angular.element($window).off('resize', applyHideMenu); - }); + $scope.$on('$destroy', function () { + angular.element(document).off('click touchstart', applyHideMenu); + }); + - if (uiGridCtrl) { - $scope.$on('$destroy', uiGridCtrl.grid.api.core.on.scrollEvent($scope, applyHideMenu )); - } + $scope.$on('$destroy', function() { + angular.element($window).off('resize', applyHideMenu); + }); - $scope.$on('$destroy', $scope.$on(uiGridConstants.events.ITEM_DRAGGING, applyHideMenu )); - } - }; - }, + if (uiGridCtrl) { + $scope.$on('$destroy', uiGridCtrl.grid.api.core.on.scrollBegin($scope, applyHideMenu )); + } + $scope.$on('$destroy', $scope.$on(uiGridConstants.events.ITEM_DRAGGING, applyHideMenu )); + }, + + controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) { var self = this; }] @@ -182,7 +164,6 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { }]) .directive('uiGridMenuItem', ['gridUtil', '$compile', 'i18nService', function (gridUtil, $compile, i18nService) { - var defaultTemplate = 'ui-grid/uiGridMenuItem'; var uiGridMenuItem = { priority: 0, scope: { @@ -192,22 +173,27 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { icon: '=', shown: '=', context: '=', - templateUrl: '=' + templateUrl: '=', + leaveOpen: '=' }, require: ['?^uiGrid', '^uiGridMenu'], + templateUrl: 'ui-grid/uiGridMenuItem', replace: true, compile: function($elm, $attrs) { return { pre: function ($scope, $elm, $attrs, controllers) { var uiGridCtrl = controllers[0], uiGridMenuCtrl = controllers[1]; - var menuItemTemplate = $scope.templateUrl || defaultTemplate; - gridUtil.getTemplate(menuItemTemplate) - .then(function (contents) { - var template = angular.element(contents); - $compile(template)($scope); - $elm.replaceWith(template); - }); + + if ($scope.templateUrl) { + gridUtil.getTemplate($scope.templateUrl) + .then(function (contents) { + var template = angular.element(contents); + + var newElm = $compile(template)($scope); + $elm.replaceWith(newElm); + }); + } }, post: function ($scope, $elm, $attrs, controllers) { var uiGridCtrl = controllers[0], @@ -252,7 +238,9 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { $scope.action.call(context, $event, title); - $scope.$emit('hide-menu'); + if ( !$scope.leaveOpen ){ + $scope.$emit('hide-menu'); + } } }; @@ -265,4 +253,4 @@ function ($compile, $timeout, $window, $document, gridUtil, uiGridConstants) { return uiGridMenuItem; }]); -})(); +})(); \ No newline at end of file diff --git a/src/js/core/directives/ui-grid-render-container.js b/src/js/core/directives/ui-grid-render-container.js index 02044d89b2..b8ee5ed299 100644 --- a/src/js/core/directives/ui-grid-render-container.js +++ b/src/js/core/directives/ui-grid-render-container.js @@ -2,7 +2,7 @@ 'use strict'; var module = angular.module('ui.grid'); - + module.directive('uiGridRenderContainer', ['$timeout', '$document', 'uiGridConstants', 'gridUtil', 'ScrollEvent', function($timeout, $document, uiGridConstants, gridUtil, ScrollEvent) { return { @@ -23,11 +23,9 @@ compile: function () { return { pre: function prelink($scope, $elm, $attrs, controllers) { - // gridUtil.logDebug('render container ' + $scope.containerId + ' pre-link'); var uiGridCtrl = controllers[0]; var containerCtrl = controllers[1]; - var grid = $scope.grid = uiGridCtrl.grid; // Verify that the render container for this element exists @@ -47,13 +45,12 @@ var rowContainer = $scope.rowContainer = grid.renderContainers[$scope.rowContainerName]; var colContainer = $scope.colContainer = grid.renderContainers[$scope.colContainerName]; - + containerCtrl.containerId = $scope.containerId; containerCtrl.rowContainer = rowContainer; containerCtrl.colContainer = colContainer; }, post: function postlink($scope, $elm, $attrs, controllers) { - // gridUtil.logDebug('render container ' + $scope.containerId + ' post-link'); var uiGridCtrl = controllers[0]; var containerCtrl = controllers[1]; @@ -61,72 +58,24 @@ var grid = uiGridCtrl.grid; var rowContainer = containerCtrl.rowContainer; var colContainer = containerCtrl.colContainer; + var scrollTop = null; + var scrollLeft = null; + var renderContainer = grid.renderContainers[$scope.containerId]; // Put the container name on this element as a class $elm.addClass('ui-grid-render-container-' + $scope.containerId); - // Bind to left/right-scroll events - if ($scope.bindScrollHorizontal || $scope.bindScrollVertical) { - grid.api.core.on.scrollEvent($scope,scrollHandler); - } - - function scrollHandler (args) { - // exit if not for this grid - if (args.grid && args.grid.id !== grid.id){ - return; - } - - - // Vertical scroll - if (args.y && $scope.bindScrollVertical) { - containerCtrl.prevScrollArgs = args; - - var newScrollTop = args.getNewScrollTop(rowContainer,containerCtrl.viewport); - - //only set scrollTop if we coming from something other than viewPort scrollBar or - //another column container - if (args.source !== ScrollEvent.Sources.ViewPortScroll || - args.sourceColContainer !== colContainer) { - containerCtrl.viewport[0].scrollTop = newScrollTop; - } - - } - - // Horizontal scroll - if (args.x && $scope.bindScrollHorizontal) { - containerCtrl.prevScrollArgs = args; - var newScrollLeft = args.getNewScrollLeft(colContainer,containerCtrl.viewport); - - // Make the current horizontal scroll position available in the $scope - $scope.newScrollLeft = newScrollLeft; - - if (containerCtrl.headerViewport) { - containerCtrl.headerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.headerViewport, newScrollLeft); - } - - if (containerCtrl.footerViewport) { - containerCtrl.footerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.footerViewport, newScrollLeft); - } - - // Scroll came from somewhere else, so the viewport must be positioned - if (args.source !== ScrollEvent.Sources.ViewPortScroll) { - containerCtrl.viewport[0].scrollLeft = newScrollLeft; - } - - containerCtrl.prevScrollLeft = newScrollLeft; - } - } - // Scroll the render container viewport when the mousewheel is used gridUtil.on.mousewheel($elm, function (event) { var scrollEvent = new ScrollEvent(grid, rowContainer, colContainer, ScrollEvent.Sources.RenderContainerMouseWheel); if (event.deltaY !== 0) { var scrollYAmount = event.deltaY * -1 * event.deltaFactor; + scrollTop = containerCtrl.viewport[0].scrollTop; // Get the scroll percentage - var scrollYPercentage = (containerCtrl.viewport[0].scrollTop + scrollYAmount) / rowContainer.getVerticalScrollLength(); + var scrollYPercentage = (scrollTop + scrollYAmount) / rowContainer.getVerticalScrollLength(); // Keep scrollPercentage within the range 0-1. if (scrollYPercentage < 0) { scrollYPercentage = 0; } @@ -138,7 +87,7 @@ var scrollXAmount = event.deltaX * event.deltaFactor; // Get the scroll percentage - var scrollLeft = gridUtil.normalizeScrollLeft(containerCtrl.viewport); + scrollLeft = gridUtil.normalizeScrollLeft(containerCtrl.viewport, grid); var scrollXPercentage = (scrollLeft + scrollXAmount) / (colContainer.getCanvasWidth() - colContainer.getViewportWidth()); // Keep scrollPercentage within the range 0-1. @@ -148,14 +97,16 @@ scrollEvent.x = { percentage: scrollXPercentage, pixels: scrollXAmount }; } - // Let the parent container scroll if the grid is already at the top/bottom - if ((scrollEvent.y && scrollEvent.y.percentage !== 0 && scrollEvent.y.percentage !== 1 && containerCtrl.viewport[0].scrollTop !== 0 ) || - (scrollEvent.x && scrollEvent.x.percentage !== 0 && scrollEvent.x.percentage !== 1)) { - - event.preventDefault(); - scrollEvent.fireThrottledScrollingEvent(); + if ((event.deltaY !== 0 && (scrollEvent.atTop(scrollTop) || scrollEvent.atBottom(scrollTop))) || + (event.deltaX !== 0 && (scrollEvent.atLeft(scrollLeft) || scrollEvent.atRight(scrollLeft)))) { + //parent controller scrolls + } + else { + event.preventDefault(); + scrollEvent.fireThrottledScrollingEvent('', scrollEvent); } + }); $elm.bind('$destroy', function() { @@ -165,12 +116,12 @@ $elm.unbind(eventName); }); }); - + // TODO(c0bra): Handle resizing the inner canvas based on the number of elements function update() { var ret = ''; - var canvasWidth = colContainer.getCanvasWidth(); + var canvasWidth = colContainer.canvasWidth; var viewportWidth = colContainer.getViewportWidth(); var canvasHeight = rowContainer.getCanvasHeight(); @@ -182,9 +133,10 @@ var viewportHeight = rowContainer.getViewportHeight(); - var headerViewportWidth = colContainer.getHeaderViewportWidth(); - var footerViewportWidth = colContainer.getHeaderViewportWidth(); - + var headerViewportWidth, + footerViewportWidth; + headerViewportWidth = footerViewportWidth = colContainer.getHeaderViewportWidth(); + // Set canvas dimensions ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-canvas { width: ' + canvasWidth + 'px; height: ' + canvasHeight + 'px; }'; @@ -193,16 +145,19 @@ if (renderContainer.explicitHeaderCanvasHeight) { ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-canvas { height: ' + renderContainer.explicitHeaderCanvasHeight + 'px; }'; } - + else { + ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-canvas { height: inherit; }'; + } + ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-viewport { width: ' + viewportWidth + 'px; height: ' + viewportHeight + 'px; }'; ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-header-viewport { width: ' + headerViewportWidth + 'px; }'; - ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-footer-canvas { width: ' + canvasWidth + grid.scrollbarWidth + 'px; }'; + ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-footer-canvas { width: ' + (canvasWidth + grid.scrollbarWidth) + 'px; }'; ret += '\n .grid' + uiGridCtrl.grid.id + ' .ui-grid-render-container-' + $scope.containerId + ' .ui-grid-footer-viewport { width: ' + footerViewportWidth + 'px; }'; return ret; } - + uiGridCtrl.grid.registerStyleComputation({ priority: 6, func: update @@ -215,7 +170,7 @@ }]); module.controller('uiGridRenderContainer', ['$scope', 'gridUtil', function ($scope, gridUtil) { - + }]); })(); diff --git a/src/js/core/directives/ui-grid-viewport.js b/src/js/core/directives/ui-grid-viewport.js index 5db29e0565..04b4ba8c71 100644 --- a/src/js/core/directives/ui-grid-viewport.js +++ b/src/js/core/directives/ui-grid-viewport.js @@ -1,8 +1,8 @@ (function(){ 'use strict'; - angular.module('ui.grid').directive('uiGridViewport', ['gridUtil','ScrollEvent','uiGridConstants', - function(gridUtil, ScrollEvent, uiGridConstants) { + angular.module('ui.grid').directive('uiGridViewport', ['gridUtil','ScrollEvent','uiGridConstants', '$log', + function(gridUtil, ScrollEvent, uiGridConstants, $log) { return { replace: true, scope: {}, @@ -31,62 +31,81 @@ // Register this viewport with its container containerCtrl.viewport = $elm; - $elm.on('scroll', function (evt) { - var newScrollTop = $elm[0].scrollTop; - // var newScrollLeft = $elm[0].scrollLeft; - var newScrollLeft = gridUtil.normalizeScrollLeft($elm); - var horizScrollPercentage = -1; - var vertScrollPercentage = -1; - // Handle RTL here + $elm.on('scroll', scrollHandler); - if (newScrollLeft !== colContainer.prevScrollLeft) { - grid.flagScrollingHorizontally(); - var xDiff = newScrollLeft - colContainer.prevScrollLeft; + var ignoreScroll = false; - if (xDiff > 0) { grid.scrollDirection = uiGridConstants.scrollDirection.RIGHT; } - if (xDiff < 0) { grid.scrollDirection = uiGridConstants.scrollDirection.LEFT; } + function scrollHandler(evt) { + //Leaving in this commented code in case it can someday be used + //It does improve performance, but because the horizontal scroll is normalized, + // using this code will lead to the column header getting slightly out of line with columns + // + //if (ignoreScroll && (grid.isScrollingHorizontally || grid.isScrollingHorizontally)) { + // //don't ask for scrollTop if we just set it + // ignoreScroll = false; + // return; + //} + //ignoreScroll = true; - var horizScrollLength = (colContainer.getCanvasWidth() - colContainer.getViewportWidth()); - if (horizScrollLength !== 0) { - horizScrollPercentage = newScrollLeft / horizScrollLength; - } - else { - horizScrollPercentage = 0; - } + var newScrollTop = $elm[0].scrollTop; + var newScrollLeft = gridUtil.normalizeScrollLeft($elm, grid); + + var vertScrollPercentage = rowContainer.scrollVertical(newScrollTop); + var horizScrollPercentage = colContainer.scrollHorizontal(newScrollLeft); + + var scrollEvent = new ScrollEvent(grid, rowContainer, colContainer, ScrollEvent.Sources.ViewPortScroll); + scrollEvent.newScrollLeft = newScrollLeft; + scrollEvent.newScrollTop = newScrollTop; + if ( horizScrollPercentage > -1 ){ + scrollEvent.x = { percentage: horizScrollPercentage }; + } - colContainer.adjustScrollHorizontal(newScrollLeft, horizScrollPercentage); + if ( vertScrollPercentage > -1 ){ + scrollEvent.y = { percentage: vertScrollPercentage }; } - if (newScrollTop !== rowContainer.prevScrollTop) { - grid.flagScrollingVertically(); - var yDiff = newScrollTop - rowContainer.prevScrollTop; + grid.scrollContainers($scope.$parent.containerId, scrollEvent); + } + + if ($scope.$parent.bindScrollVertical) { + grid.addVerticalScrollSync($scope.$parent.containerId, syncVerticalScroll); + } - if (yDiff > 0 ) { grid.scrollDirection = uiGridConstants.scrollDirection.DOWN; } - if (yDiff < 0 ) { grid.scrollDirection = uiGridConstants.scrollDirection.UP; } + if ($scope.$parent.bindScrollHorizontal) { + grid.addHorizontalScrollSync($scope.$parent.containerId, syncHorizontalScroll); + grid.addHorizontalScrollSync($scope.$parent.containerId + 'header', syncHorizontalHeader); + grid.addHorizontalScrollSync($scope.$parent.containerId + 'footer', syncHorizontalFooter); + } - var vertScrollLength = rowContainer.getVerticalScrollLength(); + function syncVerticalScroll(scrollEvent){ + containerCtrl.prevScrollArgs = scrollEvent; + var newScrollTop = scrollEvent.getNewScrollTop(rowContainer,containerCtrl.viewport); + $elm[0].scrollTop = newScrollTop; - vertScrollPercentage = newScrollTop / vertScrollLength; + } - if (vertScrollPercentage > 1) { vertScrollPercentage = 1; } - if (vertScrollPercentage < 0) { vertScrollPercentage = 0; } - - rowContainer.adjustScrollVertical(newScrollTop, vertScrollPercentage); + function syncHorizontalScroll(scrollEvent){ + containerCtrl.prevScrollArgs = scrollEvent; + var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport); + $elm[0].scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid); + } + + function syncHorizontalHeader(scrollEvent){ + var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport); + if (containerCtrl.headerViewport) { + containerCtrl.headerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid); } + } + + function syncHorizontalFooter(scrollEvent){ + var newScrollLeft = scrollEvent.getNewScrollLeft(colContainer, containerCtrl.viewport); + if (containerCtrl.footerViewport) { + containerCtrl.footerViewport.scrollLeft = gridUtil.denormalizeScrollLeft(containerCtrl.viewport,newScrollLeft, grid); + } + } - var scrollEvent = new ScrollEvent(grid, rowContainer, colContainer, ScrollEvent.Sources.ViewPortScroll); - scrollEvent.newScrollLeft = newScrollLeft; - scrollEvent.newScrollTop = newScrollTop; - if ( horizScrollPercentage > -1 ){ - scrollEvent.x = { percentage: horizScrollPercentage }; - } - if ( vertScrollPercentage > -1 ){ - scrollEvent.y = { percentage: vertScrollPercentage }; - } - scrollEvent.fireScrollingEvent(); - }); }, controller: ['$scope', function ($scope) { this.rowStyle = function (index) { diff --git a/src/js/core/directives/ui-grid.js b/src/js/core/directives/ui-grid.js index 97ce1fe9b7..c235316f5e 100644 --- a/src/js/core/directives/ui-grid.js +++ b/src/js/core/directives/ui-grid.js @@ -2,9 +2,9 @@ 'use strict'; angular.module('ui.grid').controller('uiGridController', ['$scope', '$element', '$attrs', 'gridUtil', '$q', 'uiGridConstants', - '$templateCache', 'gridClassFactory', '$timeout', '$parse', '$compile', 'ScrollEvent', + '$templateCache', 'gridClassFactory', '$timeout', '$parse', '$compile', function ($scope, $elm, $attrs, gridUtil, $q, uiGridConstants, - $templateCache, gridClassFactory, $timeout, $parse, $compile, ScrollEvent) { + $templateCache, gridClassFactory, $timeout, $parse, $compile) { // gridUtil.logDebug('ui-grid controller'); var self = this; @@ -36,15 +36,34 @@ } - var dataWatchCollectionDereg; - if (angular.isString($scope.uiGrid.data)) { - dataWatchCollectionDereg = $scope.$parent.$watchCollection($scope.uiGrid.data, dataWatchFunction); - } - else { - dataWatchCollectionDereg = $scope.$parent.$watchCollection(function() { return $scope.uiGrid.data; }, dataWatchFunction); + // if fastWatch is set we watch only the length and the reference, not every individual object + var deregFunctions = []; + if (self.grid.options.fastWatch) { + self.uiGrid = $scope.uiGrid; + if (angular.isString($scope.uiGrid.data)) { + deregFunctions.push( $scope.$parent.$watch($scope.uiGrid.data, dataWatchFunction) ); + deregFunctions.push( $scope.$parent.$watch(function() { + if ( self.grid.appScope[$scope.uiGrid.data] ){ + return self.grid.appScope[$scope.uiGrid.data].length; + } else { + return undefined; + } + }, dataWatchFunction) ); + } else { + deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.data; }, dataWatchFunction) ); + deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.data.length; }, dataWatchFunction) ); + } + deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.columnDefs; }, columnDefsWatchFunction) ); + deregFunctions.push( $scope.$parent.$watch(function() { return $scope.uiGrid.columnDefs.length; }, columnDefsWatchFunction) ); + } else { + if (angular.isString($scope.uiGrid.data)) { + deregFunctions.push( $scope.$parent.$watchCollection($scope.uiGrid.data, dataWatchFunction) ); + } else { + deregFunctions.push( $scope.$parent.$watchCollection(function() { return $scope.uiGrid.data; }, dataWatchFunction) ); + } + deregFunctions.push( $scope.$parent.$watchCollection(function() { return $scope.uiGrid.columnDefs; }, columnDefsWatchFunction) ); } - - var columnDefWatchCollectionDereg = $scope.$parent.$watchCollection(function() { return $scope.uiGrid.columnDefs; }, columnDefsWatchFunction); + function columnDefsWatchFunction(n, o) { if (n && n !== o) { @@ -59,27 +78,18 @@ } } - function adjustInfiniteScrollPosition (scrollToRow) { - - var scrollEvent = new ScrollEvent(self.grid, null, null, 'ui.grid.adjustInfiniteScrollPosition'); - var totalRows = self.grid.renderContainers.body.visibleRowCache.length; - var percentage = ( scrollToRow + ( scrollToRow / ( totalRows - 1 ) ) ) / totalRows; - - //for infinite scroll, never allow it to be at the zero position so the up button can be active - if ( percentage === 0 ) { - scrollEvent.y = {pixels: 1}; - } - else { - scrollEvent.y = {percentage: percentage}; - } - scrollEvent.fireScrollingEvent(); - - } - function dataWatchFunction(newData) { // gridUtil.logDebug('dataWatch fired'); var promises = []; + if ( self.grid.options.fastWatch ){ + if (angular.isString($scope.uiGrid.data)) { + newData = self.grid.appScope[$scope.uiGrid.data]; + } else { + newData = $scope.uiGrid.data; + } + } + if (newData) { if ( // If we have no columns (i.e. columns length is either 0 or equal to the number of row header columns, which don't count because they're created automatically) @@ -114,20 +124,6 @@ $scope.$evalAsync(function() { self.grid.refreshCanvas(true); self.grid.callDataChangeCallbacks(uiGridConstants.dataChange.ROW); - - $timeout(function () { - //Process post load scroll events if using infinite scroll - if ( self.grid.options.enableInfiniteScroll ) { - //If first load, seed the scrollbar down a little to activate the button - if ( self.grid.renderContainers.body.prevRowScrollIndex === 0 ) { - adjustInfiniteScrollPosition(0); - } - //If we are scrolling up, we need to reseed the grid. - if (self.grid.scrollDirection === uiGridConstants.scrollDirection.UP) { - adjustInfiniteScrollPosition(self.grid.renderContainers.body.prevRowScrollIndex + 1 + self.grid.options.excessRows); - } - } - }, 0); }); }); }); @@ -139,8 +135,7 @@ }); $scope.$on('$destroy', function() { - dataWatchCollectionDereg(); - columnDefWatchCollectionDereg(); + deregFunctions.forEach( function( deregFn ){ deregFn(); }); styleWatchDereg(); }); @@ -191,124 +186,148 @@ */ -angular.module('ui.grid').directive('uiGrid', - [ - '$compile', - '$templateCache', - 'gridUtil', - '$window', - 'uiGridConstants', - function( - $compile, - $templateCache, - gridUtil, - $window, - uiGridConstants - ) { +angular.module('ui.grid').directive('uiGrid', uiGridDirective); + +uiGridDirective.$inject = ['$compile', '$templateCache', '$timeout', '$window', 'gridUtil', 'uiGridConstants']; +function uiGridDirective($compile, $templateCache, $timeout, $window, gridUtil, uiGridConstants) { + return { + templateUrl: 'ui-grid/ui-grid', + scope: { + uiGrid: '=' + }, + replace: true, + transclude: true, + controller: 'uiGridController', + compile: function () { return { - templateUrl: 'ui-grid/ui-grid', - scope: { - uiGrid: '=' - }, - replace: true, - transclude: true, - controller: 'uiGridController', - compile: function () { - return { - post: function ($scope, $elm, $attrs, uiGridCtrl) { - // gridUtil.logDebug('ui-grid postlink'); - - var grid = uiGridCtrl.grid; - - // Initialize scrollbars (TODO: move to controller??) - uiGridCtrl.scrollbars = []; - - //todo: assume it is ok to communicate that rendering is complete?? - grid.renderingComplete(); - - grid.element = $elm; - - grid.gridWidth = $scope.gridWidth = gridUtil.elementWidth($elm); - - // Default canvasWidth to the grid width, in case we don't get any column definitions to calculate it from - grid.canvasWidth = uiGridCtrl.grid.gridWidth; - - grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm); - - // If the grid isn't tall enough to fit a single row, it's kind of useless. Resize it to fit a minimum number of rows - if (grid.gridHeight < grid.options.rowHeight) { - // Figure out the new height - var contentHeight = grid.options.minRowsToShow * grid.options.rowHeight; - var headerHeight = grid.options.showHeader ? grid.options.headerRowHeight : 0; - var footerHeight = grid.calcFooterHeight(); - - var scrollbarHeight = 0; - if (grid.options.enableHorizontalScrollbar === uiGridConstants.scrollbars.ALWAYS) { - scrollbarHeight = gridUtil.getScrollbarWidth(); - } + post: function ($scope, $elm, $attrs, uiGridCtrl) { + var grid = uiGridCtrl.grid; + // Initialize scrollbars (TODO: move to controller??) + uiGridCtrl.scrollbars = []; + grid.element = $elm; - var maxNumberOfFilters = 0; - // Calculates the maximum number of filters in the columns - angular.forEach(grid.options.columnDefs, function(col) { - if (col.hasOwnProperty('filter')) { - if (maxNumberOfFilters < 1) { - maxNumberOfFilters = 1; - } - } - else if (col.hasOwnProperty('filters')) { - if (maxNumberOfFilters < col.filters.length) { - maxNumberOfFilters = col.filters.length; - } - } - }); - var filterHeight = maxNumberOfFilters * headerHeight; - var newHeight = headerHeight + contentHeight + footerHeight + scrollbarHeight + filterHeight; + // See if the grid has a rendered width, if not, wait a bit and try again + var sizeCheckInterval = 100; // ms + var maxSizeChecks = 20; // 2 seconds total + var sizeChecks = 0; - $elm.css('height', newHeight + 'px'); + // Setup (event listeners) the grid + setup(); - grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm); - } + // And initialize it + init(); - // Run initial canvas refresh - grid.refreshCanvas(); + // Mark rendering complete so API events can happen + grid.renderingComplete(); - //if we add a left container after render, we need to watch and react - $scope.$watch(function () { return grid.hasLeftContainer();}, function (newValue, oldValue) { - if (newValue === oldValue) { - return; - } - grid.refreshCanvas(true); - }); + // If the grid doesn't have size currently, wait for a bit to see if it gets size + checkSize(); - //if we add a right container after render, we need to watch and react - $scope.$watch(function () { return grid.hasRightContainer();}, function (newValue, oldValue) { - if (newValue === oldValue) { - return; - } - grid.refreshCanvas(true); - }); + /*-- Methods --*/ + + function checkSize() { + // If the grid has no width and we haven't checked more than times, check again in milliseconds + if ($elm[0].offsetWidth <= 0 && sizeChecks < maxSizeChecks) { + setTimeout(checkSize, sizeCheckInterval); + sizeChecks++; + } + else { + $timeout(init); + } + } + // Setup event listeners and watchers + function setup() { + // Bind to window resize events + angular.element($window).on('resize', gridResize); - // Resize the grid on window resize events - function gridResize($event) { - grid.gridWidth = $scope.gridWidth = gridUtil.elementWidth($elm); - grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm); + // Unbind from window resize events when the grid is destroyed + $elm.on('$destroy', function () { + angular.element($window).off('resize', gridResize); + }); - grid.refreshCanvas(true); + // If we add a left container after render, we need to watch and react + $scope.$watch(function () { return grid.hasLeftContainer();}, function (newValue, oldValue) { + if (newValue === oldValue) { + return; } + grid.refreshCanvas(true); + }); - angular.element($window).on('resize', gridResize); + // If we add a right container after render, we need to watch and react + $scope.$watch(function () { return grid.hasRightContainer();}, function (newValue, oldValue) { + if (newValue === oldValue) { + return; + } + grid.refreshCanvas(true); + }); + } - // Unbind from window resize events when the grid is destroyed - $elm.on('$destroy', function () { - angular.element($window).off('resize', gridResize); - }); + // Initialize the directive + function init() { + grid.gridWidth = $scope.gridWidth = gridUtil.elementWidth($elm); + + // Default canvasWidth to the grid width, in case we don't get any column definitions to calculate it from + grid.canvasWidth = uiGridCtrl.grid.gridWidth; + + grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm); + + // If the grid isn't tall enough to fit a single row, it's kind of useless. Resize it to fit a minimum number of rows + if (grid.gridHeight < grid.options.rowHeight && grid.options.enableMinHeightCheck) { + autoAdjustHeight(); + } + + // Run initial canvas refresh + grid.refreshCanvas(true); + } + + // Set the grid's height ourselves in the case that its height would be unusably small + function autoAdjustHeight() { + // Figure out the new height + var contentHeight = grid.options.minRowsToShow * grid.options.rowHeight; + var headerHeight = grid.options.showHeader ? grid.options.headerRowHeight : 0; + var footerHeight = grid.calcFooterHeight(); + + var scrollbarHeight = 0; + if (grid.options.enableHorizontalScrollbar === uiGridConstants.scrollbars.ALWAYS) { + scrollbarHeight = gridUtil.getScrollbarWidth(); } - }; + + var maxNumberOfFilters = 0; + // Calculates the maximum number of filters in the columns + angular.forEach(grid.options.columnDefs, function(col) { + if (col.hasOwnProperty('filter')) { + if (maxNumberOfFilters < 1) { + maxNumberOfFilters = 1; + } + } + else if (col.hasOwnProperty('filters')) { + if (maxNumberOfFilters < col.filters.length) { + maxNumberOfFilters = col.filters.length; + } + } + }); + var filterHeight = maxNumberOfFilters * headerHeight; + + var newHeight = headerHeight + contentHeight + footerHeight + scrollbarHeight + filterHeight; + + $elm.css('height', newHeight + 'px'); + + grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm); + } + + // Resize the grid on window resize events + function gridResize($event) { + grid.gridWidth = $scope.gridWidth = gridUtil.elementWidth($elm); + grid.gridHeight = $scope.gridHeight = gridUtil.elementHeight($elm); + + grid.refreshCanvas(true); + } } }; } - ]); + }; +} })(); diff --git a/src/js/core/directives/ui-pinned-container.js b/src/js/core/directives/ui-pinned-container.js index 3ff5fd1b1f..f500791911 100644 --- a/src/js/core/directives/ui-pinned-container.js +++ b/src/js/core/directives/ui-pinned-container.js @@ -23,6 +23,27 @@ $elm.addClass('ui-grid-pinned-container-' + $scope.side); + // Monkey-patch the viewport width function + if ($scope.side === 'left' || $scope.side === 'right') { + grid.renderContainers[$scope.side].getViewportWidth = monkeyPatchedGetViewportWidth; + } + + function monkeyPatchedGetViewportWidth() { + /*jshint validthis: true */ + var self = this; + + var viewportWidth = 0; + self.visibleColumnCache.forEach(function (column) { + viewportWidth += column.drawnWidth; + }); + + var adjustment = self.getViewportAdjustment(); + + viewportWidth = viewportWidth + adjustment.width; + + return viewportWidth; + } + function updateContainerWidth() { if ($scope.side === 'left' || $scope.side === 'right') { var cols = grid.renderContainers[$scope.side].visibleColumnCache; @@ -32,18 +53,16 @@ width += col.drawnWidth || col.width || 0; } - myWidth = width; - } + return width; + } } - - function updateContainerDimensions() { - // gridUtil.logDebug('update ' + $scope.side + ' dimensions'); + function updateContainerDimensions() { var ret = ''; - + // Column containers if ($scope.side === 'left' || $scope.side === 'right') { - updateContainerWidth(); + myWidth = updateContainerWidth(); // gridUtil.logDebug('myWidth', myWidth); @@ -59,11 +78,11 @@ } grid.renderContainers.body.registerViewportAdjuster(function (adjustment) { - if ( myWidth === 0 || !myWidth ){ - updateContainerWidth(); - } + myWidth = updateContainerWidth(); + // Subtract our own width adjustment.width -= myWidth; + adjustment.side = $scope.side; return adjustment; }); @@ -78,4 +97,4 @@ } }; }]); -})(); \ No newline at end of file +})(); diff --git a/src/js/core/factories/Grid.js b/src/js/core/factories/Grid.js index 1865369a66..0f14be43d7 100644 --- a/src/js/core/factories/Grid.js +++ b/src/js/core/factories/Grid.js @@ -1,8 +1,8 @@ (function(){ angular.module('ui.grid') -.factory('Grid', ['$q', '$compile', '$parse', 'gridUtil', 'uiGridConstants', 'GridOptions', 'GridColumn', 'GridRow', 'GridApi', 'rowSorter', 'rowSearcher', 'GridRenderContainer', '$timeout', - function($q, $compile, $parse, gridUtil, uiGridConstants, GridOptions, GridColumn, GridRow, GridApi, rowSorter, rowSearcher, GridRenderContainer, $timeout) { +.factory('Grid', ['$q', '$compile', '$parse', 'gridUtil', 'uiGridConstants', 'GridOptions', 'GridColumn', 'GridRow', 'GridApi', 'rowSorter', 'rowSearcher', 'GridRenderContainer', '$timeout','ScrollEvent', + function($q, $compile, $parse, gridUtil, uiGridConstants, GridOptions, GridColumn, GridRow, GridApi, rowSorter, rowSearcher, GridRenderContainer, $timeout, ScrollEvent) { /** * @ngdoc object @@ -10,7 +10,7 @@ angular.module('ui.grid') * @description Public Api for the core grid features * */ - + /** * @ngdoc function * @name ui.grid.class:Grid @@ -29,10 +29,10 @@ angular.module('ui.grid') else { throw new Error('No ID provided. An ID must be given when creating a grid.'); } - + self.id = options.id; delete options.id; - + // Get default options self.options = GridOptions.initialize( options ); @@ -45,12 +45,27 @@ angular.module('ui.grid') * use gridOptions.appScopeProvider to override the default assignment of $scope.$parent with any reference */ self.appScope = self.options.appScopeProvider; - + self.headerHeight = self.options.headerRowHeight; + /** + * @ngdoc object + * @name footerHeight + * @propertyOf ui.grid.class:Grid + * @description returns the total footer height gridFooter + columnFooter + */ self.footerHeight = self.calcFooterHeight(); + + /** + * @ngdoc object + * @name columnFooterHeight + * @propertyOf ui.grid.class:Grid + * @description returns the total column footer height + */ + self.columnFooterHeight = self.calcColumnFooterHeight(); + self.rtl = false; self.gridHeight = 0; self.gridWidth = 0; @@ -62,28 +77,30 @@ angular.module('ui.grid') self.viewportAdjusters = []; self.rowHeaderColumns = []; self.dataChangeCallbacks = {}; - + self.verticalScrollSyncCallBackFns = {}; + self.horizontalScrollSyncCallBackFns = {}; + // self.visibleRowCache = []; - + // Set of 'render' containers for self grid, which can render sets of rows self.renderContainers = {}; - + // Create a self.renderContainers.body = new GridRenderContainer('body', self); - + self.cellValueGetterCache = {}; - + // Cached function to use with custom row templates self.getRowTemplateFn = null; - - + + //representation of the rows on the grid. //these are wrapped references to the actual data rows (options.data) self.rows = []; - + //represents the columns on the grid self.columns = []; - + /** * @ngdoc boolean * @name isScrollingVertically @@ -91,7 +108,7 @@ angular.module('ui.grid') * @description set to true when Grid is scrolling vertically. Set to false via debounced method */ self.isScrollingVertically = false; - + /** * @ngdoc boolean * @name isScrollingHorizontally @@ -109,37 +126,61 @@ angular.module('ui.grid') */ self.scrollDirection = uiGridConstants.scrollDirection.NONE; - var debouncedVertical = gridUtil.debounce(function () { + function vertical (scrollEvent) { self.isScrollingVertically = false; + self.api.core.raise.scrollEnd(scrollEvent); self.scrollDirection = uiGridConstants.scrollDirection.NONE; - }, 1000); - - var debouncedHorizontal = gridUtil.debounce(function () { + } + + var debouncedVertical = gridUtil.debounce(vertical, self.options.scrollDebounce); + var debouncedVerticalMinDelay = gridUtil.debounce(vertical, 0); + + function horizontal (scrollEvent) { self.isScrollingHorizontally = false; + self.api.core.raise.scrollEnd(scrollEvent); self.scrollDirection = uiGridConstants.scrollDirection.NONE; - }, 1000); - - + } + + var debouncedHorizontal = gridUtil.debounce(horizontal, self.options.scrollDebounce); + var debouncedHorizontalMinDelay = gridUtil.debounce(horizontal, 0); + + /** * @ngdoc function * @name flagScrollingVertically * @methodOf ui.grid.class:Grid * @description sets isScrollingVertically to true and sets it to false in a debounced function */ - self.flagScrollingVertically = function() { + self.flagScrollingVertically = function(scrollEvent) { + if (!self.isScrollingVertically && !self.isScrollingHorizontally) { + self.api.core.raise.scrollBegin(scrollEvent); + } self.isScrollingVertically = true; - debouncedVertical(); + if (self.options.scrollDebounce === 0 || !scrollEvent.withDelay) { + debouncedVerticalMinDelay(scrollEvent); + } + else { + debouncedVertical(scrollEvent); + } }; - + /** * @ngdoc function * @name flagScrollingHorizontally * @methodOf ui.grid.class:Grid * @description sets isScrollingHorizontally to true and sets it to false in a debounced function */ - self.flagScrollingHorizontally = function() { + self.flagScrollingHorizontally = function(scrollEvent) { + if (!self.isScrollingVertically && !self.isScrollingHorizontally) { + self.api.core.raise.scrollBegin(scrollEvent); + } self.isScrollingHorizontally = true; - debouncedHorizontal(); + if (self.options.scrollDebounce === 0 || !scrollEvent.withDelay) { + debouncedHorizontalMinDelay(scrollEvent); + } + else { + debouncedHorizontal(scrollEvent); + } }; self.scrollbarHeight = 0; @@ -151,11 +192,11 @@ angular.module('ui.grid') if (self.options.enableVerticalScrollbar === uiGridConstants.scrollbars.ALWAYS) { self.scrollbarWidth = gridUtil.getScrollbarWidth(); } - - - + + + self.api = new GridApi(self); - + /** * @ngdoc function * @name refresh @@ -165,13 +206,13 @@ angular.module('ui.grid') * rowProcessors, as well as calling refreshCanvas to update all * the grid sizing. In general you should prefer to use queueGridRefresh * instead, which is basically a debounced version of refresh. - * + * * If you only want to resize the grid, not regenerate all the rows * and columns, you should consider directly calling refreshCanvas instead. - * + * */ self.api.registerMethod( 'core', 'refresh', this.refresh ); - + /** * @ngdoc function * @name queueGridRefresh @@ -182,10 +223,10 @@ angular.module('ui.grid') * rowProcessors, as well as calling refreshCanvas to update all * the grid sizing. In general you should prefer to use queueGridRefresh * instead, which is basically a debounced version of refresh. - * + * */ self.api.registerMethod( 'core', 'queueGridRefresh', this.queueGridRefresh ); - + /** * @ngdoc function * @name refreshRows @@ -193,10 +234,10 @@ angular.module('ui.grid') * @description Runs only the rowProcessors, columns remain as they were. * It then calls redrawInPlace and refreshCanvas, which adjust the grid sizing. * @returns {promise} promise that is resolved when render completes? - * + * */ self.api.registerMethod( 'core', 'refreshRows', this.refreshRows ); - + /** * @ngdoc function * @name queueRefresh @@ -204,10 +245,10 @@ angular.module('ui.grid') * @description Requests execution of refreshCanvas, if multiple requests are made * during a digest cycle only one will run. RefreshCanvas updates the grid sizing. * @returns {promise} promise that is resolved when render completes? - * + * */ self.api.registerMethod( 'core', 'refreshRows', this.queueRefresh ); - + /** * @ngdoc function * @name handleWindowResize @@ -216,22 +257,88 @@ angular.module('ui.grid') * up by a watch on window size, but in some circumstances it is necessary * to call this manually * @returns {promise} promise that is resolved when render completes? - * + * */ self.api.registerMethod( 'core', 'handleWindowResize', this.handleWindowResize ); - - + + /** * @ngdoc function * @name addRowHeaderColumn * @methodOf ui.grid.core.api:PublicApi * @description adds a row header column to the grid * @param {object} column def - * + * */ self.api.registerMethod( 'core', 'addRowHeaderColumn', this.addRowHeaderColumn ); - - + + /** + * @ngdoc function + * @name scrollToIfNecessary + * @methodOf ui.grid.core.api:PublicApi + * @description Scrolls the grid to make a certain row and column combo visible, + * in the case that it is not completely visible on the screen already. + * @param {GridRow} gridRow row to make visible + * @param {GridCol} gridCol column to make visible + * @returns {promise} a promise that is resolved when scrolling is complete + * + */ + self.api.registerMethod( 'core', 'scrollToIfNecessary', function(gridRow, gridCol) { return self.scrollToIfNecessary(gridRow, gridCol);} ); + + /** + * @ngdoc function + * @name scrollTo + * @methodOf ui.grid.core.api:PublicApi + * @description Scroll the grid such that the specified + * row and column is in view + * @param {object} rowEntity gridOptions.data[] array instance to make visible + * @param {object} colDef to make visible + * @returns {promise} a promise that is resolved after any scrolling is finished + */ + self.api.registerMethod( 'core', 'scrollTo', function (rowEntity, colDef) { return self.scrollTo(rowEntity, colDef);} ); + + /** + * @ngdoc function + * @name registerRowsProcessor + * @methodOf ui.grid.core.api:PublicApi + * @description + * Register a "rows processor" function. When the rows are updated, + * the grid calls each registered "rows processor", which has a chance + * to alter the set of rows (sorting, etc) as long as the count is not + * modified. + * + * @param {function(renderedRowsToProcess, columns )} processorFunction rows processor function, which + * is run in the context of the grid (i.e. this for the function will be the grid), and must + * return the updated rows list, which is passed to the next processor in the chain + * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room + * for other people to inject rows processors at intermediate priorities. Lower priority rowsProcessors run earlier. + * + * At present allRowsVisible is running at 50, filter is running at 100, sort is at 200, grouping at 400, selectable rows at 500, pagination at 900 (pagination will generally want to be last) + */ + self.api.registerMethod( 'core', 'registerRowsProcessor', this.registerRowsProcessor ); + + /** + * @ngdoc function + * @name registerColumnsProcessor + * @methodOf ui.grid.core.api:PublicApi + * @description + * Register a "columns processor" function. When the columns are updated, + * the grid calls each registered "columns processor", which has a chance + * to alter the set of columns as long as the count is not + * modified. + * + * @param {function(renderedColumnsToProcess, rows )} processorFunction columns processor function, which + * is run in the context of the grid (i.e. this for the function will be the grid), and must + * return the updated columns list, which is passed to the next processor in the chain + * @param {number} priority the priority of this processor. In general we try to do them in 100s to leave room + * for other people to inject columns processors at intermediate priorities. Lower priority columnsProcessors run earlier. + * + * At present allRowsVisible is running at 50, filter is running at 100, sort is at 200, grouping at 400, selectable rows at 500, pagination at 900 (pagination will generally want to be last) + */ + self.api.registerMethod( 'core', 'registerColumnsProcessor', this.registerColumnsProcessor ); + + + /** * @ngdoc function * @name sortHandleNulls @@ -252,11 +359,11 @@ angular.module('ui.grid') * @param {object} b sort value b * @returns {number} null if there were no nulls/undefineds, otherwise returns * a sort value that should be passed back from the sort function - * + * */ self.api.registerMethod( 'core', 'sortHandleNulls', rowSorter.handleNulls ); - - + + /** * @ngdoc function * @name sortChanged @@ -264,28 +371,28 @@ angular.module('ui.grid') * @description The sort criteria on one or more columns has * changed. Provides as parameters the grid and the output of * getColumnSorting, which is an array of gridColumns - * that have sorting on them, sorted in priority order. - * + * that have sorting on them, sorted in priority order. + * * @param {Grid} grid the grid - * @param {array} sortColumns an array of columns with + * @param {array} sortColumns an array of columns with * sorts on them, in priority order - * + * * @example *
      *      gridApi.core.on.sortChanged( grid, sortColumns );
      * 
*/ self.api.registerEvent( 'core', 'sortChanged' ); - + /** * @ngdoc function * @name columnVisibilityChanged * @methodOf ui.grid.core.api:PublicApi * @description The visibility of a column has changed, - * the column itself is passed out as a parameter of the event. - * + * the column itself is passed out as a parameter of the event. + * * @param {GridCol} column the column that changed - * + * * @example *
      *      gridApi.core.on.columnVisibilityChanged( $scope, function (column) {
@@ -294,23 +401,35 @@ angular.module('ui.grid')
      * 
*/ self.api.registerEvent( 'core', 'columnVisibilityChanged' ); - + /** * @ngdoc method * @name notifyDataChange * @methodOf ui.grid.core.api:PublicApi * @description Notify the grid that a data or config change has occurred, - * where that change isn't something the grid was otherwise noticing. This + * where that change isn't something the grid was otherwise noticing. This * might be particularly relevant where you've changed values within the data - * and you'd like cell classes to be re-evaluated, or changed config within + * and you'd like cell classes to be re-evaluated, or changed config within * the columnDef and you'd like headerCellClasses to be re-evaluated. - * @param {string} type one of the + * @param {string} type one of the * uiGridConstants.dataChange values (ALL, ROW, EDIT, COLUMN), which tells * us which refreshes to fire. - * + * */ self.api.registerMethod( 'core', 'notifyDataChange', this.notifyDataChange ); - + + /** + * @ngdoc method + * @name clearAllFilters + * @methodOf ui.grid.core.api:PublicApi + * @description Clears all filters and optionally refreshes the visible rows. + * @params {object} refreshRows Defaults to true. + * @params {object} clearConditions Defaults to false. + * @params {object} clearFlags Defaults to false. + * @returns {promise} If `refreshRows` is true, returns a promise of the rows refreshing. + */ + self.api.registerMethod('core', 'clearAllFilters', this.clearAllFilters); + self.registerDataChangeCallback( self.columnRefreshCallback, [uiGridConstants.dataChange.COLUMN]); self.registerDataChangeCallback( self.processRowsCallback, [uiGridConstants.dataChange.EDIT]); @@ -330,6 +449,14 @@ angular.module('ui.grid') height += this.options.gridFooterHeight; } + height += this.calcColumnFooterHeight(); + + return height; + }; + + Grid.prototype.calcColumnFooterHeight = function () { + var height = 0; + if (this.options.showColumnFooter) { height += this.options.columnFooterHeight; } @@ -364,7 +491,7 @@ angular.module('ui.grid') * @methodOf ui.grid.class:Grid * @description When the build creates columns from column definitions, the columnbuilders will be called to add * additional properties to the column. - * @param {function(colDef, col, gridOptions)} columnsProcessor function to be called + * @param {function(colDef, col, gridOptions)} columnBuilder function to be called */ Grid.prototype.registerColumnBuilder = function registerColumnBuilder(columnBuilder) { this.columnBuilders.push(columnBuilder); @@ -400,25 +527,25 @@ angular.module('ui.grid') * @methodOf ui.grid.class:Grid * @description When a data change occurs, the data change callbacks of the specified type * will be called. The rules are: - * + * * - when the data watch fires, that is considered a ROW change (the data watch only notices * added or removed rows) * - when the api is called to inform us of a change, the declared type of that change is used * - when a cell edit completes, the EDIT callbacks are triggered * - when the columnDef watch fires, the COLUMN callbacks are triggered * - when the options watch fires, the OPTIONS callbacks are triggered - * + * * For a given event: * - ALL calls ROW, EDIT, COLUMN, OPTIONS and ALL callbacks * - ROW calls ROW and ALL callbacks * - EDIT calls EDIT and ALL callbacks * - COLUMN calls COLUMN and ALL callbacks * - OPTIONS calls OPTIONS and ALL callbacks - * + * * @param {function(grid)} callback function to be called - * @param {array} types the types of data change you want to be informed of. Values from + * @param {array} types the types of data change you want to be informed of. Values from * the uiGridConstants.dataChange values ( ALL, EDIT, ROW, COLUMN, OPTIONS ). Optional and defaults to - * ALL + * ALL * @returns {function} deregister function - a function that can be called to deregister this callback */ Grid.prototype.registerDataChangeCallback = function registerDataChangeCallback(callback, types, _this) { @@ -430,7 +557,7 @@ angular.module('ui.grid') gridUtil.logError("Expected types to be an array or null in registerDataChangeCallback, value passed was: " + types ); } this.dataChangeCallbacks[uid] = { callback: callback, types: types, _this:_this }; - + var self = this; var deregisterFunction = function() { delete self.dataChangeCallbacks[uid]; @@ -443,9 +570,9 @@ angular.module('ui.grid') * @name callDataChangeCallbacks * @methodOf ui.grid.class:Grid * @description Calls the callbacks based on the type of data change that - * has occurred. Always calls the ALL callbacks, calls the ROW, EDIT, COLUMN and OPTIONS callbacks if the + * has occurred. Always calls the ALL callbacks, calls the ROW, EDIT, COLUMN and OPTIONS callbacks if the * event type is matching, or if the type is ALL. - * @param {number} type the type of event that occurred - one of the + * @param {number} type the type of event that occurred - one of the * uiGridConstants.dataChange values (ALL, ROW, EDIT, COLUMN, OPTIONS) */ Grid.prototype.callDataChangeCallbacks = function callDataChangeCallbacks(type, options) { @@ -462,20 +589,20 @@ angular.module('ui.grid') } }, this); }; - + /** * @ngdoc function * @name notifyDataChange * @methodOf ui.grid.class:Grid * @description Notifies us that a data change has occurred, used in the public - * api for users to tell us when they've changed data or some other event that + * api for users to tell us when they've changed data or some other event that * our watches cannot pick up - * @param {string} type the type of event that occurred - one of the + * @param {string} type the type of event that occurred - one of the * uiGridConstants.dataChange values (ALL, ROW, EDIT, COLUMN) */ Grid.prototype.notifyDataChange = function notifyDataChange(type) { var constants = uiGridConstants.dataChange; - if ( type === constants.ALL || + if ( type === constants.ALL || type === constants.COLUMN || type === constants.EDIT || type === constants.ROW || @@ -485,15 +612,15 @@ angular.module('ui.grid') gridUtil.logError("Notified of a data change, but the type was not recognised, so no action taken, type was: " + type); } }; - - + + /** * @ngdoc function * @name columnRefreshCallback * @methodOf ui.grid.class:Grid * @description refreshes the grid when a column refresh - * is notified, which triggers handling of the visible flag. - * This is called on uiGridConstants.dataChange.COLUMN, and is + * is notified, which triggers handling of the visible flag. + * This is called on uiGridConstants.dataChange.COLUMN, and is * registered as a dataChangeCallback in grid.js * @param {string} name column name */ @@ -515,7 +642,7 @@ angular.module('ui.grid') Grid.prototype.processRowsCallback = function processRowsCallback( grid ){ grid.queueGridRefresh(); }; - + /** * @ngdoc function @@ -555,10 +682,16 @@ angular.module('ui.grid') * @ngdoc property * @name type * @propertyOf ui.grid.class:GridOptions.columnDef - * @description the type of the column, used in sorting. If not provided then the + * @description the type of the column, used in sorting. If not provided then the * grid will guess the type. Add this only if the grid guessing is not to your - * satisfaction. Refer to {@link ui.grid.service:GridUtil.guessType gridUtil.guessType} for - * a list of values the grid knows about. + * satisfaction. One of: + * - 'string' + * - 'boolean' + * - 'number' + * - 'date' + * - 'object' + * - 'numberStr' + * Note that if you choose date, your dates should be in a javascript date type * */ Grid.prototype.assignTypes = function(){ @@ -580,6 +713,18 @@ angular.module('ui.grid') }); }; + + /** + * @ngdoc function + * @name isRowHeaderColumn + * @methodOf ui.grid.class:Grid + * @description returns true if the column is a row Header + * @param {object} column column + */ + Grid.prototype.isRowHeaderColumn = function isRowHeaderColumn(column) { + return this.rowHeaderColumns.indexOf(column) !== -1; + }; + /** * @ngdoc function * @name addRowHeaderColumn @@ -589,7 +734,6 @@ angular.module('ui.grid') */ Grid.prototype.addRowHeaderColumn = function addRowHeaderColumn(colDef) { var self = this; - //self.createLeftContainer(); var rowHeaderCol = new GridColumn(colDef, gridUtil.nextUid(), self); rowHeaderCol.isRowHeader = true; if (self.isRTL()) { @@ -617,6 +761,23 @@ angular.module('ui.grid') }); }; + /** + * @ngdoc function + * @name getOnlyDataColumns + * @methodOf ui.grid.class:Grid + * @description returns all columns except for rowHeader columns + */ + Grid.prototype.getOnlyDataColumns = function getOnlyDataColumns() { + var self = this; + var cols = []; + self.columns.forEach(function (col) { + if (self.rowHeaderColumns.indexOf(col) === -1) { + cols.push(col); + } + }); + return cols; + }; + /** * @ngdoc function * @name buildColumns @@ -686,7 +847,7 @@ angular.module('ui.grid') // We need to allow for the "row headers" when mapping from the column defs array to the columns array // If we have a row header in columns[0] and don't account for it we'll overwrite it with the column in columnDefs[0] - // Go through all the column defs, use the shorter of columns length and colDefs.length because if a user has given two columns the same name then + // Go through all the column defs, use the shorter of columns length and colDefs.length because if a user has given two columns the same name then // columns will be shorter than columnDefs. In this situation we'll avoid an error, but the user will still get an unexpected result var len = Math.min(self.options.columnDefs.length, self.columns.length); for (i = 0; i < len; i++) { @@ -727,6 +888,16 @@ angular.module('ui.grid') var html = col.cellTemplate.replace(uiGridConstants.MODEL_COL_FIELD, self.getQualifiedColField(col)); html = html.replace(uiGridConstants.COL_FIELD, 'grid.getCellValue(row, col)'); + if (col.cellTooltip === false){ + html = html.replace(uiGridConstants.TOOLTIP, ''); + } else { + // gridColumn will have made sure that the col either has false or a function for this value + if (col.cellFilter){ + html = html.replace(uiGridConstants.TOOLTIP, 'title="{{col.cellTooltip(row, col) | ' + col.cellFilter + '}}"'); + } else { + html = html.replace(uiGridConstants.TOOLTIP, 'title="{{col.cellTooltip(row, col)}}"'); + } + } var compiledElementFn = $compile(html); col.compiledElementFn = compiledElementFn; @@ -846,7 +1017,7 @@ angular.module('ui.grid') // Make sure to parse to an int lastNum = parseInt(lastNum, 10); - // Add 1 to the number from the last column and tack it on to the field to be the name for this new column + // Add 1 to the number from the last column and tack it on to the field to be the name for this new column colDef.name = colDef.field + (lastNum + 1); } } @@ -865,7 +1036,7 @@ angular.module('ui.grid') var t = []; for (var i = 0; i < n.length; i++) { var nV = nAccessor ? n[i][nAccessor] : n[i]; - + var found = false; for (var j = 0; j < o.length; j++) { var oV = oAccessor ? o[j][oAccessor] : o[j]; @@ -878,189 +1049,89 @@ angular.module('ui.grid') t.push(nV); } } - + return t; }; - /** - * @ngdoc function - * @name getRow - * @methodOf ui.grid.class:Grid - * @description returns the GridRow that contains the rowEntity - * @param {object} rowEntity the gridOptions.data array element instance - */ - Grid.prototype.getRow = function getRow(rowEntity) { - var self = this; - var rows = this.rows.filter(function (row) { - return self.options.rowEquality(row.entity, rowEntity); - }); - return rows.length > 0 ? rows[0] : null; - }; + /** + * @ngdoc function + * @name getRow + * @methodOf ui.grid.class:Grid + * @description returns the GridRow that contains the rowEntity + * @param {object} rowEntity the gridOptions.data array element instance + * @param {array} rows [optional] the rows to look in - if not provided then + * looks in grid.rows + */ + Grid.prototype.getRow = function getRow(rowEntity, lookInRows) { + var self = this; + lookInRows = typeof(lookInRows) === 'undefined' ? self.rows : lookInRows; - /** + var rows = lookInRows.filter(function (row) { + return self.options.rowEquality(row.entity, rowEntity); + }); + return rows.length > 0 ? rows[0] : null; + }; + + + /** * @ngdoc function * @name modifyRows * @methodOf ui.grid.class:Grid * @description creates or removes GridRow objects from the newRawData array. Calls each registered * rowBuilder to further process the row * - * Rows are identified using the gridOptions.rowEquality function + * This method aims to achieve three things: + * 1. the resulting rows array is in the same order as the newRawData, we'll call + * rowsProcessors immediately after to sort the data anyway + * 2. if we have row hashing available, we try to use the rowHash to find the row + * 3. no memory leaks - rows that are no longer in newRawData need to be garbage collected + * + * The basic logic flow makes use of the newRawData, oldRows and oldHash, and creates + * the newRows and newHash + * + * ``` + * newRawData.forEach newEntity + * if (hashing enabled) + * check oldHash for newEntity + * else + * look for old row directly in oldRows + * if !oldRowFound // must be a new row + * create newRow + * append to the newRows and add to newHash + * run the processors + * + * Rows are identified using the hashKey if configured. If not configured, then rows + * are identified using the gridOptions.rowEquality function */ Grid.prototype.modifyRows = function modifyRows(newRawData) { - var self = this, - i, - rowhash, - found, - newRow; - if ((self.options.useExternalSorting || self.getColumnSorting().length === 0) && newRawData.length > 0) { - var oldRowHash = self.rowHashMap; - if (!oldRowHash) { - oldRowHash = {get: function(){return null;}}; - } - self.createRowHashMap(); - rowhash = self.rowHashMap; - var wasEmpty = self.rows.length === 0; - self.rows.length = 0; - for (i = 0; i < newRawData.length; i++) { - var newRawRow = newRawData[i]; - found = oldRowHash.get(newRawRow); - if (found) { - newRow = found.row; - } - else { - newRow = self.processRowBuilders(new GridRow(newRawRow, i, self)); - } - self.rows.push(newRow); - rowhash.put(newRawRow, { - i: i, - entity: newRawRow, - row:newRow - }); - } - //now that we have data, it is save to assign types to colDefs -// if (wasEmpty) { - self.assignTypes(); -// } - } else { - if (self.rows.length === 0 && newRawData.length > 0) { - if (self.options.enableRowHashing) { - if (!self.rowHashMap) { - self.createRowHashMap(); - } - - for (i = 0; i < newRawData.length; i++) { - newRow = newRawData[i]; - - self.rowHashMap.put(newRow, { - i: i, - entity: newRow - }); - } + var self = this; + var oldRows = self.rows.slice(0); + var oldRowHash = self.rowHashMap || self.createRowHashMap(); + self.rowHashMap = self.createRowHashMap(); + self.rows.length = 0; + + newRawData.forEach( function( newEntity, i ) { + var newRow; + if ( self.options.enableRowHashing ){ + // if hashing is enabled, then this row will be in the hash if we already know about it + newRow = oldRowHash.get( newEntity ); + } else { + // otherwise, manually search the oldRows to see if we can find this row + newRow = self.getRow(newEntity, oldRows); } - self.addRows(newRawData); - //now that we have data, it is save to assign types to colDefs - self.assignTypes(); - } - else if (newRawData.length > 0) { - var unfoundNewRows, unfoundOldRows, unfoundNewRowsToFind; - - // If row hashing is turned on - if (self.options.enableRowHashing) { - // Array of new rows that haven't been found in the old rowset - unfoundNewRows = []; - // Array of new rows that we explicitly HAVE to search for manually in the old row set. They cannot be looked up by their identity (because it doesn't exist). - unfoundNewRowsToFind = []; - // Map of rows that have been found in the new rowset - var foundOldRows = {}; - // Array of old rows that have NOT been found in the new rowset - unfoundOldRows = []; - - // Create the row HashMap if it doesn't exist already - if (!self.rowHashMap) { - self.createRowHashMap(); - } - rowhash = self.rowHashMap; - - // Make sure every new row has a hash - for (i = 0; i < newRawData.length; i++) { - newRow = newRawData[i]; - - // Flag this row as needing to be manually found if it didn't come in with a $$hashKey - var mustFind = false; - if (!self.options.getRowIdentity(newRow)) { - mustFind = true; - } - - // See if the new row is already in the rowhash - found = rowhash.get(newRow); - // If so... - if (found) { - // See if it's already being used by as GridRow - if (found.row) { - // If so, mark this new row as being found - foundOldRows[self.options.rowIdentity(newRow)] = true; - } - } - else { - // Put the row in the hashmap with the index it corresponds to - rowhash.put(newRow, { - i: i, - entity: newRow - }); - - // This row has to be searched for manually in the old row set - if (mustFind) { - unfoundNewRowsToFind.push(newRow); - } - else { - unfoundNewRows.push(newRow); - } - } - } - - // Build the list of unfound old rows - for (i = 0; i < self.rows.length; i++) { - var row = self.rows[i]; - var hash = self.options.rowIdentity(row.entity); - if (!foundOldRows[hash]) { - unfoundOldRows.push(row); - } - } + // if we didn't find the row, it must be new, so create it + if ( !newRow ){ + newRow = self.processRowBuilders(new GridRow(newEntity, i, self)); } - // Look for new rows - var newRows = unfoundNewRows || []; - - // The unfound new rows is either `unfoundNewRowsToFind`, if row hashing is turned on, or straight `newRawData` if it isn't - var unfoundNew = (unfoundNewRowsToFind || newRawData); - - // Search for real new rows in `unfoundNew` and concat them onto `newRows` - newRows = newRows.concat(self.newInN(self.rows, unfoundNew, 'entity')); - - self.addRows(newRows); - - var deletedRows = self.getDeletedRows((unfoundOldRows || self.rows), newRawData); - - for (i = 0; i < deletedRows.length; i++) { - if (self.options.enableRowHashing) { - self.rowHashMap.remove(deletedRows[i].entity); - } + self.rows.push( newRow ); + self.rowHashMap.put( newEntity, newRow ); + }); - self.rows.splice( self.rows.indexOf(deletedRows[i]), 1 ); - } - } - // Empty data set - else { - // Reset the row HashMap - self.createRowHashMap(); + self.assignTypes(); - // Reset the rows length! - self.rows.length = 0; - } - } - var p1 = $q.when(self.processRowsProcessors(self.rows)) .then(function (renderableRows) { return self.setVisibleRows(renderableRows); @@ -1074,18 +1145,6 @@ angular.module('ui.grid') return $q.all([p1, p2]); }; - Grid.prototype.getDeletedRows = function(oldRows, newRows) { - var self = this; - - var olds = oldRows.filter(function (oldRow) { - return !newRows.some(function (newItem) { - return self.options.rowEquality(newItem, oldRow.entity); - }); - }); - // var olds = self.newInN(newRows, oldRows, null, 'entity'); - // dump('olds', olds); - return olds; - }; /** * Private Undocumented Method @@ -1135,7 +1194,7 @@ angular.module('ui.grid') * @name registerStyleComputation * @methodOf ui.grid.class:Grid * @description registered a styleComputation function - * + * * If the function returns a value it will be appended into the grid's `