From 50979e750889c11801bc60b0dc422537d3c1eeee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Fl=C3=A9ron?= Date: Thu, 1 Apr 2021 10:09:15 +0200 Subject: [PATCH] Features/octane version (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Revert "[bugfix] Recompute row meta index when previous prepend causes shift (#623)" (#651) This reverts commit 5efba8c34d105c011d5682921beb67291bee3206. * Fix build (#661) * Update addepar-style-toolbox to fix import issue * [CHORE] allow ember-release to fail tests This is temporary until we have time to investigate what is going on. * Upgrade several project dependencies (#658) * Upgrade linting packages * Upgrade ember-math-helpers * Upgrade ember-native-dom-helpers * Upgrade ember-angle-bracket-invocation-polyfill * Upgrade ember-fetch * Upgrade ember-cli-dependency-checker * Upgrade ember-try * Upgrade ember-auto-import * Upgrade default addon dependencies * Upgrade ember-cli-sass * Upgrade ember-compatibility-helpers * Upgrade @html-next/vertical-collection * Upgrade husky * Revert "Upgrade ember-cli-sass" This reverts commit ad3fc22bd1e5f8e0570dcd322a5ff88945e603a5. * Add .yarnrc and .npmrc This will allow to add the yarn.lock file and to rely on the public Addepar packages available for all. * Add yarn.lock * [FEATURE] Add support for context menu on header cells (#662) ``` {{ember-th api=r onContextMenu='actionHandler'}} ``` * don't try to notify the cell value of a change if the context is being destroyed * [CHORE] Node 6 support - pin jsdom * Pin ember-cli-page-object Fix build issue on Travis https://travis-ci.org/Addepar/ember-table/jobs/513359138 Related to san650/ember-cli-page-object#446 * [FEATURE] Use `sortEmptyLast` on the header to have empty values at the end of a sort result. ``` ``` * ember-th renders only the block when one is passed. Add ember-th/sort-indicator and ember-th/resize-handle It allows to fully customize the content of `ember-th` without the need to duplicate the logic for sorting and resizing ``` {{#ember-th |columnValue columnMeta|}} {{columnValue.name}} {{ember-th/sort-indicator columnMeta=columnMeta}} {{ember-th/resize-handle columnMeta=columnMeta}} {{/ember-th}} ``` * [BUGFIX] Empty values are sorted properly * [CHORE] Update to node 8 * [CHORE] Unpin jsdom after dropping support for Node 6 * [BUGFIX] Ensure dynamicAlias works on latest Ember (#679) * [BUGFIX] Ensure dynamicAlias works on latest Ember There were some subtle changes on the latest Ember that made aliases and properties that include . in their name to stop working. Instead of creating dynamic aliases, we now just access the properties directly on _context, which should fix the issue. * fix release tests * Start converting to classic classes (#677) * Start converting to classic classes * Finish up conversion and convert TBody * Convert row-wrapper and simple-checkbox to classic classes (#681) There is one issue, where we are setting properties in a computed, which did not seem to be flagged be ESLint before, but is flagged when we use a class computed. I added `// eslint-disable-next-line ember/no-side-effects, ember-best-practices/no-side-effect-cp` to get around the issue for now. @pzuraq do we need to do something else to fix this or is disabling ESLint there okay? * thead, th, and tr to classic components (#680) * thead, th, and tr to classic components * Fix some tests * Add missing attributeBindings * Use defaultTo for sortFunction and compareFunction * fix defaultTo * Convert collapse-tree, column-tree, and ember-td to classic (#682) * Convert collapse-tree and ember-td to classic * Convert column-tree to classic * Move ember-decorators to devDeps (#685) * Move ember-decorators to devDeps * Store functions before calling removeObserver on them * [BUG] Setting the column width to its current value works It used to return `undefined` which would lead the `delta` to be `NaN`, causing an infinite loop when resizing a leaf column. * [BUG] Fix memory leaks related to vertical-collection * Change pinned dependency specifier for vertical-collection Change from `user/repo#sha` to `https://github.com/user/repo.git#sha` form. Yarn has a bug related to installing changed SHA versions when they are pinned in `user/repo` form, that could cause consumers of this addon (or developers of this addon) to fail to get updated dependency code via `yarn install`, see: https://github.com/yarnpkg/yarn/issues/4722#issuecomment-487140809 * Ensure that table column widths are recomputed when columns change This fixes an issue where removing a column can leave a blank space in the table because it doesn't recompute column widths. The table's `ResizeSensor` is primarily responsible for noticing resizes and updating widths, but when a column is removed, although the inner `table` element width changes, the container `.ember-table` element does not change its width, and thus the `ResizeSensor` never notices, and the column widths are not recomputed. This modifies the `ember-thead` observer to have it call `fillupHandler` when column count changes. It also changes the observer in `ember-thead` to watch `columns.[]` instead of just `columns`, because in the latter case, the `fillupHandler` will not be called if a column was removed via mutation (e.g. `popObject`). Adds tests for removal via both ways (mutation and `this.set('columns', newColumns)`). Also adds tests that adding columns also causes column widths to be computed. Co-authored-by: Jonathan Jackson * Skip _onColumnsChange when there are no remaining columns * Convert ember-thead test to a form that works with older Ember This should make it so that the tests pass on Ember 1.12 and 1.13. Use a special `requestAnimationFrame` waiter that waits 2 RAF turns to ensure that the table's columns are done being laid out when the test starts measuring their dimensions. * Fix arguments for test helper functions * Use back-compatible template for ember 1.12 and 1.13 * Add Ember-1.12 compatibility for `this.set` and `this.get` in test * Add comment about the `rafFinished` helper * [CHORE] ember-beta is supported It was deactivated with #661 * [PERF] Set default bufferSize to 1 By default, ember-table allocates a buffer of 20 rows before and after the visible ones. This default value forces a lot of computation in the case of tables with many columns and complex cells, from syncinc more data (from rows to cells for example) and triggering more observers (based on the number of computed properties defines per cell for example). VC also does not allocate the part of the buffer that should be above the visible first row, as there is none when loading the table. This means that the initial scroll will also lead to the creation of 20 rows in the DOM (and the underlying computation to render the content). From a pure UI/UX point of view, we don't need more than 1 row on each side of the table, and it's already the default for `vertical-collection`. * [TEST] Add rowCount to page object for thead, tbody and tfoot In some tests, it is interesting to see if a row is properly inserted or removed. Because `vertical-collection` tries to not render all the rows, we can't simply count the number of `tr`. Instead we use the `data-test-row-count` attribute to keep track of the number of available rows in a table. * ember-decorators/argument is needed to generate the documentation Custom cell and header pages rely on it * Enable strict BEM class name format * [FEATURE] Text alignment can be defined per column The `textAlign` property on a column definition accepts `left`, `center` and `right`. When the property is set, a class is added to the cell (`ember-table__text-align-left`, `ember-table__text-align-center`, `ember-table__text-align-right`). * CHORE: Change jsconfig settings so that tests are included Both "include" and "exclude" are unnecessary together, so modify "exclude" to exclude all relevant directories. This makes it so that VSCode won't complain about decorator usage in dummy/* JS files, among other things. * DOCS: Fix rendering of addon docs pages * Upgrade ember-cli-addon-docs and related devDeps * Remove @ember-decorators/argument devDep Convert custom-cell and custom-header docs components to classic. Fixes issue where the table-customization addon docs page would throw errors at runtime related to the `@argument` decorator. * Restructure docs index and application templates Add a `nav.item` for 'docs.index', otherwise ember-cli-addon-docs hits an assertion and throws an error in local development when it tries and fails to associate the currentURL ('/docs') with a known docs route. Remove some private-api `style=` component props and errant spacer `li`s in the nav. In newer ember-cli-addon-docs its styles are namespaced (under `docs-X` classes), and the styling of the nav no longer looked correct. * Upgrade angle-bracket polyfill Upgrade the polyfill to allow using `` nested angle-bracket invocation style on the table-customization docs page. Prior to this upgrade, the page would throw errors when visited. * DOCS: Avoid passing incorrect/unneeded args to faker.* methods faker.company.companyName for its first arg expects either a format string or an integer in the range [0,2]: https://github.com/Marak/faker.js/blob/3a4bb358614c1e1f5d73f4df45c13a1a7aa013d7/lib/company.js#L26-L39 Passing a value greater than 2 results in a returned value of `string parameter is required!` instead of a faked name. This wasn't obvious because the number of generated rows is dynamic (via `getRandomInt`), so it only shows up if the # of generated rows is enough to trigger the error, and then you have to scroll to the bottom of the table to notice it. Also remove the argument passed to `department` because it doesn't accept an argument: https://github.com/Marak/faker.js/blob/3a4bb358614c1e1f5d73f4df45c13a1a7aa013d7/lib/commerce.js#L22-L24 Ditto for `productName`: https://github.com/Marak/faker.js/blob/3a4bb358614c1e1f5d73f4df45c13a1a7aa013d7/lib/commerce.js#L31-L35 * DOCS: Improve table-customization docs pages * Move `getRandomInt` helper to utils/generators * Use more explanatory data for table-customization sorting Previously, the data displayed in the table didn't have any difference, row-to-row, so it wasn't obvious that clicking the column headers actually did change the sorting. This copies over the sortable columns/rows data from the sorting docs example. * Interactive sort/resize indicators on customization page On the table customization page, add checkboxes to turn on/off the sort/resize indicators in the header columns, so that docs readers can better understand how they are used. * table-customization: More obvious cell/column customizations Use a SCSS loop to define the `.text-` and `.bg-` style rules. Remove the "text-" prefix from the color names used in code, move this into the template. Make custom cells use a `text-` class. Make custom header cells use a `bg-` class, for variety. Separate some of the reused controller properties so that it's clearer which property is used for which example. * DOCS: Add basic acceptance test for docs/ routes * DOCS: Test that subcolumns docs render cells Fix rendering of generated dummy rows. Cells with valuePaths longer than a single character were being skipped over in the dummy row's `unknownProperty` hook. Fix by removing the length check in `unknownProperty` and explicitly adding the needed properties to the class definition so that the only accesses that hit `unknownProperty` should be `valuePath`s for rendering. * [DOCS] Fix autogenerated API docs for components Change from using the addon-docs esdoc to yuidoc plugin. esdoc is meant for "native classes"-style exports, and it was silently ignoring the documentation for the components, resulting in no output. Add some additional `@argument ` annotations so that yuidoc will properly pick up argument names. yuidoc's syntax does allow for `@optional`, `@required` and `@default` annotations for arguments, but when I added them these were either not being parsed correctly or (my assumption) the addon-docs yuidoc plugin wasn't using that extra parsed data, but either way the addition of those annotations wasn't affecting the generated API docs so instead I've modified the `@type` annotations to indicate optional args, with default values in parens when appropriate. * [DOCS] Add "Testing" section Add `addon-test-support/index.js` to export the TablePage from a central location. Upgrade @addepar/eslint-config to ^4.0.2 so that the `index.js` is linted correctly. Add a "Testing" section to the addon docs that describes basic usage of the TablePage. * DOCS: Better spacing for Resize/Reorder columns demo * DOCS: Remove unused args to docs-hero * [DOCS] Remove unused `.red-cell` css style The "red-cell" class usage was removed here in https://github.com/Addepar/ember-table/commit/cf37ef1bdcde91e061923304135ea4b68ef4e06b#diff-ab03e59e6d25ee3864870f2d10de6a7fL8 but the selector wasn't removed at the same time. * [CHORE] v2.0.0-beta.8 * [CHORE] Add 3.4, 3.8 LTS to ember-try Fixes #719 * [CHORE] Update .travis.yml Remove some deprecated config settings for Travis. Update settings to more closely align with the default addon output, see https://github.com/ember-cli/ember-addon-output/blob/master/.travis.yml * [CHORE] More update to travis.yml * [CHORE] Remove unused ember-cli-addon-docs configs addon docs no longer uses configuration settings in ember-cli-build.js, it has its own separate config file in config/addon-docs.js instead. * Remove @function annotation from private mergeSort function * [DOCS] Unconditionally enable faker for dummy app * [DOCS] Show selected-row checkmark Fixes #593 The underlying styles that we import from '@addepar/style-toolbox/onyx/index' should be modified so that they don't hardcode an absolute URL for the background-image checkmark svg, but in lieu of that larger-scope fix, this PR copies the checkmark.svg asset and adds a singular style override to display it. * [DOCS] Add 2-state sorting example Fixes #721 Also add checkbox to configure `sortEmptyLast` on that demo. Add test for the 2-state sorting. Use `label` instead of `span` on all the places `demo-options` are shown. * Bump up the size of demo data for selection * Use objectAt to fetch rowValues (cause meta alloc) Although the `selection` set contains raw values only, some other code (like the summing of selected counts for group selection state in `collapse-tree.js`) expect that all items in the `selection` set have a meta allocated. The non-meta-allocated rows were added where the bare `children` property of a rowValue was being iterated. Instead with this change the `tree` is referenced for pulling out the children, meaning the meta cache system is exercised and metas allocated for any selected row. * [DOCS] Fix up wording and typo in docs * Update tests/dummy/app/pods/docs/guides/body/row-selection/template.md Co-Authored-By: Matthew Beale * fix eslint * [DOCS] Change styling for row-selection demo options Change the styling so the interactive row-selection demo moves the radio button labels to the right of the button, and add back in some alignment and spacing between the labels. The docs were using a tailwind CSS class "pr-4" but tailwind is no longer provided via addon-docs, so those styles were not being applied. * Test that unrendered (occluded) row selection can be managed This is a failing test for #726. Also fixes a typo in an unrelated test assertion message. * v2.0.0-beta.9 * [DOCS] Add interactive row-selection demo Change the row-selection docs example to display the current value of `selection` to help clarify that it can be an array or single item, and it doesn't include a group's children when that group is selected. * Add failing test for #735 * Potential fix for #735 * v2.0.0 * [DOCS] Miscellaneous small fixes Remove undefined `py-2` and `pr-4` CSS classes (vestiges from when tailwind was included in the build from addon-docs). Consolidate example options to use `demo-options` class wherever possible. Remove unneeded `demo-option` class. Fix the 'color' property on the columns in the custom-header example so that the correct bg-color CSS classes are applied. Use a separate selection property `demoSelection` for the last demo on the 'row selection' page. When the `selection` property was shared amongst all the demos on the page, it was possible to select a row in one of the demos that didn't exist in the later ones, and would cause an error. Fix typos. * [DOCS] Update readme to add link to online docs * move addon to dependencies Fix 741. Required so consuming apps can import the helper. * enable fillMode to support an nth-column option * make all single column fill modes reuse the same resize implementation * [DOCS] Update docs for `fillColumnIndex` * [DOCS] Test clicking on snippets links on docs pages * [DOCS] Fix demo snippets on Sorting, Dynamic Fixed Columns docs * Add failing tests for #747 Adds several failing tests for #747. * Add `setupAllRowMeta`, `mapSelectionToMeta` to CollapseTree Add a function `setupAllRowMeta` that walks all the table's rows and sets up a row meta for each one. This is called lazily in the case where the table has a `selection` that includes some rows that don't yet have rowMetas. This can happen if the selection includes rows that haven't yet rendered. The rowMeta for a row is lazily created when it is rendered, so rows that haven't yet rendered won't have a row meta. This usually is not a problem because in normal user interaction with a table, the user will only interact with rows that are rendered. However, it's possible to programmatically set a selection, and in this case a row in the selection may not yet have been rendered. It's also possible for a `selection` to contain a row without a corresponding rowMeta if that row is not part of the table's rows. This is an invalid selection, and this commit adds an Ember.warning for times when we detect such a case. Fixes #747 Modifies one of the 747 test cases to assert that the warning is triggered when ET encounters a selection with a row that is not part of the table. * Add debug-handlers polyfill to capture warning in older Ember Add `test` condition to warn call in collapse-tree * assert row is selected in test * only register the warn handler 1x during testing * Do not JSON.stringify missing row * [TESTS] footer page object extends body page object The footer component extends the body component. This makes the exported Test Page object able to operate on `table.footer` the same way it does on `table.body`. * [DOCS] Add Changelog.md (#750) auto-generated via `npx auto-changelog` * [CHORE] Use release-it for releases (#751) * Update readme and release-it config * Release 2.1.0 * Added example of using CSS Flex with ember-table (#752) Per discussion on #topic-tables I've added my Twiddles for both a pure CSS version and a Bootstrap version. * [TESTS] Allow ember-beta scenario to fail at CI (#755) Relates to #754 * Add npm version badge * Update version badge in README * [FEAT] Enforce maximum height (by percentage) for sticky footers and he… (#753) When the footer or header will take up more than 50% of the height of the table, the sticky polyfill will now position some of the cells so that they must be scrolled to in order to be seen. Otherwise, all of the body cells are covered by the sticky header/footer cells. * Release 2.1.1 * [CHORE] use vertical-collection @ ^1.0.0-beta.14 (#756) The latest beta release. Does not include any new functionality. The comparison between the previous pinned sha and this version: https://github.com/html-next/vertical-collection/compare/ca8cab8a3204f99cca1cf7661e4170ac41dc3a07...v1.0.0-beta.14 * Release 2.1.2 * bugfix: allow zero for fillColumnIndex fix: #767 * Bump deps. Pin addon-docs to avoid regression The regression is tracked upstream: https://github.com/ember-learn/ember-cli-addon-docs/issues/402#issuecomment-532291377 if a fix lands the strict version dependency on ember-cli-addon-docs can be loosened. * Release 2.1.3 * Update ember-classy-page-object to latest ^0.6.0 (#772) * Update ember-classy-page-object to latest ^0.6.0 * Bump to ^0.6.1 * Release 2.1.4 * Fix release badge (#774) Use the "npm version" badge since the "release" badge only shows github releases (not tags), and we don't always create an official github release when we tag a new version. * Fix CI (#773) * remove legacy ember-decorators * use ember-qunit instead of ember-cli-qunit * Remove ember-legacy-class-shim docs: https://github.com/pzuraq/ember-legacy-class-shim We no longer have ES6 classes inside this addon, so we don't need the shim * Skip column reordering tests on ember release, beta, canary See: https://github.com/Addepar/ember-table/issues/775 * Add note that async observers are needed for Ember 3.13+ * Update README to point out Ember 3.13 regressions * Use async observer when target app is Ember 3.13+ (#777) * Add `observer` util to opt in to async in 3.13+ * Remove code that skips column-reordering tests for 3.13+ * Use ember 3.12 in package.json * Fix lint * change eslint no-restricted-imports messages, pr feedback * Rename imported ember observer functions, pr feedback * Fix argument order for addObserver/removeObserver * Use `settled` to fix collapse-tree tests The use of `await settled()` seems to be required to properly wait for the now-async observers to finish notifying of property changes to the collapse tree. Also: * Fix propogate typo * Replace some hardcoded for loop lengths in tests with eg `expectedValue.length` * Remove unused `_notifyCollection` property (#779) This was introduced in #529 but never used either in that change or since, so it seems like it can be safely removed. * [DOCS] Add browser compatibility section (#769) * Fix column-reordering with Ember 3.13+ (#778) * Add parameterizedComponentModule helper, test w/ and w/out ember arrays * Notify using the keyName rather than the full path Fixes #776 * Run resize tests w/ and w/o emberA column/rows * [FEAT] Add direction to reorder indicator (#766) * [DOCS] Remove Ember 3.13 section from readme (#781) The Ember 3.13 bugs are now all fixed, so remove this section. Also: run the markdown formatter over the README to clean up. * Bump ember-test-selectors (#782) 0.3.9 of Ember test selectors is very old and does not strip data-test- when using Babel 7. Bump to 2.1.0. See: https://github.com/simplabs/ember-test-selectors/releases * Release v2.2.0 * [DOCS] Add new reorder-directional classes (#783) * Use resolutions to force prettier to 1.18.2 * bugfix: first multiselection has no _lastSelectedIndex * Release 2.2.1 * Avoid "update prop already used in computation" Ember internals use an observer for attribute bindings. In this case the observer timing causes a computation based on a value which is then updated by a side-effect-having CP. Here avoid the observer interaction by instead setting the count itself as part of the computation side effects. See: * https://github.com/Addepar/ember-table/issues/795 * https://github.com/emberjs/ember.js/issues/18613 Fixes #795 * Ignore engine incompat Avoids the issue with Node 8 caused by https://github.com/npm/node-semver/commit/d61f828e64260a0a097f26210f5500e91a621828#diff-b9cfc7f2cdf78a7f4b91a753d10865a2R35 * Release 2.2.2 * Add testing for rowCount for tree tables * Update row count when tree collapses The row count did not update when the collapse of a tree was toggled. Here ensure that happens with an observer in dev mode. Fixes #804 * Release 2.2.3 * restore ember th behavior * Quickfix: column re-ordering Co-authored-by: Cy Klassen Co-authored-by: Cyril Fluck Co-authored-by: Josemar Luedke <230476+josemarluedke@users.noreply.github.com> Co-authored-by: Cyril Fluck Co-authored-by: Nolan Evans <22493+nolman@users.noreply.github.com> Co-authored-by: Matthew Beale Co-authored-by: Chris Garrett Co-authored-by: Robert Wagner Co-authored-by: Jerry Nummi Co-authored-by: Cory Forsyth Co-authored-by: Jonathan Jackson Co-authored-by: Cory Forsyth Co-authored-by: Matthew Beale Co-authored-by: Eli Flanagan Co-authored-by: Chris Bonser Co-authored-by: Chris Bonser Co-authored-by: octabode Co-authored-by: Michal Bryxí Co-authored-by: Camille TJHOA Co-authored-by: Fry Kten <32638334+frykten@users.noreply.github.com> Co-authored-by: frykten Co-authored-by: Jiaying --- .eslintrc.js | 24 +- .gitignore | 1 - .npmrc | 1 + .release-it.json | 12 + .sass-lint.yml | 6 + .travis.yml | 66 +- .yarnrc | 1 + CHANGELOG.md | 528 + README.md | 98 +- addon-test-support/index.js | 3 + .../pages/-private/ember-table-body.js | 19 +- .../pages/-private/ember-table-footer.js | 13 +- .../pages/-private/ember-table-header.js | 36 +- addon-test-support/pages/ember-table.js | 15 +- addon/-private/collapse-tree.js | 734 +- addon/-private/column-tree.js | 521 +- .../-private/sticky/table-sticky-polyfill.js | 116 +- addon/-private/utils/computed.js | 29 +- addon/-private/utils/default-to.js | 37 + addon/-private/utils/observer.js | 64 + addon/-private/utils/reorder-indicators.js | 9 + addon/-private/utils/sort.js | 39 +- addon/components/-private/base-table-cell.js | 18 +- addon/components/-private/row-wrapper.js | 150 +- addon/components/-private/simple-checkbox.js | 72 +- addon/components/ember-table/component.js | 59 +- addon/components/ember-tbody/component.js | 284 +- addon/components/ember-td/component.js | 193 +- addon/components/ember-tfoot/component.js | 31 +- addon/components/ember-th/component.js | 132 +- .../ember-th/resize-handle/component.js | 40 + .../ember-th/resize-handle/template.hbs | 4 + .../ember-th/sort-indicator/component.js | 49 + .../ember-th/sort-indicator/template.hbs | 15 + addon/components/ember-th/template.hbs | 17 +- addon/components/ember-thead/component.js | 285 +- addon/components/ember-tr/component.js | 101 +- addon/styles/addon.scss | 12 + app/components/ember-th/resize-handle.js | 1 + app/components/ember-th/sort-indicator.js | 1 + config/ember-try.js | 25 + ember-cli-build.js | 26 +- jsconfig.json | 12 +- package.json | 90 +- tests/acceptance/docs-test.js | 113 + tests/dummy/app/app.js | 6 +- .../dummy/app/pods/application/controller.js | 12 +- .../pods/components/custom-cell/component.js | 11 +- .../pods/components/custom-cell/template.hbs | 2 +- .../components/custom-header/component.js | 11 +- .../components/custom-header/template.hbs | 2 +- .../pods/components/custom-row/component.js | 6 +- .../docs/guides/body/occlusion/controller.js | 16 +- .../guides/body/row-selection/controller.js | 108 +- .../guides/body/row-selection/template.md | 91 +- .../guides/body/rows-and-trees/controller.js | 30 +- .../guides/body/rows-and-trees/template.md | 4 +- .../docs/guides/header/columns/controller.js | 62 +- .../docs/guides/header/columns/template.md | 38 +- .../guides/header/fixed-columns/controller.js | 46 +- .../guides/header/fixed-columns/template.md | 4 +- .../header/size-constraints/controller.js | 21 +- .../header/size-constraints/template.md | 73 +- .../docs/guides/header/sorting/controller.js | 56 +- .../header/sorting/empty-values/component.js | 63 + .../header/sorting/empty-values/template.hbs | 31 + .../docs/guides/header/sorting/template.md | 52 +- .../guides/header/subcolumns/controller.js | 23 +- .../docs/guides/header/subcolumns/template.md | 2 +- .../guides/main/basic-table/controller.js | 16 +- .../guides/main/legacy-usage/controller.js | 16 +- .../guides/main/styling-the-table/template.md | 14 + .../main/table-customization/controller.js | 197 +- .../main/table-customization/template.md | 55 +- .../guides/main/table-meta-data/controller.js | 53 +- .../guides/main/table-meta-data/template.md | 2 +- tests/dummy/app/pods/docs/index/template.md | 10 +- tests/dummy/app/pods/docs/template.hbs | 14 +- .../pods/docs/testing/test-page/template.md | 29 + tests/dummy/app/pods/index/route.js | 10 +- .../pods/scenarios/performance/controller.js | 36 +- .../app/pods/scenarios/simple/controller.js | 17 +- tests/dummy/app/router.js | 4 + tests/dummy/app/styles/app.scss | 137 +- tests/dummy/app/styles/tables.scss | 33 +- tests/dummy/app/utils/generators.js | 32 +- tests/dummy/config/environment.js | 9 + .../dummy/public/assets/images/checkmark.svg | 1 + tests/helpers/generate-table.js | 14 +- tests/helpers/module.js | 19 + tests/helpers/warn-handlers.js | 28 + tests/integration/components/basic-test.js | 64 + tests/integration/components/footer-test.js | 5 + .../components/headers/cell-test.js | 20 + .../components/headers/ember-th-test.js | 60 + .../components/headers/ember-thead-test.js | 191 + .../components/headers/main-test.js | 58 +- .../components/headers/reorder-test.js | 29 +- .../components/headers/resize-handle-test.js | 25 + .../components/headers/resize-test.js | 25 +- .../components/headers/sort-indicator-test.js | 94 + tests/integration/components/row-test.js | 24 +- .../integration/components/selection-test.js | 156 +- tests/integration/components/sort-test.js | 54 + tests/integration/components/thead-test.js | 0 tests/integration/components/tree-test.js | 3 + tests/test-helper.js | 13 + tests/unit/-private/collapse-tree-test.js | 98 +- tests/unit/-private/column-tree-test.js | 65 + .../-private/table-sticky-polyfill-test.js | 132 +- yarn.lock | 14785 ++++++++++++++++ 111 files changed, 19405 insertions(+), 2114 deletions(-) create mode 100644 .npmrc create mode 100644 .release-it.json create mode 100644 .yarnrc create mode 100644 CHANGELOG.md create mode 100644 addon-test-support/index.js create mode 100644 addon/-private/utils/default-to.js create mode 100644 addon/-private/utils/observer.js create mode 100644 addon/components/ember-th/resize-handle/component.js create mode 100644 addon/components/ember-th/resize-handle/template.hbs create mode 100644 addon/components/ember-th/sort-indicator/component.js create mode 100644 addon/components/ember-th/sort-indicator/template.hbs create mode 100644 app/components/ember-th/resize-handle.js create mode 100644 app/components/ember-th/sort-indicator.js create mode 100644 tests/acceptance/docs-test.js create mode 100644 tests/dummy/app/pods/docs/guides/header/sorting/empty-values/component.js create mode 100644 tests/dummy/app/pods/docs/guides/header/sorting/empty-values/template.hbs create mode 100644 tests/dummy/app/pods/docs/testing/test-page/template.md create mode 100644 tests/dummy/public/assets/images/checkmark.svg create mode 100644 tests/helpers/warn-handlers.js create mode 100644 tests/integration/components/headers/cell-test.js create mode 100644 tests/integration/components/headers/ember-th-test.js create mode 100644 tests/integration/components/headers/ember-thead-test.js create mode 100644 tests/integration/components/headers/resize-handle-test.js create mode 100644 tests/integration/components/headers/sort-indicator-test.js delete mode 100644 tests/integration/components/thead-test.js create mode 100644 tests/unit/-private/column-tree-test.js create mode 100644 yarn.lock diff --git a/.eslintrc.js b/.eslintrc.js index 7d66e9be4..ad1662191 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,9 +1,6 @@ module.exports = { extends: ['@addepar', '@addepar/eslint-config/ember'], parser: 'babel-eslint', - env: { - es6: true, - }, rules: { 'no-restricted-globals': 'off', @@ -11,5 +8,24 @@ module.exports = { 'ember/avoid-leaking-state-in-ember-objects': 'off', 'ember-best-practices/require-dependent-keys': 'off', - } + + 'no-restricted-imports': [ + 'error', + { + paths: [ + { + name: '@ember/object', + importNames: ['observer'], + message: 'For compatibility, use `import { observer } from "-private/utils/observer"`', + }, + { + name: '@ember/object/observers', + importNames: ['addObserver', 'removeObserver'], + message: + 'For compatibility, use `import { addObserver, removeObserver } from "-private/utils/observer"`', + }, + ], + }, + ], + }, }; diff --git a/.gitignore b/.gitignore index 44761ecb1..8fa39a63c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,3 @@ testem.log .node_modules.ember-try/ bower.json.ember-try package.json.ember-try -yarn.lock diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..cca776def --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@addepar:registry = "https://registry.npmjs.org/" diff --git a/.release-it.json b/.release-it.json new file mode 100644 index 000000000..8375e89b5 --- /dev/null +++ b/.release-it.json @@ -0,0 +1,12 @@ +{ + "git": { + "tagName": "v${version}" + }, + "github": { + "release": false + }, + "hooks": { + "after:release": "yarn run docs:deploy", + "after:bump": "npx auto-changelog -p" + } +} diff --git a/.sass-lint.yml b/.sass-lint.yml index 83adcbd30..f02b0f5c0 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -1,2 +1,8 @@ options: config-file: node_modules/@addepar/sass-lint-config/config.yml +rules: + # Name Formats + class-name-format: + - 2 + - + convention: strictbem diff --git a/.travis.yml b/.travis.yml index 0f692de68..bea113469 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,8 @@ language: node_js node_js: # we recommend testing addons with the same minimum supported node version as Ember CLI # so that your addon works for all apps - - "6" + - '8' -sudo: required dist: trusty addons: @@ -13,25 +12,53 @@ addons: cache: yarn: true + directories: + - node_modules -global: - # See https://git.io/vdao3 for details. - - JOBS=1 env: - # we recommend new addons test the current and previous LTS - # as well as latest stable release (bonus points to beta/canary) - - EMBER_TRY_SCENARIO=ember-1.12 - - EMBER_TRY_SCENARIO=ember-1.13 - - EMBER_TRY_SCENARIO=ember-lts-2.8 - - EMBER_TRY_SCENARIO=ember-lts-2.18 - - EMBER_TRY_SCENARIO=ember-release - - EMBER_TRY_SCENARIO=ember-beta - - EMBER_TRY_SCENARIO=ember-canary - - EMBER_TRY_SCENARIO=ember-default - -matrix: - fast_finish: true + global: + # See https://git.io/vdao3 for details. + - JOBS=1 + +branches: + only: + - master + # npm version tags + - /^v\d+\.\d+\.\d+/ + +jobs: + fail_fast: true allow_failures: + - env: EMBER_TRY_SCENARIO=ember-beta + - env: EMBER_TRY_SCENARIO=ember-canary + + include: + # runs linting and tests with current locked deps + + - stage: 'Tests' + name: 'Tests' + install: + - yarn install --non-interactive + + script: + - yarn run lint + - yarn run test + + - name: 'Floating dependencies' + script: + - yarn test + + # we recommend new addons test the current and previous LTS + # as well as latest stable release (bonus points to beta/canary) + - stage: 'Additional Tests' + env: EMBER_TRY_SCENARIO=ember-1.12 + - env: EMBER_TRY_SCENARIO=ember-1.13 + - env: EMBER_TRY_SCENARIO=ember-lts-2.8 + - env: EMBER_TRY_SCENARIO=ember-lts-2.18 + - env: EMBER_TRY_SCENARIO=ember-lts-3.4 + - env: EMBER_TRY_SCENARIO=ember-lts-3.8 + - env: EMBER_TRY_SCENARIO=ember-release + - env: EMBER_TRY_SCENARIO=ember-beta - env: EMBER_TRY_SCENARIO=ember-canary before_install: @@ -39,10 +66,9 @@ before_install: - export PATH=$HOME/.yarn/bin:$PATH install: - - yarn install --no-lockfile --non-interactive + - yarn install --no-lockfile --non-interactive --ignore-engines script: - - yarn run lint # Usually, it's ok to finish the test scenario without reverting # to the addon's original dependency state, skipping "cleanup". - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO --skip-cleanup diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 000000000..45291c133 --- /dev/null +++ b/.yarnrc @@ -0,0 +1 @@ +registry "https://registry.npmjs.org/" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..870d19fab --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,528 @@ +### Changelog + +All notable changes to this project will be documented in this file. Dates are displayed in UTC. + +Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). + +#### [v2.2.3](https://github.com/Addepar/ember-table/compare/v2.2.2...v2.2.3) + +> 13 January 2020 + +- Update test rowcount when collapse state changes [`#806`](https://github.com/Addepar/ember-table/pull/806) +- Update row count when tree collapses [`#804`](https://github.com/Addepar/ember-table/issues/804) +- Add testing for rowCount for tree tables [`195c842`](https://github.com/Addepar/ember-table/commit/195c84295ee28b8a239cc48d29849132dc1ec46d) + +#### [v2.2.2](https://github.com/Addepar/ember-table/compare/v2.2.1...v2.2.2) + +> 9 January 2020 + +- Avoid "update prop already used in computation" [`#802`](https://github.com/Addepar/ember-table/pull/802) +- Avoid "update prop already used in computation" [`#795`](https://github.com/Addepar/ember-table/issues/795) +- Release 2.2.2 [`020ad0b`](https://github.com/Addepar/ember-table/commit/020ad0b4a670b240a6a0259033e2852cf1094051) +- Ignore engine incompat [`d923e47`](https://github.com/Addepar/ember-table/commit/d923e477cf698377c3bd6bf97874c135b5eecd25) + +#### [v2.2.1](https://github.com/Addepar/ember-table/compare/v2.2.0...v2.2.1) + +> 11 December 2019 + +- bugfix: first multiselection has no _lastSelectedIndex [`#788`](https://github.com/Addepar/ember-table/pull/788) +- Fix prettier lint errors [`#790`](https://github.com/Addepar/ember-table/pull/790) +- [DOCS] Add new reorder-directional classes [`#783`](https://github.com/Addepar/ember-table/pull/783) +- Release 2.2.1 [`040df20`](https://github.com/Addepar/ember-table/commit/040df201be07fe753ad36a931ce845423d78379a) +- Use resolutions to force prettier to 1.18.2 [`8d18c9b`](https://github.com/Addepar/ember-table/commit/8d18c9b808f0b6ee03f7fdacaa3872e518676358) + +#### [v2.2.0](https://github.com/Addepar/ember-table/compare/v2.1.4...v2.2.0) + +> 5 November 2019 + +- Bump ember-test-selectors [`#782`](https://github.com/Addepar/ember-table/pull/782) +- [DOCS] Remove Ember 3.13 section from readme [`#781`](https://github.com/Addepar/ember-table/pull/781) +- [FEAT] Add direction to reorder indicator [`#766`](https://github.com/Addepar/ember-table/pull/766) +- Fix column-reordering with Ember 3.13+ [`#778`](https://github.com/Addepar/ember-table/pull/778) +- [DOCS] Add browser compatibility section [`#769`](https://github.com/Addepar/ember-table/pull/769) +- Remove unused `_notifyCollection` property [`#779`](https://github.com/Addepar/ember-table/pull/779) +- Use async observer when target app is Ember 3.13+ [`#777`](https://github.com/Addepar/ember-table/pull/777) +- Fix CI [`#773`](https://github.com/Addepar/ember-table/pull/773) +- Fix release badge [`#774`](https://github.com/Addepar/ember-table/pull/774) +- Fix column-reordering with Ember 3.13+ (#778) [`#776`](https://github.com/Addepar/ember-table/issues/776) +- Release v2.2.0 [`6529af2`](https://github.com/Addepar/ember-table/commit/6529af23ca8242ba270ba4c33f551112312a6064) + +#### [v2.1.4](https://github.com/Addepar/ember-table/compare/v2.1.3...v2.1.4) + +> 25 October 2019 + +- Update ember-classy-page-object to latest ^0.6.0 [`#772`](https://github.com/Addepar/ember-table/pull/772) +- Release 2.1.4 [`cc6897e`](https://github.com/Addepar/ember-table/commit/cc6897e62ddb2ad4579adc76a512dfced3cce3ff) + +#### [v2.1.3](https://github.com/Addepar/ember-table/compare/v2.1.2...v2.1.3) + +> 17 September 2019 + +- bugfix: allow zero for fillColumnIndex [`#768`](https://github.com/Addepar/ember-table/pull/768) +- Bump deps. Pin addon-docs to avoid regression [`#770`](https://github.com/Addepar/ember-table/pull/770) +- Release 2.1.3 [`5d33dc0`](https://github.com/Addepar/ember-table/commit/5d33dc021390c15e4add64c73c3a6f011896b7ff) + +#### [v2.1.2](https://github.com/Addepar/ember-table/compare/v2.1.1...v2.1.2) + +> 16 August 2019 + +- [CHORE] use vertical-collection @ ^1.0.0-beta.14 [`#756`](https://github.com/Addepar/ember-table/pull/756) +- Release 2.1.2 [`3aa2645`](https://github.com/Addepar/ember-table/commit/3aa26452a4f1bdfa58239a1b8a74d811fc259000) + +#### [v2.1.1](https://github.com/Addepar/ember-table/compare/v2.1.0...v2.1.1) + +> 15 August 2019 + +- [FEAT] Enforce maximum height (by percentage) for sticky footers and he… [`#753`](https://github.com/Addepar/ember-table/pull/753) +- [TESTS] Allow ember-beta scenario to fail at CI [`#755`](https://github.com/Addepar/ember-table/pull/755) +- Added example of using CSS Flex with ember-table [`#752`](https://github.com/Addepar/ember-table/pull/752) +- Release 2.1.1 [`275b176`](https://github.com/Addepar/ember-table/commit/275b176cc446001355a54a75a513f9415a126553) +- Add npm version badge [`8412c1f`](https://github.com/Addepar/ember-table/commit/8412c1fd5bc93612f610e7e043ea8e7262cdfe35) +- Update version badge in README [`5a2a51c`](https://github.com/Addepar/ember-table/commit/5a2a51c752e3978006f956109d96c61e691a1779) + +#### [v2.1.0](https://github.com/Addepar/ember-table/compare/v2.0.0...v2.1.0) + +> 31 July 2019 + +- [CHORE] Use release-it for releases [`#751`](https://github.com/Addepar/ember-table/pull/751) +- [DOCS] Add Changelog.md [`#750`](https://github.com/Addepar/ember-table/pull/750) +- [TESTS] footer page object extends body page object [`#749`](https://github.com/Addepar/ember-table/pull/749) +- [BUG] Fix cases where manual selection includes unrendered or no… [`#748`](https://github.com/Addepar/ember-table/pull/748) +- [DOCS] Test clicking on snippets links on docs pages [`#746`](https://github.com/Addepar/ember-table/pull/746) +- [FEAT] enable fillMode to support an nth-column option [`#635`](https://github.com/Addepar/ember-table/pull/635) +- move addon to dependencies [`#742`](https://github.com/Addepar/ember-table/pull/742) +- [DOCS] Update readme to add link to online docs [`#739`](https://github.com/Addepar/ember-table/pull/739) +- [DOCS] Miscellaneous small fixes [`#738`](https://github.com/Addepar/ember-table/pull/738) +- Add `setupAllRowMeta`, `mapSelectionToMeta` to CollapseTree [`#747`](https://github.com/Addepar/ember-table/issues/747) +- enable fillMode to support an nth-column option [`3861c1e`](https://github.com/Addepar/ember-table/commit/3861c1ea6ea8769a4ec761f8e17f4e3bfa693a2a) +- Add failing tests for #747 [`d2ea37e`](https://github.com/Addepar/ember-table/commit/d2ea37ecf7531f9019ba72f954317e6f652b693b) +- Update readme and release-it config [`eeccff6`](https://github.com/Addepar/ember-table/commit/eeccff60c7318e9672c4373878cccf1ec188251d) + +#### [v2.0.0](https://github.com/Addepar/ember-table/compare/v2.0.0-beta.9...v2.0.0) + +> 23 July 2019 + +- Render tables with no columns correctly [`#737`](https://github.com/Addepar/ember-table/pull/737) +- Add failing test for #735 [`#736`](https://github.com/Addepar/ember-table/pull/736) +- [DOCS] Add interactive row-selection demo [`#732`](https://github.com/Addepar/ember-table/pull/732) +- Potential fix for #735 [`4ed6b17`](https://github.com/Addepar/ember-table/commit/4ed6b179d0f518b77a73eaa3a414271ba2cce6f7) + +#### [v2.0.0-beta.9](https://github.com/Addepar/ember-table/compare/v2.0.0-beta.8...v2.0.0-beta.9) + +> 19 July 2019 + +- Test that unrendered (occluded) row selection can be managed [`#731`](https://github.com/Addepar/ember-table/pull/731) +- Use objectAt to fetch rowValues (cause meta alloc) [`#727`](https://github.com/Addepar/ember-table/pull/727) +- [DOCS] Change styling for row-selection demo options [`#730`](https://github.com/Addepar/ember-table/pull/730) +- [DOCS] Fix up wording and typo in docs [`#729`](https://github.com/Addepar/ember-table/pull/729) +- [DOCS] Add 2-state sorting example [`#725`](https://github.com/Addepar/ember-table/pull/725) +- [DOCS] Show selected-row checkmark [`#724`](https://github.com/Addepar/ember-table/pull/724) +- [DOCS] Unconditionally enable faker for dummy app [`#723`](https://github.com/Addepar/ember-table/pull/723) +- [CHORE] Remove unused ember-cli-addon-docs configs [`#722`](https://github.com/Addepar/ember-table/pull/722) +- [CHORE] Add 3.4, 3.8 LTS to ember-try [`#720`](https://github.com/Addepar/ember-table/pull/720) +- [DOCS] Add 2-state sorting example [`#721`](https://github.com/Addepar/ember-table/issues/721) +- [DOCS] Show selected-row checkmark [`#593`](https://github.com/Addepar/ember-table/issues/593) +- [CHORE] Add 3.4, 3.8 LTS to ember-try [`#719`](https://github.com/Addepar/ember-table/issues/719) +- [CHORE] Update .travis.yml [`a4cc1cc`](https://github.com/Addepar/ember-table/commit/a4cc1ccf31c1710c8458ce2f9c7054c0b8a71b51) +- Use objectAt to fetch rowValues (cause meta alloc) [`a22c73e`](https://github.com/Addepar/ember-table/commit/a22c73e9f7ce5453b03068009d256277ab59be76) +- [CHORE] More update to travis.yml [`fc9ac40`](https://github.com/Addepar/ember-table/commit/fc9ac40227d32e2ad76d17fd165840cf9cd32dc3) + +#### [v2.0.0-beta.8](https://github.com/Addepar/ember-table/compare/v2.0.0-beta.7...v2.0.0-beta.8) + +> 15 July 2019 + +- [DOCS] Style tweaks [`#718`](https://github.com/Addepar/ember-table/pull/718) +- [DOCS] Add "Testing" section [`#717`](https://github.com/Addepar/ember-table/pull/717) +- [DOCS] Fix autogenerated API docs for components [`#716`](https://github.com/Addepar/ember-table/pull/716) +- DOCS: Test that subcolumns docs render cells [`#710`](https://github.com/Addepar/ember-table/pull/710) +- [DOCS]: Add basic acceptance test for docs/ routes [`#714`](https://github.com/Addepar/ember-table/pull/714) +- DOCS: Table-customization docs improvements [`#708`](https://github.com/Addepar/ember-table/pull/708) +- DOCS: Avoid passing incorrect/unneeded args to faker.* methods [`#707`](https://github.com/Addepar/ember-table/pull/707) +- DOCS: Fix rendering of addon docs pages [`#706`](https://github.com/Addepar/ember-table/pull/706) +- CHORE: Change jsconfig settings so that tests are included [`#705`](https://github.com/Addepar/ember-table/pull/705) +- Ensure that table column widths are recomputed when columns change [`#690`](https://github.com/Addepar/ember-table/pull/690) +- [BUG] Setting the column width to its current value works [`#684`](https://github.com/Addepar/ember-table/pull/684) +- Move ember-decorators to devDeps [`#685`](https://github.com/Addepar/ember-table/pull/685) +- Convert collapse-tree, column-tree, and ember-td to classic [`#682`](https://github.com/Addepar/ember-table/pull/682) +- thead, th, and tr to classic components [`#680`](https://github.com/Addepar/ember-table/pull/680) +- Convert row-wrapper and simple-checkbox to classic classes [`#681`](https://github.com/Addepar/ember-table/pull/681) +- Start converting to classic classes [`#677`](https://github.com/Addepar/ember-table/pull/677) +- [BUGFIX] Ensure dynamicAlias works on latest Ember [`#679`](https://github.com/Addepar/ember-table/pull/679) +- [CHORE] Update to node 8 [`#670`](https://github.com/Addepar/ember-table/pull/670) +- [BUGFIX] Empty values are sorted properly [`#675`](https://github.com/Addepar/ember-table/pull/675) +- [CHORE] Pin ember-cli-page-object [`#674`](https://github.com/Addepar/ember-table/pull/674) +- [CHORE] Node 6 support - pin jsdom [`#671`](https://github.com/Addepar/ember-table/pull/671) +- [FEATURE] Add support for context menu on header cells [`#662`](https://github.com/Addepar/ember-table/pull/662) +- Upgrade several project dependencies [`#658`](https://github.com/Addepar/ember-table/pull/658) +- Fix build [`#661`](https://github.com/Addepar/ember-table/pull/661) +- Revert "[bugfix] Recompute row meta index when previous prepend causes shift (#623)" [`#651`](https://github.com/Addepar/ember-table/pull/651) +- [bugfix] Recompute TableRowMeta.isSelected on external selection array changes [`#644`](https://github.com/Addepar/ember-table/pull/644) +- [bugfix] Disable ember/new-module-imports rule for special case [`#645`](https://github.com/Addepar/ember-table/pull/645) +- Add yarn.lock [`3d38dad`](https://github.com/Addepar/ember-table/commit/3d38dad619cdc061e24c26db96a078754bedfa45) +- ember-th renders only the block when one is passed. [`91ab599`](https://github.com/Addepar/ember-table/commit/91ab599771972e4099db95c9b476fe16691c8b91) +- DOCS: Improve table-customization docs pages [`fecbdda`](https://github.com/Addepar/ember-table/commit/fecbddafb046890db2168fc80484a09b940eabbd) + +#### [v2.0.0-beta.7](https://github.com/Addepar/ember-table/compare/v2.0.0-beta.6...v2.0.0-beta.7) + +> 19 December 2018 + +- Yield to inverse when tbody rows are empty [`#608`](https://github.com/Addepar/ember-table/pull/608) +- Add optional containerWidthAdjustment [`#637`](https://github.com/Addepar/ember-table/pull/637) +- [bugfix] Recompute row meta index when previous prepend causes shift [`#623`](https://github.com/Addepar/ember-table/pull/623) +- Replace `constructor` with `init` [`#633`](https://github.com/Addepar/ember-table/pull/633) +- Pass through 'idForFirstItem' to the vertical-collection, so scroll-position can be restored [`#616`](https://github.com/Addepar/ember-table/pull/616) + +#### [v2.0.0-beta.6](https://github.com/Addepar/ember-table/compare/v2.0.0-beta.5...v2.0.0-beta.6) + +> 31 October 2018 + +- "Disable collapse" support at a row level [`#629`](https://github.com/Addepar/ember-table/pull/629) +- [bugfix] Only clean caches on table destroy [`#611`](https://github.com/Addepar/ember-table/pull/611) +- Added possibility to set custom container selector [`#619`](https://github.com/Addepar/ember-table/pull/619) +- CHG: remove legacy tree-generator [`#620`](https://github.com/Addepar/ember-table/pull/620) +- FIX: fix styling link in documentation [`#606`](https://github.com/Addepar/ember-table/pull/606) +- [bugfix] Use CSSOM for style manipulations [`#601`](https://github.com/Addepar/ember-table/pull/601) +- [bugfix] Use CSSOM for style manipulations (#601) [`#566`](https://github.com/Addepar/ember-table/issues/566) +- Release v2.0.0-beta.6 [`7d6d54f`](https://github.com/Addepar/ember-table/commit/7d6d54f807ce9d436db1786d6836148979ef8ae2) + +#### [v2.0.0-beta.5](https://github.com/Addepar/ember-table/compare/v2.0.0-beta.4...v2.0.0-beta.5) + +> 26 July 2018 + +- [feat] Extracts the sort indicator [`#597`](https://github.com/Addepar/ember-table/pull/597) +- failing test for selection after rows update [`#596`](https://github.com/Addepar/ember-table/pull/596) +- Update typedefs for table-head functions [`#592`](https://github.com/Addepar/ember-table/pull/592) +- Adds prev, next,first, last computed properties to rowmeta [`#590`](https://github.com/Addepar/ember-table/pull/590) +- [bugfix] Ensures computeds are properly destroyed [`#583`](https://github.com/Addepar/ember-table/pull/583) +- Can add a child leaf to a collapse tree node [`#582`](https://github.com/Addepar/ember-table/pull/582) +- use window scroll position for ember-router-scroll [`#579`](https://github.com/Addepar/ember-table/pull/579) +- remove ember-faker and use faker from npm. use faker to generate random numbers. [`#575`](https://github.com/Addepar/ember-table/pull/575) +- fix README typo [`#576`](https://github.com/Addepar/ember-table/pull/576) +- readme: update columns description to match guide [`#574`](https://github.com/Addepar/ember-table/pull/574) + +#### [v2.0.0-beta.4](https://github.com/Addepar/ember-table/compare/v2.0.0-beta.3...v2.0.0-beta.4) + +> 7 July 2018 + +- [feat] Allow empty valuePath [`#572`](https://github.com/Addepar/ember-table/pull/572) +- [feat] Adds column level configs functionality [`#571`](https://github.com/Addepar/ember-table/pull/571) +- move style dependencies to devDependencies [`#570`](https://github.com/Addepar/ember-table/pull/570) +- allow specifying the collection key [`#568`](https://github.com/Addepar/ember-table/pull/568) +- [bugfix] Properly alias cellValue [`#567`](https://github.com/Addepar/ember-table/pull/567) + +#### [v2.0.0-beta.3](https://github.com/Addepar/ember-table/compare/v2.0.0-beta.2...v2.0.0-beta.3) + +> 28 June 2018 + +- Update notifyPropertyChange polyfill to work with proper versions [`#562`](https://github.com/Addepar/ember-table/pull/562) +- [bugfix] Pin eslint-plugin-prettier [`#564`](https://github.com/Addepar/ember-table/pull/564) +- Update description in package.json [`8519064`](https://github.com/Addepar/ember-table/commit/8519064e2b7c6d596fb42ad7708e6caf6cf9c84e) + +#### [v2.0.0-beta.2](https://github.com/Addepar/ember-table/compare/v2.0.0-beta.1...v2.0.0-beta.2) + +> 18 June 2018 + +- [bugfix] Removes a bunch of unnecessary files that weren't being used [`#557`](https://github.com/Addepar/ember-table/pull/557) +- Doc fix: s/viola/voila [`#555`](https://github.com/Addepar/ember-table/pull/555) +- fix: example in why section [`#550`](https://github.com/Addepar/ember-table/pull/550) +- [bugfix] Fix Column Sorting [`#554`](https://github.com/Addepar/ember-table/pull/554) +- [bugfix] Fix Column Sorting (#554) [`#553`](https://github.com/Addepar/ember-table/issues/553) +- add deploy deps [`588b5e5`](https://github.com/Addepar/ember-table/commit/588b5e506c442f42859e8406e76392aba94c84b4) +- make sure faker is included in production [`2eec4cb`](https://github.com/Addepar/ember-table/commit/2eec4cbe0054c5faa645fdcdefb0084be0b4b90f) +- add repository field [`a4a80e0`](https://github.com/Addepar/ember-table/commit/a4a80e0dadc902e617dc9cf368c2c8ad801003fc) + +#### [v2.0.0-beta.1](https://github.com/Addepar/ember-table/compare/v2.0.0-alpha.2...v2.0.0-beta.1) + +> 16 June 2018 + +- [guides] Finalize Guides for v2.0.0-beta.1 [`#549`](https://github.com/Addepar/ember-table/pull/549) +- [bugfix] Basic Fastboot Support [`#548`](https://github.com/Addepar/ember-table/pull/548) +- [feat] Adds passthrough options for vertical-collection [`#547`](https://github.com/Addepar/ember-table/pull/547) +- [bugfix] Use sticky polyfill in all browsers [`#546`](https://github.com/Addepar/ember-table/pull/546) +- overflow scroll is auto [`#516`](https://github.com/Addepar/ember-table/pull/516) +- [feat] Separate row/checkbox selection, fix reordering/resizing [`#544`](https://github.com/Addepar/ember-table/pull/544) +- [styles] Fixes up the markup for integration with style framework [`#542`](https://github.com/Addepar/ember-table/pull/542) +- [modernize] Adds Contextual Component API [`#541`](https://github.com/Addepar/ember-table/pull/541) +- [refactor] Refactors Sorting Markup [`#540`](https://github.com/Addepar/ember-table/pull/540) +- test to repro footer bug [`#535`](https://github.com/Addepar/ember-table/pull/535) +- [feat] Implement Column Sorting and Actions [`#537`](https://github.com/Addepar/ember-table/pull/537) +- [bugfix] Avoid clickable elements when toggling row selection [`#538`](https://github.com/Addepar/ember-table/pull/538) +- table row meta - dont select if destroying [`#536`](https://github.com/Addepar/ember-table/pull/536) +- [refactor] Remove component references from trees [`#534`](https://github.com/Addepar/ember-table/pull/534) +- [bugfix] Fixes column widths when reordering [`#533`](https://github.com/Addepar/ember-table/pull/533) +- [refactor] Move row meta to CollapseTree [`#532`](https://github.com/Addepar/ember-table/pull/532) +- There is a bug where the width distributor will get into an infinite loop. The loop would check for delta being zero, but if delta started as a decimal, then the check would never pass. This does 3 things: [`#530`](https://github.com/Addepar/ember-table/pull/530) +- [bugfix] Plugs Memory leaks [`#529`](https://github.com/Addepar/ember-table/pull/529) +- [upgrade] Ember 3.0 [`#528`](https://github.com/Addepar/ember-table/pull/528) +- [bugfix] Ensures Cell Meta is unique per table instance [`#527`](https://github.com/Addepar/ember-table/pull/527) +- README: added instructions for documentation [`#526`](https://github.com/Addepar/ember-table/pull/526) +- Add a blank page to perf tests [`#525`](https://github.com/Addepar/ember-table/pull/525) +- readme - minor problem with ember-td [`#524`](https://github.com/Addepar/ember-table/pull/524) +- [optimization] Performance and DX fixes [`#522`](https://github.com/Addepar/ember-table/pull/522) +- [feat] ColumnTree [`#521`](https://github.com/Addepar/ember-table/pull/521) +- [restructure] New Table API [`#517`](https://github.com/Addepar/ember-table/pull/517) +- [restructure] Simplify and flatten caching [`#513`](https://github.com/Addepar/ember-table/pull/513) +- The `tree` attribute is correctly observed by the table [`#515`](https://github.com/Addepar/ember-table/pull/515) +- collapse tree supports ember arrays [`#510`](https://github.com/Addepar/ember-table/pull/510) +- Add Travis badge to README [`#512`](https://github.com/Addepar/ember-table/pull/512) +- fix table sticky polyfill test [`#511`](https://github.com/Addepar/ember-table/pull/511) +- CHG: move ember-useragent to dependencies [`#508`](https://github.com/Addepar/ember-table/pull/508) +- [polyfill] Polyfill the behavior of position: sticky; in thead/tfoot [`#507`](https://github.com/Addepar/ember-table/pull/507) +- [refactor] Combine ember-table and tree-table [`#506`](https://github.com/Addepar/ember-table/pull/506) +- [refactor] Use position: sticky; for fixed headers/footers/columns [`#504`](https://github.com/Addepar/ember-table/pull/504) +- Now a `CollapseTree` has behavior similar to an Ember Array: when you ask for `objectAt(outOfBounds)`, you get `undefined` back. [`#503`](https://github.com/Addepar/ember-table/pull/503) +- [bugfix] Fix fixed column behavior [`#502`](https://github.com/Addepar/ember-table/pull/502) +- [tree-table] Refactor tree-table, make data structures private [`#500`](https://github.com/Addepar/ember-table/pull/500) +- Tests for sending action up after header reordering & resizing [`#492`](https://github.com/Addepar/ember-table/pull/492) +- upgrade(ember-decorators): Upgrade ember-decorators to v2 [`#491`](https://github.com/Addepar/ember-table/pull/491) +- [tests] Cleanup tests [`#486`](https://github.com/Addepar/ember-table/pull/486) +- Integrate with ember-cli-addon-docs and add examples [`#488`](https://github.com/Addepar/ember-table/pull/488) +- Add basic accessibility test [`#487`](https://github.com/Addepar/ember-table/pull/487) +- feat(checkbox): Adds a checkbox option to columns [`#484`](https://github.com/Addepar/ember-table/pull/484) +- chore(deps): Update legacy shim and @argument [`#485`](https://github.com/Addepar/ember-table/pull/485) +- maintenance(VC): Bumps vertical-collection [`#483`](https://github.com/Addepar/ember-table/pull/483) +- refactor(argument): Update to use @argument [`#479`](https://github.com/Addepar/ember-table/pull/479) +- Add max-height for table to fix collapse bug [`#476`](https://github.com/Addepar/ember-table/pull/476) +- [bugfix] Use sticky polyfill in all browsers (#546) [`#545`](https://github.com/Addepar/ember-table/issues/545) +- Merge pull request #515 from Addepar/andy/tree-observer [`#514`](https://github.com/Addepar/ember-table/issues/514) +- only destroy children when they are nodes [`43ceb0e`](https://github.com/Addepar/ember-table/commit/43ceb0ed1799897d8298857be39228cc59b35efa) +- [refactor] Use position: sticky; for fixed headers/footers/columns [`6c9b8a4`](https://github.com/Addepar/ember-table/commit/6c9b8a4d96a13f74371044eda43f9968f74d4ffc) +- tree-table tree argument is now nullable [`3abad7e`](https://github.com/Addepar/ember-table/commit/3abad7ed8b9331aa8c3b401de0a37083a51b8787) + +#### [v2.0.0-alpha.2](https://github.com/Addepar/ember-table/compare/v0.9.2...v2.0.0-alpha.2) + +> 4 January 2018 + +- Use RAF Scheduler in ember table [`#470`](https://github.com/Addepar/ember-table/pull/470) +- Reduce press time wait for hammer [`#473`](https://github.com/Addepar/ember-table/pull/473) +- Send event to outer controller when columns are reordered. [`#469`](https://github.com/Addepar/ember-table/pull/469) +- Allow custom footer to send action up [`#468`](https://github.com/Addepar/ember-table/pull/468) +- Handle 0 length columns gracefully [`#465`](https://github.com/Addepar/ember-table/pull/465) +- Put resize sensor code in a run loop as resize sensor uses RAF [`#467`](https://github.com/Addepar/ember-table/pull/467) +- Add column resizing & footer to Page Objects in test [`#464`](https://github.com/Addepar/ember-table/pull/464) +- ES getter function with @computed to avoid DEPRECATION warnings [`#466`](https://github.com/Addepar/ember-table/pull/466) +- Use function set of ember/object to set collapse property [`#463`](https://github.com/Addepar/ember-table/pull/463) +- Add yarn.lock to gitignore [`#462`](https://github.com/Addepar/ember-table/pull/462) +- Add migration guide to README [`#461`](https://github.com/Addepar/ember-table/pull/461) +- Table PageObject [`#458`](https://github.com/Addepar/ember-table/pull/458) +- Refactor footer API [`#455`](https://github.com/Addepar/ember-table/pull/455) +- Support subcolumns [`#446`](https://github.com/Addepar/ember-table/pull/446) +- API to set row height [`#453`](https://github.com/Addepar/ember-table/pull/453) +- chore(collection): update vertical collection [`#454`](https://github.com/Addepar/ember-table/pull/454) +- chore(deps): update ember cli and deps [`#452`](https://github.com/Addepar/ember-table/pull/452) +- Pass list of arguments instead of the first arguments. [`#442`](https://github.com/Addepar/ember-table/pull/442) +- Fix collapse bug in issues 439 [`#440`](https://github.com/Addepar/ember-table/pull/440) +- Use RAF Scheduler + bug fix [`#436`](https://github.com/Addepar/ember-table/pull/436) +- Fix column width in IE [`#432`](https://github.com/Addepar/ember-table/pull/432) +- Prepare alpha [`#430`](https://github.com/Addepar/ember-table/pull/430) +- Header & row action [`#428`](https://github.com/Addepar/ember-table/pull/428) +- Update ember decorator [`#427`](https://github.com/Addepar/ember-table/pull/427) +- Table Resize mode [`#424`](https://github.com/Addepar/ember-table/pull/424) +- Table theme [`#426`](https://github.com/Addepar/ember-table/pull/426) +- Rename table [`#422`](https://github.com/Addepar/ember-table/pull/422) +- Add Ember try scenarios and remove unused tests [`#41`](https://github.com/Addepar/ember-table/pull/41) +- Fix all table tests [`#40`](https://github.com/Addepar/ember-table/pull/40) +- Fix ui bugs [`#39`](https://github.com/Addepar/ember-table/pull/39) +- Tree Table [`#36`](https://github.com/Addepar/ember-table/pull/36) +- Upgrade VC [`#37`](https://github.com/Addepar/ember-table/pull/37) +- refactor(scheduler): Adds a simple RAF scheduler [`#38`](https://github.com/Addepar/ember-table/pull/38) +- refactor(styles): Switch to flex based styles [`#35`](https://github.com/Addepar/ember-table/pull/35) +- Refactor api [`#34`](https://github.com/Addepar/ember-table/pull/34) +- refactor(scroll): Better scroll handlers [`#33`](https://github.com/Addepar/ember-table/pull/33) +- Support 1.11 [`#32`](https://github.com/Addepar/ember-table/pull/32) +- Selected Rows + Table API change [`#30`](https://github.com/Addepar/ember-table/pull/30) +- Column Fluid mode [`#29`](https://github.com/Addepar/ember-table/pull/29) +- Move column definition to model folder [`#28`](https://github.com/Addepar/ember-table/pull/28) +- Table styling [`#27`](https://github.com/Addepar/ember-table/pull/27) +- Custom cell [`#26`](https://github.com/Addepar/ember-table/pull/26) +- Separate column definition [`#25`](https://github.com/Addepar/ember-table/pull/25) +- Custom row [`#23`](https://github.com/Addepar/ember-table/pull/23) +- Fix initial rendering bug [`#24`](https://github.com/Addepar/ember-table/pull/24) +- Add custom header [`#22`](https://github.com/Addepar/ember-table/pull/22) +- Table column reordering tests [`#21`](https://github.com/Addepar/ember-table/pull/21) +- Test helpers & initial integration tests [`#20`](https://github.com/Addepar/ember-table/pull/20) +- Fix table scrolling [`#19`](https://github.com/Addepar/ember-table/pull/19) +- Use weak map to get tree node. [`#18`](https://github.com/Addepar/ember-table/pull/18) +- Use `row` instead of `row.value` [`#17`](https://github.com/Addepar/ember-table/pull/17) +- feature(cell-proxy): Adds a cell proxy layer for transient state [`#15`](https://github.com/Addepar/ember-table/pull/15) +- Remove calculating _height. [`#16`](https://github.com/Addepar/ember-table/pull/16) +- refactor(styles): Cleans up fixed column styles [`#14`](https://github.com/Addepar/ember-table/pull/14) +- Fixed footer [`#13`](https://github.com/Addepar/ember-table/pull/13) +- Implement fixed header [`#12`](https://github.com/Addepar/ember-table/pull/12) +- Fix lint errors [`#11`](https://github.com/Addepar/ember-table/pull/11) +- Add virtual root [`#10`](https://github.com/Addepar/ember-table/pull/10) +- Change component name to EmberTable2 [`#9`](https://github.com/Addepar/ember-table/pull/9) +- Linked list tree [`#8`](https://github.com/Addepar/ember-table/pull/8) +- Set height table to be equal parent container height. [`#7`](https://github.com/Addepar/ember-table/pull/7) +- Polish header action [`#4`](https://github.com/Addepar/ember-table/pull/4) +- Fix column resizing & ordering bug [`#3`](https://github.com/Addepar/ember-table/pull/3) +- refactor(ES6): Moves to ES Classes [`#2`](https://github.com/Addepar/ember-table/pull/2) +- cleanup(build): Removes Bower, uses NPM and latest vertical-collection [`#1`](https://github.com/Addepar/ember-table/pull/1) +- Remove problematic CSS !importants [`#410`](https://github.com/Addepar/ember-table/pull/410) +- Update Ember-CLI to 2.4.2 [`cf34c50`](https://github.com/Addepar/ember-table/commit/cf34c506cd220ea9d5aadcad6993dac9f43b7e5d) +- Remove V1 code [`bfafba5`](https://github.com/Addepar/ember-table/commit/bfafba54b1f8ad7802f5be36fba73f8e948f03f8) +- update eslint config [`05465d1`](https://github.com/Addepar/ember-table/commit/05465d13b2287a4a9101e80689b71141b42c24b3) + +#### [v0.9.2](https://github.com/Addepar/ember-table/compare/v0.9.1...v0.9.2) + +> 7 August 2015 + +- Release 0.9.2 [`af2daaa`](https://github.com/Addepar/ember-table/commit/af2daaa941603d8da634a44fc3209f6238e1b858) +- Restructured antiscroll dependency [`db73229`](https://github.com/Addepar/ember-table/commit/db732299b8779de74c89b95a9054efae7ca16692) + +#### [v0.9.1](https://github.com/Addepar/ember-table/compare/v0.9.0...v0.9.1) + +> 1 August 2015 + +- Added Acceptance Test [`076355f`](https://github.com/Addepar/ember-table/commit/076355f34bef69c8264d999aadad9e50759dd40b) +- Release 0.9.1 [`e543f55`](https://github.com/Addepar/ember-table/commit/e543f55db1a86a1208e92534f5f7bda99e247a52) +- Use SafeString to avoid style attributes warning [`f49d734`](https://github.com/Addepar/ember-table/commit/f49d73423cecaf69040e800fbd98deb09b8e7f01) + +#### [v0.9.0](https://github.com/Addepar/ember-table/compare/v0.8.0...v0.9.0) + +> 15 July 2015 + +- Upgraded to EmberCLI 0.2.7 [`c359647`](https://github.com/Addepar/ember-table/commit/c3596470225015e5b79b810c55c93b1af0fa0c61) +- Release 0.9.0 [`8fded1c`](https://github.com/Addepar/ember-table/commit/8fded1c1787f163ccad396d7dcbbb19842fb6181) + +#### [v0.8.0](https://github.com/Addepar/ember-table/compare/v0.7.0...v0.8.0) + +> 13 July 2015 + +- Support for Ember 1.11 [`d5b357e`](https://github.com/Addepar/ember-table/commit/d5b357e37b67b42aba80086fdb4d8712bc5a17e3) +- Release 0.8.0 [`d9fb086`](https://github.com/Addepar/ember-table/commit/d9fb08644e7ef13ce0d8887f2b9d0fce283ca1b2) + +#### [v0.7.0](https://github.com/Addepar/ember-table/compare/v0.6.0...v0.7.0) + +> 13 July 2015 + +- Support for Ember 1.10 [`b88b75f`](https://github.com/Addepar/ember-table/commit/b88b75f03b19590d0b51e01a0cb1fd98340dd4d3) +- Release 0.7.0 [`b1c510a`](https://github.com/Addepar/ember-table/commit/b1c510a642349bf3552de8e162f871a5b0b19117) + +#### [v0.6.0](https://github.com/Addepar/ember-table/compare/v0.5.1...v0.6.0) + +> 11 July 2015 + +- Support for Ember 1.9 [`7f90c7c`](https://github.com/Addepar/ember-table/commit/7f90c7c66a88cea0849788b111a7bdd5b4060b1c) +- Release 0.6.0 [`88f3921`](https://github.com/Addepar/ember-table/commit/88f3921a127a2d1b01f101b5c03fcd07e44ef412) + +#### [v0.5.1](https://github.com/Addepar/ember-table/compare/v0.5.0...v0.5.1) + +> 7 July 2015 + +- Adds "demoURL" to package.json [`#323`](https://github.com/Addepar/ember-table/pull/323) +- Remove demo app's dependency on lodash [`d0f9748`](https://github.com/Addepar/ember-table/commit/d0f9748d9ca2c285d48a547dd2e2f4e762e6e4ad) +- Allow content to be a promise [`f7dc957`](https://github.com/Addepar/ember-table/commit/f7dc95703ccebf6f5a642c3880e33222a6a33f91) +- fix issue #165, isLastRow property problem. [`d464dad`](https://github.com/Addepar/ember-table/commit/d464dada6e50e0949be1b8abda756108d94d1020) + +#### [v0.5.0](https://github.com/Addepar/ember-table/compare/v0.4.1...v0.5.0) + +> 22 May 2015 + +- Fix #304 - use `width` instead of `outerWidth` [`#304`](https://github.com/Addepar/ember-table/issues/304) [`#304`](https://github.com/Addepar/ember-table/issues/304) [`#93`](https://github.com/Addepar/ember-table/issues/93) +- Fix style bindings mixin not observing changes after init [`#295`](https://github.com/Addepar/ember-table/issues/295) +- Move to Ember CLI version [`710bd51`](https://github.com/Addepar/ember-table/commit/710bd51ee951f94d7f277ba39c67e9427c04963c) +- Release 0.5.0 [`bcb01ab`](https://github.com/Addepar/ember-table/commit/bcb01ab7f40c557cf0ff62b62fdc7350c3278aae) +- Move back to using pointer style sortable tolerance from intersect [`8df61e6`](https://github.com/Addepar/ember-table/commit/8df61e6fcdbf2c585728a8aa60c5f7076beb83b7) + +#### [v0.4.1](https://github.com/Addepar/ember-table/compare/v0.4.0...v0.4.1) + +> 5 March 2015 + +- Changes to dist files for 1850041ae29247ffc7baec9bad808cca45acf239 [`#267`](https://github.com/Addepar/ember-table/pull/267) +- Prevent jQuery UI from moving DOM nodes itself, otherwise Ember’s morph ... [`#265`](https://github.com/Addepar/ember-table/pull/265) +- Fix various invalid HTML [`#257`](https://github.com/Addepar/ember-table/pull/257) +- Add a mixin to give views access to tableComponent [`5f5807a`](https://github.com/Addepar/ember-table/commit/5f5807a8d908f1260e479fd08aa7be1953264c99) +- Switch back to old API for `selection` [`074ad3d`](https://github.com/Addepar/ember-table/commit/074ad3dcfbbe7c47a5e2a014c293030dc652ad85) +- Remove `_selection` and use content instead of rows in selection logic [`d75c4ad`](https://github.com/Addepar/ember-table/commit/d75c4adf4cb924bc3dafe671f01e5a9a7698e8d1) + +#### [v0.4.0](https://github.com/Addepar/ember-table/compare/v0.3.0...v0.4.0) + +> 6 January 2015 + +- Update contributing guidelines [`#242`](https://github.com/Addepar/ember-table/issues/242) +- Release 0.4.0 [`e9d2448`](https://github.com/Addepar/ember-table/commit/e9d2448d3962112101eaa3b53f8344989bf6fd51) +- Change canAutoResize default to false [`6772402`](https://github.com/Addepar/ember-table/commit/67724028b0bfa76ec9d22ab8df0878fe1546ab5a) + +#### [v0.3.0](https://github.com/Addepar/ember-table/compare/v0.2.4...v0.3.0) + +> 6 January 2015 + +- New column resizing behavior and modes. [`3dd7254`](https://github.com/Addepar/ember-table/commit/3dd725419338d22b75223d1a5fc9174cb0bb7542) +- Release 0.3.0 [`ebd56ac`](https://github.com/Addepar/ember-table/commit/ebd56ac893e0999632a90d563c6c5cc18e410676) +- Revert "Fix rendering issue in Linux Chrome v38" [`fe716a3`](https://github.com/Addepar/ember-table/commit/fe716a3f0710302614c43dcde8210f6896f59767) + +#### [v0.2.4](https://github.com/Addepar/ember-table/compare/v0.2.3...v0.2.4) + +> 5 December 2014 + +- Fix ember-table with Ember.EXTEND_PROTOTYPES=false [`#224`](https://github.com/Addepar/ember-table/issues/224) +- Fix Ember.Set deprecation warnings [`b12852d`](https://github.com/Addepar/ember-table/commit/b12852df82a9c18a4e87c7287384879534288597) +- Release 0.2.4 [`a8c0c59`](https://github.com/Addepar/ember-table/commit/a8c0c5963098dd57f8fb8d2a553119211d628767) +- Fix formatting error in CHANGELOG.md [`25bb053`](https://github.com/Addepar/ember-table/commit/25bb0538756e40b6c98706704c51584acff46a23) + +#### [v0.2.3](https://github.com/Addepar/ember-table/compare/v0.2.2...v0.2.3) + +> 31 October 2014 + +- Clean up comments, adding inputs/output docs [`9ef2f11`](https://github.com/Addepar/ember-table/commit/9ef2f112b40305d1beac2fca8aeb3b74ea55c5fc) +- Update docs pages. [`bc7113d`](https://github.com/Addepar/ember-table/commit/bc7113dea8dc252214d45797704c2d69eece3da9) +- Release 0.2.3 [`2ec2792`](https://github.com/Addepar/ember-table/commit/2ec27925b3a5c233f315b7fa920963e8bff62ad5) + +#### [v0.2.2](https://github.com/Addepar/ember-table/compare/v0.2.1...v0.2.2) + +> 26 September 2014 + +- Fix multiselect bugs [`#192`](https://github.com/Addepar/ember-table/issues/192) +- Improvements to docs pages [`29c7e8b`](https://github.com/Addepar/ember-table/commit/29c7e8b407ecd430997ee4deeb1088d7085281a3) +- Add multiselect functionality [`a938728`](https://github.com/Addepar/ember-table/commit/a9387284a4042ba198ad840d9decec42477e3b9d) +- Publish new gh-pages with every release [`b3ead87`](https://github.com/Addepar/ember-table/commit/b3ead8761b9076b4ef04a50b02b98223b9e2bde3) + +#### [v0.2.1](https://github.com/Addepar/ember-table/compare/v0.2.0...v0.2.1) + +> 30 June 2014 + +- Hide fixed columns table when numFixedColumns is 0 [`#179`](https://github.com/Addepar/ember-table/pull/179) +- Update README.md and CONTRIBUTING.md [`1c47d97`](https://github.com/Addepar/ember-table/commit/1c47d9757ed3e0433daaffaea6b3fefbcd150cbc) +- hide left table when numFixedColumns is 0 [`3f0a755`](https://github.com/Addepar/ember-table/commit/3f0a7553348bc439cf97b5c0036c2d697c51045c) +- should refer to row instead of the content of the row object proxy in the table cells [`5f20b47`](https://github.com/Addepar/ember-table/commit/5f20b47382bdda43394a5c528c371afe20c6aea4) + +#### [v0.2.0](https://github.com/Addepar/ember-table/compare/v0.1.0...v0.2.0) + +> 20 May 2014 + +- Bower and versioning [`#157`](https://github.com/Addepar/ember-table/pull/157) +- Throw exception when ember-table dependencies are missing [`#143`](https://github.com/Addepar/ember-table/pull/143) +- Fix a few minor UI bugs [`#126`](https://github.com/Addepar/ember-table/pull/126) +- Migration to Ember v1.4.0 [`#106`](https://github.com/Addepar/ember-table/pull/106) +- Switcher that enables text selection in table cells [`#105`](https://github.com/Addepar/ember-table/pull/105) +- Added minimum jQuery UI build info to site [`#89`](https://github.com/Addepar/ember-table/pull/89) +- 'template'/'templateName' is not appropriate for components that are reg... [`#90`](https://github.com/Addepar/ember-table/pull/90) +- Missing dependency in README [`#84`](https://github.com/Addepar/ember-table/pull/84) +- Throw exception when ember-table dependencies are missing [`#138`](https://github.com/Addepar/ember-table/issues/138) +- Set up versioning with bower and release-it [`6c56fce`](https://github.com/Addepar/ember-table/commit/6c56fce12490d699a98fee35109e451d4d970ff5) +- Update dependencies with Handlebars 1.3.0 [`e006f8c`](https://github.com/Addepar/ember-table/commit/e006f8cdd7c5817f9232c2f0c3050725f9136b9f) +- track dist folder [`18d7ee9`](https://github.com/Addepar/ember-table/commit/18d7ee9f20d3fc4823a8d3af3618fa5cae735ba3) + +#### v0.1.0 + +> 23 October 2013 + +- Ppong/1.0 wip rebase [`#82`](https://github.com/Addepar/ember-table/pull/82) +- General updates. [`#75`](https://github.com/Addepar/ember-table/pull/75) +- Cleaner Gruntfile, Added JSDoc, Building standalone vanilla lib (without deps) and many more .. [`#64`](https://github.com/Addepar/ember-table/pull/64) +- Using node-static npm package for serving examples - python dependency eliminated, hooray! [`#67`](https://github.com/Addepar/ember-table/pull/67) +- Update ember and handlebars [`#58`](https://github.com/Addepar/ember-table/pull/58) +- Update to Handlebars 1.0.0 [`#54`](https://github.com/Addepar/ember-table/pull/54) +- Update handlebars to rc4 [`#52`](https://github.com/Addepar/ember-table/pull/52) +- Ember table with fluid (%) layout for columns [`#28`](https://github.com/Addepar/ember-table/pull/28) +- Add contentPath attribute [`#33`](https://github.com/Addepar/ember-table/pull/33) +- Fix blank table after switching routes [`#30`](https://github.com/Addepar/ember-table/pull/30) +- Fix ContainerView deprecations in latest Ember [`#27`](https://github.com/Addepar/ember-table/pull/27) +- Dynamic update selection diagnostic [`#26`](https://github.com/Addepar/ember-table/pull/26) +- Selection improvements [`#20`](https://github.com/Addepar/ember-table/pull/20) +- Fix issue when selection is set without content [`#18`](https://github.com/Addepar/ember-table/pull/18) +- Remove Underscore dependency [`#15`](https://github.com/Addepar/ember-table/pull/15) +- better docs, gh_pages and code cleanup [`e7ffeb5`](https://github.com/Addepar/ember-table/commit/e7ffeb5a8995d94a843be48e8512250bd9e49bdc) +- initial commit [`029a624`](https://github.com/Addepar/ember-table/commit/029a62413ae1d8393f0bcda4b69c0fa423146eda) +- Update to ember 1.0.0. [`9aa76c5`](https://github.com/Addepar/ember-table/commit/9aa76c5fca49f5367f34ae39ef6085cf94bf9f21) diff --git a/README.md b/README.md index 8454e6f53..a83ea6b8e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ [![Build Status](https://travis-ci.org/Addepar/ember-table.svg?branch=master)](https://travis-ci.org/Addepar/ember-table) +![npm version](https://img.shields.io/npm/v/ember-table) # Ember Table @@ -13,6 +14,7 @@ ember install ember-table ``` ## Features + - Column resizing, column reordering. - Table resizing. - Fixed first column. @@ -23,9 +25,12 @@ ember install ember-table ## Documentation -For more detailed documentation, clone the repo, run `yarn && yarn start` and then navigate to `http://localhost:4200/docs` +Documentation is available at: https://opensource.addepar.com/ember-table/docs + +Ember Table uses [ember-cli-addon-docs](https://github.com/ember-learn/ember-cli-addon-docs) for its documentation. +To run the docs locally, clone the repo, run `yarn && yarn start` and then navigate to `http://localhost:4200/docs`. -## Usage. +## Usage To use `Ember Table`, you need to create `columns` and `rows` dataset. @@ -36,16 +41,16 @@ each row for that column. If you only want to use the default template, you can specify a `name` on the column which will be rendered in the template. ```javascript - columns: [ - { - name: `Open time`, - valuePath: `open` - }, - { - name: `Close time`, - valuePath: `close` - } - ] +columns: [ + { + name: `Open time`, + valuePath: `open`, + }, + { + name: `Close time`, + valuePath: `close`, + }, +]; ``` `rows` could be a javascript array, ember array or any data structure that implements `length` and @@ -54,21 +59,21 @@ data as user scrolls. Each object in the `rows` data structure should contains a by all `valuePath` in `columns` array. ```javascript - rows: computed(function() { - const rows = emberA(); +rows: computed(function() { + const rows = emberA(); - rows.pushObject({ - open: '8AM', - close: '8PM' - }); + rows.pushObject({ + open: '8AM', + close: '8PM', + }); - rows.pushObject({ - open: '11AM', - close: '9PM' - }); + rows.pushObject({ + open: '11AM', + close: '9PM', + }); - return rows; - }) + return rows; +}); ``` ### Template @@ -87,11 +92,11 @@ You can use the block form of the table to customize its template. The component structure matches that of actual HTML tables, and allows you to customize it at any level. At the cell level, you get access to these four values: -* `value` - The value of the cell -* `cell` - A unique cell cache. You can use this to track cell state without +- `value` - The value of the cell +- `cell` - A unique cell cache. You can use this to track cell state without dirtying the underlying model. -* `column` - The column itself. -* `row` - The row itself. +- `column` - The column itself. +- `row` - The row itself. You can use these values to customize cell in many ways. For instance, if you want to have every cell in a particular column use a component, you can add a @@ -120,7 +125,6 @@ If you want to use default table style, import the `ember-table/default` SASS fi You can also use the `ember-tfoot` component, which has the same API as `ember-tbody`: - ``` {{#ember-table as |t|}} {{t.head columns=columns}} @@ -131,19 +135,39 @@ You can also use the `ember-tfoot` component, which has the same API as {{/ember-table}} ``` +## Browser compatibility + +This project is written using an EcmaScript 6 javascript syntax. +Babel doesn't include polyfill by default, so if you want to target legacy browsers (eg. IE11), +you need to add this to your `ember-cli-build.js`: + +```js +var app = new EmberApp({ + 'ember-cli-babel': { + includePolyfill: true, + }, +}); +``` ## Migrating from old Ember table + To support smooth migration from old version of Ember table (support only till ember 1.11), we have move the old source code to separate package [ember-table-legacy](https://github.com/Addepar/ember-table-legacy). It's a separate package from this Ember table package and you can install it using yarn or npm. This allows you to have 2 versions of ember table in your code base and you can start your migrating one table at at time. The recommended migration steps are as follows (if you are using ember 1.11): -1) Rename all your ember-table import to ember-table-legacy. (for example: -`import EmberTable from 'ember-table/components/ember-table'` becomes -`import EmberTableLegacy from 'ember-table-legacy/components/ember-table-legacy'`. Remove reference -of `ember-table` in `package.json`. -2) Install `ember-table-legacy` using `yarn add ember-table-legacy` or `npm install ember-table-legacy` -3) Run your app to make sure that it works without issue. -4) Reinstall the latest version of this `ember-table` repo. -5) You can start using new version of Ember table from now or replacing the old ones. +1. Rename all your ember-table import to ember-table-legacy. (for example: + `import EmberTable from 'ember-table/components/ember-table'` becomes + `import EmberTableLegacy from 'ember-table-legacy/components/ember-table-legacy'`. Remove reference + of `ember-table` in `package.json`. +2. Install `ember-table-legacy` using `yarn add ember-table-legacy` or `npm install ember-table-legacy` +3. Run your app to make sure that it works without issue. +4. Reinstall the latest version of this `ember-table` repo. +5. You can start using new version of Ember table from now or replacing the old ones. + +# Releasing new versions (for maintainers) + +We use [`release-it`](https://github.com/release-it/release-it). +To create a new release, run `yarn run release`. To do a dry-run: `yarn run release --dry-run`. +The tool will prompt you to select the new release version. diff --git a/addon-test-support/index.js b/addon-test-support/index.js new file mode 100644 index 000000000..1ec2a356b --- /dev/null +++ b/addon-test-support/index.js @@ -0,0 +1,3 @@ +import TablePage from './pages/ember-table'; + +export { TablePage }; diff --git a/addon-test-support/pages/-private/ember-table-body.js b/addon-test-support/pages/-private/ember-table-body.js index a601135f5..df4da182f 100644 --- a/addon-test-support/pages/-private/ember-table-body.js +++ b/addon-test-support/pages/-private/ember-table-body.js @@ -1,11 +1,24 @@ -import { alias, triggerable, collection, hasClass, property } from 'ember-classy-page-object'; +import PageObject, { + alias, + triggerable, + collection, + hasClass, + property, +} from 'ember-classy-page-object'; import { findElement } from 'ember-classy-page-object/extend'; import { click } from 'ember-native-dom-helpers'; -export default { +export default PageObject.extend({ scope: 'tbody', + /** + Returns the number of rows in the body. + */ + get rowCount() { + return Number(findElement(this).getAttribute('data-test-row-count')); + }, + /** List of rows in table body. Each of property/function in this collections is the property/func of a single row selected by using calling rows.objectAt(index). @@ -73,4 +86,4 @@ export default { getCell(rowIndex, columnIndex) { return this.rows.objectAt(rowIndex).cells.objectAt(columnIndex); }, -}; +}); diff --git a/addon-test-support/pages/-private/ember-table-footer.js b/addon-test-support/pages/-private/ember-table-footer.js index af9509ea9..ff5bd9c8b 100644 --- a/addon-test-support/pages/-private/ember-table-footer.js +++ b/addon-test-support/pages/-private/ember-table-footer.js @@ -1,17 +1,10 @@ import { collection } from 'ember-classy-page-object'; +import EmberTableBody from './ember-table-body'; -export default { +export default EmberTableBody.extend({ scope: 'tfoot', footers: collection({ scope: 'td', }), - - rows: collection({ - scope: 'tr', - - cells: collection({ - scope: 'td', - }), - }), -}; +}); diff --git a/addon-test-support/pages/-private/ember-table-header.js b/addon-test-support/pages/-private/ember-table-header.js index b26174d2f..1175b2ca5 100644 --- a/addon-test-support/pages/-private/ember-table-header.js +++ b/addon-test-support/pages/-private/ember-table-header.js @@ -1,10 +1,25 @@ -import PageObject, { collection, hasClass } from 'ember-classy-page-object'; +import PageObject, { alias, collection, hasClass, triggerable } from 'ember-classy-page-object'; import { findElement } from 'ember-classy-page-object/extend'; import { click } from 'ember-native-dom-helpers'; import { mouseDown, mouseMove, mouseUp } from '../../helpers/mouse'; import { getScale } from '../../helpers/element'; +export const SortPage = PageObject.extend({ + indicator: { + scope: '[data-test-sort-indicator]', + isAscending: hasClass('is-ascending'), + isDescending: hasClass('is-descending'), + }, + toggle: { + scope: '[data-test-sort-toggle]', + }, +}); + +export const ResizePage = PageObject.extend({ + scope: '[data-test-resize-handle]', +}); + const Header = PageObject.extend({ get text() { return findElement(this) @@ -33,6 +48,8 @@ const Header = PageObject.extend({ isFixedLeft: hasClass('is-fixed-left'), isFixedRight: hasClass('is-fixed-right'), + contextMenu: triggerable('contextmenu'), + /** * Resizes this column by dragging right border several pixels. */ @@ -103,11 +120,11 @@ const Header = PageObject.extend({ isSortable: hasClass('is-sortable'), - sortIndicator: { - scope: '[data-test-sort-indicator]', - isAscending: hasClass('is-ascending'), - isDescending: hasClass('is-descending'), - }, + sort: SortPage, + sortIndicator: alias('sort.indicator'), + sortToggle: alias('sort.toggle'), + + resizeHandle: ResizePage, }); export default { @@ -118,6 +135,13 @@ export default { */ headers: collection('th', Header), + /** + Returns the number of rows in the footer. + */ + get rowCount() { + return Number(findElement(this).getAttribute('data-test-row-count')); + }, + rows: collection({ scope: 'tr', }), diff --git a/addon-test-support/pages/ember-table.js b/addon-test-support/pages/ember-table.js index 742dad870..7931a5656 100644 --- a/addon-test-support/pages/ember-table.js +++ b/addon-test-support/pages/ember-table.js @@ -66,13 +66,24 @@ export default PageObject.extend({ }, /** - * Selects a range of rows in the body + * Selects a range of rows in the body with simple click first * * @param {number} beginIndex * @param {number} endIndex */ - async selectRange(beginIndex, endIndex) { + async selectRangeFromClick(beginIndex, endIndex) { await this.body.rows.objectAt(beginIndex).click(); await this.body.rows.objectAt(endIndex).clickWith({ shiftKey: true }); }, + + /** + * Selects a range of rows in the body with shift+click first + * + * @param {number} beginIndex + * @param {number} endIndex + */ + async selectRangeFromShiftClick(beginIndex, endIndex) { + await this.body.rows.objectAt(beginIndex).clickWith({ shiftKey: true }); + await this.body.rows.objectAt(endIndex).clickWith({ shiftKey: true }); + }, }); diff --git a/addon/-private/collapse-tree.js b/addon/-private/collapse-tree.js index 886ea8108..8636acf16 100644 --- a/addon/-private/collapse-tree.js +++ b/addon/-private/collapse-tree.js @@ -1,9 +1,9 @@ import EmberObject, { get, set } from '@ember/object'; import EmberArray, { A as emberA, isArray } from '@ember/array'; -import { assert } from '@ember/debug'; +import { assert, warn } from '@ember/debug'; -import { computed } from '@ember-decorators/object'; -import { addObserver } from '@ember/object/observers'; +import { computed } from '@ember/object'; +import { addObserver } from './utils/observer'; import { objectAt } from './utils/array'; import { notifyPropertyChange } from './utils/ember'; @@ -16,44 +16,35 @@ export const SELECT_MODE = { MULTIPLE: 'multiple', }; -export class TableRowMeta extends EmberObject { - _rowValue = null; +export const TableRowMeta = EmberObject.extend({ + _rowValue: null, + _isCollapsed: false, - /** - The map that contains cell meta information for this row. Is meant to be - unique to this row, which is why it is created here. In order to prevent - memory leaks, we need to be able to clean the cache manually when the row - is destroyed or updated, which is why we use a Map instead of WeakMap - */ - _cellMetaCache = new Map(); - _isCollapsed = false; - _lastKnownIndex = null; - - @computed('_rowValue.isCollapsed') - get isCollapsed() { - let rowValue = get(this, '_rowValue'); + isCollapsed: computed('_rowValue.isCollapsed', { + get() { + let rowValue = get(this, '_rowValue'); - if (rowValue.hasOwnProperty('isCollapsed')) { - return get(rowValue, 'isCollapsed'); - } else { - return this._isCollapsed; - } - } + if (rowValue.hasOwnProperty('isCollapsed')) { + return get(rowValue, 'isCollapsed'); + } else { + return this._isCollapsed; + } + }, - set isCollapsed(isCollapsed) { - let rowValue = get(this, '_rowValue'); + set(key, isCollapsed) { + let rowValue = get(this, '_rowValue'); - if (rowValue.hasOwnProperty('isCollapsed')) { - set(rowValue, 'isCollapsed', isCollapsed); - } else { - this._isCollapsed = isCollapsed; - } + if (rowValue.hasOwnProperty('isCollapsed')) { + set(rowValue, 'isCollapsed', isCollapsed); + } else { + this._isCollapsed = isCollapsed; + } - return isCollapsed; - } + return isCollapsed; + }, + }), - @computed('_tree.selection.[]', '_parentMeta.isSelected') - get isSelected() { + isSelected: computed('_tree.selection.[]', '_parentMeta.isSelected', function() { let rowValue = get(this, '_rowValue'); let selection = get(this, '_tree.selection'); @@ -62,10 +53,9 @@ export class TableRowMeta extends EmberObject { } return selection === rowValue || get(this, '_parentMeta.isSelected'); - } + }), - @computed('_tree.selection.[]', '_parentMeta.isSelected') - get isGroupSelected() { + isGroupSelected: computed('_tree.selection.[]', '_parentMeta.isSelected', function() { let rowValue = get(this, '_rowValue'); let selection = get(this, '_tree.selection'); @@ -74,70 +64,68 @@ export class TableRowMeta extends EmberObject { } return selection.includes(rowValue) || get(this, '_parentMeta.isGroupSelected'); - } - - @computed('_tree.{enableTree,enableCollapse}', '_rowValue.{children.[],disableCollapse}') - get canCollapse() { - if (!get(this, '_tree.enableTree') || !get(this, '_tree.enableCollapse')) { - return false; - } + }), + + canCollapse: computed( + '_tree.{enableTree,enableCollapse}', + '_rowValue.{children.[],disableCollapse}', + function() { + if (!get(this, '_tree.enableTree') || !get(this, '_tree.enableCollapse')) { + return false; + } - let children = get(this, '_rowValue.children'); + let children = get(this, '_rowValue.children'); - return ( - !get(this, '_rowValue.disableCollapse') && isArray(children) && get(children, 'length') > 0 - ); - } + return ( + !get(this, '_rowValue.disableCollapse') && isArray(children) && get(children, 'length') > 0 + ); + } + ), - @computed('_parentMeta.depth') - get depth() { + depth: computed('_parentMeta.depth', function() { let parentMeta = get(this, '_parentMeta'); return parentMeta ? get(parentMeta, 'depth') + 1 : 0; - } + }), - @computed('_lastKnownIndex', '_prevSiblingMeta.index') - get index() { - let prevSiblingIndex = get(this, '_prevSiblingMeta.index'); - let lastKnownIndex = get(this, '_lastKnownIndex'); - - if (lastKnownIndex === prevSiblingIndex) { - return lastKnownIndex + 1; - } - - return lastKnownIndex; - } - - @computed('_tree.length') - get first() { + first: computed('_tree.length', function() { if (get(this, '_tree.length') === 0) { return null; } return get(this, '_tree').objectAt(0); - } + }), - @computed('_tree.length') - get last() { + last: computed('_tree.length', function() { let tree = get(this, '_tree'); return tree.objectAt(get(tree, 'length') - 1); - } + }), - @computed('_tree.length') - get next() { + next: computed('_tree.length', function() { let tree = get(this, '_tree'); - if (get(this, '_lastKnownIndex') + 1 >= get(tree, 'length')) { + if (get(this, 'index') + 1 >= get(tree, 'length')) { return null; } - return tree.objectAt(get(this, '_lastKnownIndex') + 1); - } + return tree.objectAt(get(this, 'index') + 1); + }), - @computed('_tree.length') - get prev() { - if (get(this, '_lastKnownIndex') === 0) { + prev: computed('_tree.length', function() { + if (get(this, 'index') === 0) { return null; } - return get(this, '_tree').objectAt(get(this, '_lastKnownIndex') - 1); - } + return get(this, '_tree').objectAt(get(this, 'index') - 1); + }), + + init() { + this._super(...arguments); + + /** + The map that contains cell meta information for this row. Is meant to be + unique to this row, which is why it is created here. In order to prevent + memory leaks, we need to be able to clean the cache manually when the row + is destroyed or updated, which is why we use a Map instead of WeakMap + */ + this._cellMetaCache = new Map(); + }, toggleCollapse() { let canCollapse = get(this, 'canCollapse'); @@ -145,7 +133,7 @@ export class TableRowMeta extends EmberObject { if (canCollapse) { set(this, 'isCollapsed', !get(this, 'isCollapsed')); } - } + }, select({ single, toggle, range } = {}) { if (get(this, 'isDestroying') || get(this, 'isDestroyed')) { @@ -175,8 +163,10 @@ export class TableRowMeta extends EmberObject { // Use a set to avoid item duplication let { _lastSelectedIndex } = tree; - let minIndex = Math.min(_lastSelectedIndex, rowIndex); - let maxIndex = Math.max(_lastSelectedIndex, rowIndex); + let isFirstIndexDefined = typeof _lastSelectedIndex === 'number'; + + let minIndex = isFirstIndexDefined ? Math.min(_lastSelectedIndex, rowIndex) : rowIndex; + let maxIndex = isFirstIndexDefined ? Math.max(_lastSelectedIndex, rowIndex) : rowIndex; for (let i = minIndex; i <= maxIndex; i++) { selection.add(tree.objectAt(i)); @@ -186,13 +176,44 @@ export class TableRowMeta extends EmberObject { let meta = this; let currentValue = rowValue; + // If the parent is selected all of its children are selected. Since + // the current row is going to be removed from the selection, add all + // the sibling rows at each level of its grouping to be explicitly + // selected so their state remains stable. while (get(meta, '_parentMeta.isSelected')) { meta = get(meta, '_parentMeta'); - for (let child of get(meta, '_rowValue.children')) { - if (child !== currentValue) { - selection.add(child); + // Iterate from the parent meta to the "next" tree node. Since this + // is a group it will have at least one child, so there should be at + // least one next row to iterate over. + let expectedChildDepth = get(meta, 'depth') + 1; + let childIndex = get(meta, 'index'); // will be incremented by 1 before use + let child; + while ((child = tree.objectAt(++childIndex))) { + // The currentValue is being toggled, don't add it to the selection + if (child === currentValue) { + continue; + } + + // If the depth of the row is lower than the expectedChildDepth a + // non-child meta has been found (a sibling or something higher. + // That means iterating children is complete, so break. + // + // If the depth is higher than expected then children of a child + // group are being iterated. Skip over them, but don't break since + // there may be a leaf child after a group child. + let childMeta = rowMetaCache.get(child); + let childDepth = get(childMeta, 'depth'); + if (childDepth < expectedChildDepth) { + break; } + if (childDepth > expectedChildDepth) { + continue; + } + + // Else, this is a child node which must be explictly selected. + // Add it to the list. + selection.add(child); } selection.delete(currentValue); @@ -208,7 +229,7 @@ export class TableRowMeta extends EmberObject { selection.add(rowValue); } - let rowMetas = Array.from(selection).map(r => rowMetaCache.get(r)); + let rowMetas = mapSelectionToMeta(this.get('_tree'), selection, rowMetaCache); if (selectingChildrenSelectsParent) { let groupingCounts = new Map(); @@ -244,14 +265,14 @@ export class TableRowMeta extends EmberObject { tree.sendAction('onSelect', selection); tree._lastSelectedIndex = rowIndex; - } + }, destroy() { - super.destroy(); + this._super(); this._cellMetaCache.clear(); - } -} + }, +}); function reduceSelectedRows(selection, groupingCounts, rowMetaCache) { let reducedGroupingCounts = new Map(); @@ -291,13 +312,83 @@ function setupRowMeta(tree, row, parentRow, node) { } /** - Given a list of ordered values and a target value, finds the index of - the closest value which does not exceed the target value + * Traverses the tree to set up row meta for every row in the tree. + * Usually row metas are lazily created as needed, but it's possible to end up in a state + * where a table's `selection` contains rows that do not have a rowMeta (for instance, if they + * have not yet been rendered due to occlusion rendering). In this state, there may not be a + * rowMeta for every row in the `selection`, so we need to explicitly set them all up at that + * time. + * This has adverse performance impact, so we lazily call this function only when we find that + * the `selection` has some rows with no corresponding rowMeta. + * + * @param {CollapseTree} tree The collapse tree for this section (body|footer) of the table + * @param {object} parentRow The parent row. Only present when called recursively + */ +function setupAllRowMeta(tree, rows, parentRow = null) { + for (let row of rows) { + setupRowMeta(tree, row, parentRow); + if (row.children && row.children.length) { + setupAllRowMeta(tree, row.children, row); + } + } +} + +/** + * Maps the selection to an array of rowMetas. + * + * If any row in the selection does not have a rowMeta, calls `setupAllRowMeta` + * to materialize all rowMetas, then tries again to get the rowMeta for that + * row. This happens in rare cases where, due to occlusion rendering, a row may + * be part of the selection but not in view (and thus have no rowMeta; the + * rowMeta is lazily created when the row is rendered). + * + * If after calling `setupAllRowMeta` the row still does not have a + * corresponding rowMeta, it is likely an invalid selection, which can happen when a user + * sets the table's selection programmatically and includes a row that is not + * actually part of the table. If this happens we `warn` because of the adverse + * performance impact (the forced call to `setupAllRowMeta`) that is caused by + * spurious rows in the selection. + * @param {CollapseTree} tree The collapse tree for this section (body|footer) of the table + * @param {Set|Array} selection The selected rows + * @return {rowMeta[]} rowMeta for each of the rows in the selection + */ +function mapSelectionToMeta(tree, selection) { + let rowMetaCache = tree.get('rowMetaCache'); + let rowMetas = []; + let didSetupAllRowMeta = false; + + for (let item of Array.from(selection)) { + let rowMeta = rowMetaCache.get(item); + if (!rowMeta && !didSetupAllRowMeta) { + setupAllRowMeta(tree, tree.get('rows')); + didSetupAllRowMeta = true; + rowMeta = rowMetaCache.get(item); + } + + if (!rowMeta && didSetupAllRowMeta) { + warn( + "[ember-table] The selection included a row that was not found in the table's rows. This should be avoided as it causes performance issues.", + false, + { + id: 'ember-table.selection-invalid', + } + ); + } else { + rowMetas.push(rowMeta); + } + } + + return rowMetas; +} - @param {Array} values - the list of values - @param {number} target - the index to find the closest value to - @return {number} - the index of the value closest to the target -*/ +/** + Given a list of ordered values and a target value, finds the index of + the closest value which does not exceed the target value + + @param {Array} values - the list of values + @param {number} target - the index to find the closest value to + @return {number} - the index of the value closest to the target + */ function closestLessThan(values, target) { let low = 0; let high = values.length - 1; @@ -319,13 +410,13 @@ function closestLessThan(values, target) { } /** - Single node of a CollapseTree -*/ -class CollapseTreeNode extends EmberObject { - _childNodes = null; + Single node of a CollapseTree + */ +const CollapseTreeNode = EmberObject.extend({ + _childNodes: null, init() { - super.init(...arguments); + this._super(...arguments); let value = get(this, 'value'); let parentValue = get(this, 'parent.value'); @@ -343,25 +434,25 @@ class CollapseTreeNode extends EmberObject { if (parent) { // Changes to the value directly should properly update all computeds on this - // node, but we need to manually propogate changes upwards to notify any other + // node, but we need to manually propagate changes upwards to notify any other // watchers addObserver(this, 'length', () => { notifyPropertyChange(parent, 'length'); }); } - } + }, destroy() { this.cleanChildNodes(); - super.destroy(...arguments); - } + this._super(...arguments); + }, /** - Fully destroys the child nodes in the event that they change or that this - node is destroyed. If children are not destroyed, they will leak memory due - to dangling references in Ember Meta. - */ + Fully destroys the child nodes in the event that they change or that this + node is destroyed. If children are not destroyed, they will leak memory due + to dangling references in Ember Meta. + */ cleanChildNodes() { if (this._childNodes) { for (let child of this._childNodes) { @@ -371,72 +462,74 @@ class CollapseTreeNode extends EmberObject { } this._childNodes = null; } - } + }, /** - Whether or not the node is leaf of the CollapseTree. A node is a leaf if - the wrapped value's children have no children. If so, there is no need to - create another level of nodes in the tree - true leaves of the passed in - value tree don't require any custom logic, so we can index directly into - the array of children in `objectAt`. - - @type boolean - */ - @computed('value.children.@each.children', 'isRoot', 'tree.enableTree') - get isLeaf() { + Whether or not the node is leaf of the CollapseTree. A node is a leaf if + the wrapped value's children have no children. If so, there is no need to + create another level of nodes in the tree - true leaves of the passed in + value tree don't require any custom logic, so we can index directly into + the array of children in `objectAt`. + + @type boolean + */ + isLeaf: computed('value.children.@each.children', 'isRoot', 'tree.enableTree', function() { if (get(this, 'isRoot') && !get(this, 'tree.enableTree')) { return true; } return !get(this, 'value.children').some(child => isArray(get(child, 'children'))); - } - - @computed('value.children.[]', 'tree.{sorts.[],sortFunction,compareFunction}') - get sortedChildren() { - let valueChildren = get(this, 'value.children'); - - let sorts = get(this, 'tree.sorts'); - let sortFunction = get(this, 'tree.sortFunction'); - let compareFunction = get(this, 'tree.compareFunction'); + }), + + sortedChildren: computed( + 'value.children.[]', + 'tree.{sorts.[],sortFunction,compareFunction,sortEmptyLast}', + function() { + let valueChildren = get(this, 'value.children'); + + let sorts = get(this, 'tree.sorts'); + let sortFunction = get(this, 'tree.sortFunction'); + let compareFunction = get(this, 'tree.compareFunction'); + let sortEmptyLast = get(this, 'tree.sortEmptyLast'); + + if (sortFunction && compareFunction && sorts && get(sorts, 'length') > 0) { + valueChildren = mergeSort(valueChildren, (itemA, itemB) => { + return sortFunction(itemA, itemB, sorts, compareFunction, sortEmptyLast); + }); + } - if (sortFunction && compareFunction && sorts && get(sorts, 'length') > 0) { - valueChildren = mergeSort(valueChildren, (itemA, itemB) => { - return sortFunction(itemA, itemB, sorts, compareFunction); - }); + return valueChildren; } - - return valueChildren; - } + ), /** - The children of this node, if they exist. Children can be other nodes, or - spans (arrays) of leaf value-nodes. For instance: - - ``` - A - └── B - ├── C - └── D - └── E - ``` - - In this example, A would have the following children: - - ``` - children = [ - [B, C], - Node(D) - ]; - ``` - - This allows us to do a binary search on the list of children without - creating a node for each span, arrays simply represent x-children in - a segment before a given node. - - @type Array> - */ - @computed('sortedChildren.[]', 'isLeaf') - get childNodes() { + The children of this node, if they exist. Children can be other nodes, or + spans (arrays) of leaf value-nodes. For instance: + + ``` + A + └── B + ├── C + └── D + └── E + ``` + + In this example, A would have the following children: + + ``` + children = [ + [B, C], + Node(D) + ]; + ``` + + This allows us to do a binary search on the list of children without + creating a node for each span, arrays simply represent x-children in + a segment before a given node. + + @type Array> + */ + childNodes: computed('sortedChildren.[]', 'isLeaf', function() { this.cleanChildNodes(); if (get(this, 'isLeaf')) { @@ -470,70 +563,69 @@ class CollapseTreeNode extends EmberObject { this._childNodes = children; return children; - } + }), /** - The length of the node. Branches in three directions: - - 1. If the node is collapsed, then the length of the node is 1, the - node itself. This means that the parent node will only index into - this child if it is trying to get exactly the child node, - effectively hiding its children. - 2. If the node is a leaf, then the length is the node itself plus the - length of its value-children. - 3. Otherwise, the length is the sum of the lengths of its children. - */ - @computed( + The length of the node. Branches in three directions: + + 1. If the node is collapsed, then the length of the node is 1, the + node itself. This means that the parent node will only index into + this child if it is trying to get exactly the child node, + effectively hiding its children. + 2. If the node is a leaf, then the length is the node itself plus the + length of its value-children. + 3. Otherwise, the length is the sum of the lengths of its children. + */ + length: computed( 'childNodes.[]', 'sortedChildren.[]', 'isLeaf', 'rowMeta.isCollapsed', - 'tree.enableTree' - ) - get length() { - if (get(this, 'rowMeta.isCollapsed') === true) { - return 1; - } else if (get(this, 'isLeaf')) { - return 1 + get(this, 'sortedChildren.length'); - } else { - return 1 + get(this, 'childNodes').reduce((sum, child) => sum + get(child, 'length'), 0); + 'tree.enableTree', + function() { + if (get(this, 'rowMeta.isCollapsed') === true) { + return 1; + } else if (get(this, 'isLeaf')) { + return 1 + get(this, 'sortedChildren.length'); + } else { + return 1 + get(this, 'childNodes').reduce((sum, child) => sum + get(child, 'length'), 0); + } } - } + ), /** - Calculates a list of the summation of offsets of children to run a binary - search against. Given: - - ``` - A - ├── B - │ ├── C - │ └── D - ├── E(c) - │ ├── F - │ └── G - └── H - │ ├── I - │ └── J - │ └── K - └── L - └── M - ``` - - The offsetList for A would be: `[0, 3, 6, 10, 11]`. Each item in this - list is the offset of the corresponding child, or the summation of the - lengths of all children preceding it. It is effectively the starting - index of that child. - - So, if I'm trying to find index 9 in A, which is item K (not counting A - itself), then I'm going to want to traverse down H, which is the 3rd child. - I run a binary search against these offsets, which are ordered, and find - the closest starting index which is strictly less than 9, which is the 3rd - index. I know I can then recurse down that node and I should eventually - find the item I'm after. - */ - @computed('length', 'isLeaf') - get offsetList() { + Calculates a list of the summation of offsets of children to run a binary + search against. Given: + + ``` + A + ├── B + │ ├── C + │ └── D + ├── E(c) + │ ├── F + │ └── G + └── H + │ ├── I + │ └── J + │ └── K + └── L + └── M + ``` + + The offsetList for A would be: `[0, 3, 6, 10, 11]`. Each item in this + list is the offset of the corresponding child, or the summation of the + lengths of all children preceding it. It is effectively the starting + index of that child. + + So, if I'm trying to find index 9 in A, which is item K (not counting A + itself), then I'm going to want to traverse down H, which is the 3rd child. + I run a binary search against these offsets, which are ordered, and find + the closest starting index which is strictly less than 9, which is the 3rd + index. I know I can then recurse down that node and I should eventually + find the item I'm after. + */ + offsetList: computed('length', 'isLeaf', function() { if (get(this, 'isLeaf')) { return null; } @@ -547,27 +639,27 @@ class CollapseTreeNode extends EmberObject { } return offsetList; - } + }), /** - Finds the object at the given index, where an index n is defined as the n-th - item visited during a depth first traversal of the tree. To do this, we either - - 1. Return the current node at index 0 - 2. If the node is a leaf, return the value child at the corresponding index - 3. Otherwise, find the correct child to walk down to and call `objectAt` on it - with a normalized index - - `objectAt` also tracks the depth to pass back as meta information, something - that is useful for displaying the tree as a list. `index` and `depth` are - normalized as we traverse the tree, every time you "pass" a node you subtract - it from the index for the next `objectAt` call, and you add 1 to depth for - every `objectAt` call. - - @param {number} index - the index to find - @param {Array} parents - the parents of the current node in the traversal - @return {{ value: object, parents: Array }} - */ + Finds the object at the given index, where an index n is defined as the n-th + item visited during a depth first traversal of the tree. To do this, we either + + 1. Return the current node at index 0 + 2. If the node is a leaf, return the value child at the corresponding index + 3. Otherwise, find the correct child to walk down to and call `objectAt` on it + with a normalized index + + `objectAt` also tracks the depth to pass back as meta information, something + that is useful for displaying the tree as a list. `index` and `depth` are + normalized as we traverse the tree, every time you "pass" a node you subtract + it from the index for the next `objectAt` call, and you add 1 to depth for + every `objectAt` call. + + @param {number} index - the index to find + @param {Array} parents - the parents of the current node in the traversal + @return {{ value: object, parents: Array }} + */ objectAt(index) { assert( 'index must be gte than 0 and less than the length of the node', @@ -606,88 +698,87 @@ class CollapseTreeNode extends EmberObject { } return child.objectAt(normalizedIndex); - } -} + }, +}); /** - The goal of the collapse tree is provide a data structure that: - - 1. Given an index n, can find the n-th node visited in a depth-first-walk - of the tree - 2. Can "hide" or "collapse" nodes, so that their children are not walked - - So given a tree like this, where the (c) annotation means "isCollapsed": - - ``` - A - ├── B - │ ├── C - │ └── D - ├── E(c) - │ ├── F - │ └── G - └── H - │ ├── I - │ └── J - │ └── K - └── L - ``` - - `objectAt(0) === A`, `objectAt(2) === C`, `objectAt(4) === E`, and - `objectAt(5) === H` - - We also want to wrap this structure around a pre-existing tree that is a much - simpler POJO with the shape: - - ```json - { + The goal of the collapse tree is provide a data structure that: + + 1. Given an index n, can find the n-th node visited in a depth-first-walk + of the tree + 2. Can "hide" or "collapse" nodes, so that their children are not walked + + So given a tree like this, where the (c) annotation means "isCollapsed": + + ``` + A + ├── B + │ ├── C + │ └── D + ├── E(c) + │ ├── F + │ └── G + └── H + │ ├── I + │ └── J + │ └── K + └── L + ``` + + `objectAt(0) === A`, `objectAt(2) === C`, `objectAt(4) === E`, and + `objectAt(5) === H` + + We also want to wrap this structure around a pre-existing tree that is a much + simpler POJO with the shape: + + ```json + { isCollapsed: false, children: [{ isCollapsed: true, children: [] }] } - ``` - - This allows us to provide a simple API to users while being able to index into - their tree quickly and turn it into a list/table representation, without exposing - any internal implementation details. - - To do this, each node in the tree has a `length` equal to the lengths of its - children, and we do a binary search of each layer of the tree to find the closest - node to the index. We traverse downward until we have the correct node, getting - there in O(log(n)) time at worst (where n is the average number of nodes in a layer). - - Whenever a level of the tree changes (e.g. a node is added or removed) we must - rebuild the subtree for that level. In order to keep tree construction and - allocation costs low, we also do not create nodes for leaf children, since there is - no need - they are length 1 and have no children, so no custom. Our tree saves an - order of magnitude of space and allocation costs this way. -*/ -export default class CollapseTree extends EmberObject.extend(EmberArray) { + ``` + + This allows us to provide a simple API to users while being able to index into + their tree quickly and turn it into a list/table representation, without exposing + any internal implementation details. + + To do this, each node in the tree has a `length` equal to the lengths of its + children, and we do a binary search of each layer of the tree to find the closest + node to the index. We traverse downward until we have the correct node, getting + there in O(log(n)) time at worst (where n is the average number of nodes in a layer). + + Whenever a level of the tree changes (e.g. a node is added or removed) we must + rebuild the subtree for that level. In order to keep tree construction and + allocation costs low, we also do not create nodes for leaf children, since there is + no need - they are length 1 and have no children, so no custom. Our tree saves an + order of magnitude of space and allocation costs this way. + */ +export default EmberObject.extend(EmberArray, { init() { - super.init(...arguments); + this._super(...arguments); - // Whenever the root node's length changes we need to propogate the change to + // Whenever the root node's length changes we need to propagate the change to // users of the tree, and since the tree is meant to work like an array we should // trigger a change on the `[]` key as well. addObserver(this, 'root.length', () => notifyPropertyChange(this, '[]')); - } + }, destroy() { if (this._root) { this._root.destroy(); } - super.destroy(...arguments); - } + this._super(...arguments); + }, /* The root node of the tree. Either wraps a true root, or a fake one created if the root is an array. */ - @computed('rows') - get root() { + root: computed('rows', function() { if (this._root) { this._root.destroy(); } @@ -697,39 +788,29 @@ export default class CollapseTree extends EmberObject.extend(EmberArray) { this._root = CollapseTreeNode.create({ value: { children: rows }, tree: this }); return this._root; - } + }), /** - @param {number} index - the index to find - @return {{ value: object, parents: Array }} - */ + @param {number} index - the index to find + @return {{ value: object, parents: Array }} + */ objectAt(index) { - let length = get(this, 'length'); - if (index >= length || index < 0) { + if (index >= get(this, 'length') || index < 0) { return undefined; } - let root = get(this, 'root'); - let rowMetaCache = this.get('rowMetaCache'); - // We add a "fake" top level node to account for the root node let normalizedIndex = index + 1; - let result = root.objectAt(normalizedIndex); - let meta = rowMetaCache.get(result); - - // Set the last known index on the meta and link the next siblings meta - // so that its index can recompute in case it conflicts from shifting - set(meta, '_lastKnownIndex', index); + let result = get(this, 'root').objectAt(normalizedIndex); + let meta = this.get('rowMetaCache').get(result); - if (index < length - 1) { - let nextSibling = root.objectAt(normalizedIndex + 1); - let nextMeta = rowMetaCache.get(nextSibling); - set(nextMeta, '_prevSiblingMeta', meta); - } + // Set the perceived index on the meta. It should be safe to do this here, since + // the row will always be retrieved via `objectAt` before being used. + set(meta, 'index', index); return result; - } + }, forEach(fn) { let length = get(this, 'length'); @@ -737,16 +818,15 @@ export default class CollapseTree extends EmberObject.extend(EmberArray) { for (let i = 0; i < length; i++) { fn(this.objectAt(i), i); } - } + }, /** - Normalized length of the tree + Normalized length of the tree - @type {number} - */ - @computed('root.length') - get length() { + @type {number} + */ + length: computed('root.length', function() { // Remove the root level node from the length count return get(this, 'root.length') - 1; - } -} + }), +}); diff --git a/addon/-private/column-tree.js b/addon/-private/column-tree.js index 99730ae34..d5bad2b39 100644 --- a/addon/-private/column-tree.js +++ b/addon/-private/column-tree.js @@ -1,16 +1,18 @@ +/* eslint-disable getter-return */ import EmberObject, { get, set } from '@ember/object'; -import { addObserver, removeObserver } from '@ember/object/observers'; +import { addObserver, removeObserver } from './utils/observer'; import { A as emberA } from '@ember/array'; import { DEBUG } from '@glimmer/env'; -import { computed } from '@ember-decorators/object'; -import { readOnly, gt } from '@ember-decorators/object/computed'; +import { computed } from '@ember/object'; +import { gt, readOnly } from '@ember/object/computed'; import { scheduler, Token } from 'ember-raf-scheduler'; import { getOrCreate } from './meta-cache'; import { objectAt, move, splice } from './utils/array'; import { mergeSort } from './utils/sort'; +import { isEmpty } from '@ember/utils'; import { getScale, getOuterClientRect, getInnerClientRect } from './utils/element'; import { MainIndicator, DropIndicator } from './utils/reorder-indicators'; import { notifyPropertyChange } from './utils/ember'; @@ -29,6 +31,7 @@ export const FILL_MODE = { EQUAL_COLUMN: 'equal-column', FIRST_COLUMN: 'first-column', LAST_COLUMN: 'last-column', + NTH_COLUMN: 'nth-column', }; export const WIDTH_CONSTRAINT = { @@ -59,58 +62,42 @@ function divideRounded(x, n) { return result; } -class TableColumnMeta extends EmberObject { +const TableColumnMeta = EmberObject.extend({ // If no width is set on the column itself, we cache a temporary width on the // meta object. This is set to the default width. - _width = null; + _width: null, - @readOnly('_node.align') - align; + align: readOnly('_node.align'), - @readOnly('_node.isLeaf') - isLeaf; + isLeaf: readOnly('_node.isLeaf'), - @readOnly('_node.isFixed') - isFixed; + isFixed: readOnly('_node.isFixed'), - @readOnly('_node.isSortable') - isSortable; + isSortable: readOnly('_node.isSortable'), - @readOnly('_node.isResizable') - isResizable; + isResizable: readOnly('_node.isResizable'), - @readOnly('_node.isReorderable') - isReorderable; + isReorderable: readOnly('_node.isReorderable'), - @readOnly('_node.width') - width; + width: readOnly('_node.width'), - @readOnly('_node.minWidth') - minWidth; + minWidth: readOnly('_node.minWidth'), - @readOnly('_node.maxWidth') - maxWidth; + maxWidth: readOnly('_node.maxWidth'), - @readOnly('_node.offsetLeft') - offsetLeft; + offsetLeft: readOnly('_node.offsetLeft'), - @readOnly('_node.offsetRight') - offsetRight; + offsetRight: readOnly('_node.offsetRight'), - @readOnly('_node.isLastFixedLeft') - isLastFixedLeft; + isLastFixedLeft: readOnly('_node.isLastFixedLeft'), - @readOnly('_node.isLastFixedRight') - isLastFixedRight; + isLastFixedRight: readOnly('_node.isLastFixedRight'), - @readOnly('_node.isFirstFixedLeft') - isFirstFixedLeft; + isFirstFixedLeft: readOnly('_node.isFirstFixedLeft'), - @readOnly('_node.isFirstFixedRight') - isFirstFixedRight; + isFirstFixedRight: readOnly('_node.isFirstFixedRight'), - @computed('isLeaf', '_node.{depth,tree.root.maxChildDepth}') - get rowSpan() { + rowSpan: computed('isLeaf', '_node.{depth,tree.root.maxChildDepth}', function() { if (!this.get('isLeaf')) { return 1; } @@ -119,26 +106,23 @@ class TableColumnMeta extends EmberObject { let depth = this.get('_node.depth'); return maxDepth - (depth - 1); - } + }), - @computed('isLeaf', '_node.leaves.length') - get columnSpan() { + columnSpan: computed('isLeaf', '_node.leaves.length', function() { if (this.get('isLeaf')) { return 1; } return this.get('_node.leaves.length'); - } + }), - @computed('isLeaf', '_node.offsetIndex') - get index() { + index: computed('isLeaf', '_node.offsetIndex', function() { if (this.get('isLeaf')) { return this.get('_node.offsetIndex'); } - } + }), - @computed('_node.{tree.sorts.[],column.valuePath}') - get sortIndex() { + sortIndex: computed('_node.{tree.sorts.[],column.valuePath}', function() { let valuePath = this.get('_node.column.valuePath'); let sorts = this.get('_node.tree.sorts'); @@ -154,31 +138,28 @@ class TableColumnMeta extends EmberObject { } return sortIndex; - } + }), - @gt('sortIndex', 0) - isSorted; + isSorted: gt('sortIndex', 0), - @gt('_node.tree.sorts.length', 1) - isMultiSorted; + isMultiSorted: gt('_node.tree.sorts.length', 1), - @computed('_node.tree.sorts.[]', 'sortIndex') - get isSortedAsc() { + isSortedAsc: computed('_node.tree.sorts.[]', 'sortIndex', function() { let sortIndex = this.get('sortIndex'); let sorts = this.get('_node.tree.sorts'); return get(objectAt(sorts, sortIndex - 1), 'isAscending'); - } -} + }), +}); /** Single node of a ColumnTree */ -class ColumnTreeNode extends EmberObject { - _subcolumnNodes = null; +const ColumnTreeNode = EmberObject.extend({ + _subcolumnNodes: null, init() { - super.init(...arguments); + this._super(...arguments); let tree = get(this, 'tree'); let parent = get(this, 'parent'); @@ -201,22 +182,21 @@ class ColumnTreeNode extends EmberObject { meta.endReorder = (...args) => tree.endReorder(this, ...args); // Changes to the value directly should properly update all computeds on this - // node, but we need to manually propogate changes upwards to notify any other + // node, but we need to manually propagate changes upwards to notify any other // watchers this._notifyMaxChildDepth = () => notifyPropertyChange(parent, 'maxChildDepth'); this._notifyLeaves = () => notifyPropertyChange(parent, 'leaves'); - this._notifyCollection = () => notifyPropertyChange(parent, '[]'); addObserver(this, 'maxChildDepth', this._notifyMaxChildDepth); addObserver(this, 'leaves.[]', this._notifyLeaves); } - } + }, destroy() { this.cleanSubcolumnNodes(); - super.destroy(...arguments); - } + this._super(...arguments); + }, /** Fully destroys the child nodes in the event that they change or that this @@ -228,10 +208,9 @@ class ColumnTreeNode extends EmberObject { this._subcolumnNodes.forEach(n => n.destroy()); this._subcolumnNodes = null; } - } + }, - @computed('column.subcolumns.[]') - get subcolumnNodes() { + subcolumnNodes: computed('column.subcolumns.[]', function() { this.cleanSubcolumnNodes(); if (get(this, 'isLeaf')) { @@ -246,24 +225,24 @@ class ColumnTreeNode extends EmberObject { ); return this._subcolumnNodes; - } + }), - @computed('column.align') - get align() { + align: computed('column.align', function() { let align = get(this, 'column.align'); return align; - } + }), - @computed('column.subcolumns.[]') - get isLeaf() { + isLeaf: computed('column.subcolumns.[]', 'isRoot', function() { let subcolumns = get(this, 'column.subcolumns'); + if (get(this, 'isRoot')) { + return false; + } return !subcolumns || get(subcolumns, 'length') === 0; - } + }), - @computed('column.isSortable', 'tree.enableSort') - get isSortable() { + isSortable: computed('column.isSortable', 'tree.enableSort', function() { let enableSort = get(this, 'tree.enableSort'); let valuePath = get(this, 'column.valuePath'); let isSortable = get(this, 'column.isSortable'); @@ -275,18 +254,16 @@ class ColumnTreeNode extends EmberObject { isSortable !== false && typeof valuePath === 'string' ); - } + }), - @computed('column.isReorderable', 'tree.enableReorder') - get isReorderable() { + isReorderable: computed('column.isReorderable', 'tree.enableReorder', function() { let enableReorder = get(this, 'tree.enableReorder'); let isReorderable = get(this, 'column.isReorderable'); return enableReorder !== false && isReorderable !== false; - } + }), - @computed('column.isResizable', 'tree.enableResize') - get isResizable() { + isResizable: computed('column.isResizable', 'tree.enableResize', function() { let isLeaf = get(this, 'isLeaf'); if (isLeaf) { @@ -299,73 +276,65 @@ class ColumnTreeNode extends EmberObject { return subcolumns.some(s => get(s, 'isResizable')); } - } + }), - @computed('parent.{isFixed,isRoot}', 'column.isFixed') - get isFixed() { + isFixed: computed('parent.{isFixed,isRoot}', 'column.isFixed', function() { if (get(this, 'parent.isRoot')) { return get(this, 'column.isFixed'); } return get(this, 'parent.isFixed'); - } + }), - @computed('isFixed', 'tree.leftFixedNodes') - get isLastFixedLeft() { + isLastFixedLeft: computed('isFixed', 'tree.leftFixedNodes', function() { if (!get(this, 'isFixed')) { return false; } let [last] = get(this, 'tree.leftFixedNodes').slice(-1); return last === this; - } + }), - @computed('isFixed', 'tree.leftFixedNodes') - get isFirstFixedLeft() { + isFirstFixedLeft: computed('isFixed', 'tree.leftFixedNodes', function() { if (!get(this, 'isFixed')) { return false; } let first = get(this, 'tree.leftFixedNodes')[0]; return first === this; - } + }), - @computed('isFixed', 'tree.rightFixedNodes') - get isLastFixedRight() { + isLastFixedRight: computed('isFixed', 'tree.rightFixedNodes', function() { if (!get(this, 'isFixed')) { return false; } let [last] = get(this, 'tree.rightFixedNodes').slice(-1); return last === this; - } + }), - @computed('isFixed', 'tree.rightFixedNodes') - get isFirstFixedRight() { + isFirstFixedRight: computed('isFixed', 'tree.rightFixedNodes', function() { if (!get(this, 'isFixed')) { return false; } let first = get(this, 'tree.rightFixedNodes')[0]; return first === this; - } + }), - @computed('parent.depth') - get depth() { + depth: computed('parent.depth', function() { if (get(this, 'parent')) { return get(this, 'parent.depth') + 1; } return 0; - } + }), - @computed('isLeaf', 'subcolumns.@each.depth') - get maxChildDepth() { + maxChildDepth: computed('isLeaf', 'subcolumns.@each.depth', function() { if (get(this, 'isLeaf')) { return get(this, 'depth'); } return Math.max(...get(this, 'subcolumnNodes').map(s => get(s, 'maxChildDepth'))); - } + }), - @computed('isLeaf', 'subcolumnNodes.{[],@each.leaves}') - get leaves() { + leaves: computed('isLeaf', 'subcolumnNodes.{[],@each.leaves}', function() { if (get(this, 'isLeaf')) { return [this]; } @@ -375,10 +344,9 @@ class ColumnTreeNode extends EmberObject { return leaves; }, emberA()); - } + }), - @computed('column.minWidth') - get minWidth() { + minWidth: computed('column.minWidth', function() { if (get(this, 'isLeaf')) { let columnMinWidth = get(this, 'column.minWidth'); @@ -390,10 +358,9 @@ class ColumnTreeNode extends EmberObject { return sum + subcolumnMinWidth; }, 0); - } + }), - @computed('column.maxWidth') - get maxWidth() { + maxWidth: computed('column.minWidth', function() { if (get(this, 'isLeaf')) { let columnMaxWidth = get(this, 'column.maxWidth'); @@ -405,120 +372,122 @@ class ColumnTreeNode extends EmberObject { return sum + subcolumnMaxWidth; }, 0); - } + }), + + width: computed('isLeaf', 'subcolumnNodes.@each.width', 'column.width', { + get() { + if (get(this, 'isLeaf')) { + let column = get(this, 'column'); + let columnWidth = get(column, 'width'); + + if (typeof columnWidth === 'number') { + return columnWidth; + } else { + let meta = get(this, 'tree.columnMetaCache').get(column); + let metaWidth = get(meta, '_width'); + return typeof metaWidth === 'number' ? metaWidth : null; + } + } - @computed('isLeaf', 'subcolumnNodes.@each.width', 'column.width') - get width() { - if (get(this, 'isLeaf')) { - let column = get(this, 'column'); - let columnWidth = get(column, 'width'); + return get(this, 'subcolumnNodes').reduce((sum, subcolumn) => { + let subcolumnWidth = get(subcolumn, 'width'); - if (typeof columnWidth === 'number') { - return columnWidth; - } else { - let meta = get(this, 'tree.columnMetaCache').get(column); - let metaWidth = get(meta, '_width'); - return typeof metaWidth === 'number' ? metaWidth : null; + return sum + subcolumnWidth; + }, 0); + }, + + set(key, newWidth) { + let oldWidth = get(this, 'width'); + let isResizable = get(this, 'isResizable'); + + if (!isResizable) { + return oldWidth; } - } - return get(this, 'subcolumnNodes').reduce((sum, subcolumn) => { - let subcolumnWidth = get(subcolumn, 'width'); + let delta = newWidth - oldWidth; - return sum + subcolumnWidth; - }, 0); - } + let minWidth = get(this, 'minWidth'); + let maxWidth = get(this, 'maxWidth'); - set width(newWidth) { - let oldWidth = get(this, 'width'); - let isResizable = get(this, 'isResizable'); + delta = Math.max(Math.min(oldWidth + delta, maxWidth), minWidth) - oldWidth; - if (!isResizable) { - return oldWidth; - } + if (delta === 0) { + return oldWidth; + } - let delta = newWidth - oldWidth; + if (get(this, 'isLeaf')) { + let column = get(this, 'column'); + let columnWidth = get(column, 'width'); + let width = oldWidth + delta; - let minWidth = get(this, 'minWidth'); - let maxWidth = get(this, 'maxWidth'); + if (typeof columnWidth === 'number') { + set(column, 'width', width); + } else { + let meta = get(this, 'tree.columnMetaCache').get(column); + set(meta, '_width', width); + } + return width; + } else { + let subcolumns = get(this, 'subcolumnNodes'); + + // Delta can only be rendered at a pixel level of precision in tables in + // some browsers, so we round and distribute the remainder as well. We also + // don't know when we may hit a constraint (e.g. minWidth) so we have to do + // this repeatedly. We take the largest chunk we can and try to fit it into + // each piece in a loop. + + // We distribute chunks to the columns starting from the column with the + // smallest width to the column with the largest width. + let sortedSubcolumns = subcolumns + .sortBy('width') + .filter(n => get(n, 'isResizable')) + .reverse(); + + let loopCount = 0; + let prevDelta = 0; + delta = delta > 0 ? Math.floor(delta) : Math.ceil(delta); + while (delta !== 0) { + let deltaChunks = divideRounded(delta, sortedSubcolumns.length); + for (let i = 0; i < deltaChunks.length; i++) { + let subcolumn = sortedSubcolumns[i]; + let deltaChunk = deltaChunks[i]; + let oldWidth = get(subcolumn, 'width'); + let targetWidth = oldWidth + deltaChunk; - delta = Math.max(Math.min(oldWidth + delta, maxWidth), minWidth) - oldWidth; + set(subcolumn, 'width', targetWidth); - if (delta === 0) { - return; - } + let newWidth = get(subcolumn, 'width'); - if (get(this, 'isLeaf')) { - let column = get(this, 'column'); - let columnWidth = get(column, 'width'); + // subtract the amount that changed, if any + delta -= newWidth - oldWidth; - if (typeof columnWidth === 'number') { - return set(column, 'width', oldWidth + delta); - } else { - let meta = get(this, 'tree.columnMetaCache').get(column); - return set(meta, '_width', oldWidth + delta); - } - } else { - let subcolumns = get(this, 'subcolumnNodes'); + if (delta === 0) { + break; + } + } + delta = delta > 0 ? Math.floor(delta) : Math.ceil(delta); - // Delta can only be rendered at a pixel level of precision in tables in - // some browsers, so we round and distribute the remainder as well. We also - // don't know when we may hit a constraint (e.g. minWidth) so we have to do - // this repeatedly. We take the largest chunk we can and try to fit it into - // each piece in a loop. - - // We distribute chunks to the columns starting from the column with the - // smallest width to the column with the largest width. - let sortedSubcolumns = subcolumns - .sortBy('width') - .filter(n => get(n, 'isResizable')) - .reverse(); - - let loopCount = 0; - let prevDelta = 0; - delta = delta > 0 ? Math.floor(delta) : Math.ceil(delta); - while (delta !== 0) { - let deltaChunks = divideRounded(delta, sortedSubcolumns.length); - for (let i = 0; i < deltaChunks.length; i++) { - let subcolumn = sortedSubcolumns[i]; - let deltaChunk = deltaChunks[i]; - let oldWidth = get(subcolumn, 'width'); - let targetWidth = oldWidth + deltaChunk; - - set(subcolumn, 'width', targetWidth); - - let newWidth = get(subcolumn, 'width'); - - // subtract the amount that changed, if any - delta -= newWidth - oldWidth; - - if (delta === 0) { + // If we weren't able to change the delta at all, then we hit a hard + // barrier. This can happen when a table has too many columns to size + // down, for instance. + if (prevDelta === delta) { break; } - } - delta = delta > 0 ? Math.floor(delta) : Math.ceil(delta); - // If we weren't able to change the delta at all, then we hit a hard - // barrier. This can happen when a table has too many columns to size - // down, for instance. - if (prevDelta === delta) { - break; - } + prevDelta = delta; - prevDelta = delta; - - loopCount++; - if (loopCount > LOOP_COUNT_GUARD) { - throw new Error('loop count exceeded guard while distributing width'); + loopCount++; + if (loopCount > LOOP_COUNT_GUARD) { + throw new Error('loop count exceeded guard while distributing width'); + } } - } - return get(this, 'width'); - } - } + return get(this, 'width'); + } + }, + }), - @computed('parent.{offsetIndex,subcolumnNodes.[]}') - get offsetIndex() { + offsetIndex: computed('parent.{offsetIndex,subcolumnNodes.[]}', function() { let parent = get(this, 'parent'); if (!parent) { @@ -537,10 +506,9 @@ class ColumnTreeNode extends EmberObject { } return offsetIndex; - } + }), - @computed('parent.{offsetLeft,width}') - get offsetLeft() { + offsetLeft: computed('parent.{offsetLeft,width}', function() { let parent = get(this, 'parent'); if (!parent) { @@ -559,10 +527,9 @@ class ColumnTreeNode extends EmberObject { } return offsetLeft; - } + }), - @computed('parent.{offsetRight,width}') - get offsetRight() { + offsetRight: computed('parent.{offsetRight,width}', function() { let parent = get(this, 'parent'); if (!parent) { @@ -583,42 +550,43 @@ class ColumnTreeNode extends EmberObject { } return offsetRight; - } + }), registerElement(element) { this.element = element; - } -} + }, +}); -export default class ColumnTree extends EmberObject { +export default EmberObject.extend({ init() { - super.init(...arguments); + this._super(...arguments); this.token = new Token(); - addObserver(this, 'columns.@each.isFixed', this.sortColumnsByFixed); - addObserver(this, 'widthConstraint', this.ensureWidthConstraint); - } + this._sortColumnsByFixed = this.sortColumnsByFixed.bind(this); + this._ensureWidthConstraint = this.ensureWidthConstraint.bind(this); + + addObserver(this, 'columns.@each.isFixed', this._sortColumnsByFixed); + addObserver(this, 'widthConstraint', this._ensureWidthConstraint); + }, destroy() { this.token.cancel(); get(this, 'root').destroy(); - removeObserver(this, 'columns.@each.isFixed', this.sortColumnsByFixed); - removeObserver(this, 'widthConstraint', this.ensureWidthConstraint); + removeObserver(this, 'columns.@each.isFixed', this._sortColumnsByFixed); + removeObserver(this, 'widthConstraint', this._ensureWidthConstraint); - super.destroy(...arguments); - } + this._super(...arguments); + }, - @computed('columns') - get root() { + root: computed('columns', function() { let columns = get(this, 'columns'); return ColumnTreeNode.create({ column: { subcolumns: columns }, tree: this }); - } + }), - @computed('root.{maxChildDepth,leaves.[]}') - get rows() { + rows: computed('root.{maxChildDepth,leaves.[]}', function() { let rows = emberA(); let root = get(this, 'root'); let maxDepth = get(root, 'maxChildDepth'); @@ -642,39 +610,34 @@ export default class ColumnTree extends EmberObject { } return rows; - } + }), - @computed('root.leaves.[]') - get leaves() { + leaves: computed('root.leaves.[]', function() { return emberA(get(this, 'root.leaves').map(n => n.column)); - } + }), - @computed('root.subcolumnNodes.@each.isFixed') - get leftFixedNodes() { + leftFixedNodes: computed('root.subcolumnNodes.@each.isFixed', function() { return get(this, 'root.subcolumnNodes').filterBy('isFixed', 'left'); - } + }), - @computed('root.subcolumnNodes.@each.isFixed') - get rightFixedNodes() { + rightFixedNodes: computed('root.subcolumnNodes.@each.isFixed', function() { return get(this, 'root.subcolumnNodes').filterBy('isFixed', 'right'); - } + }), - @computed('root.subcolumnNodes.@each.isFixed') - get unfixedNodes() { + unfixedNodes: computed('root.subcolumnNodes.@each.isFixed', function() { return get(this, 'root.subcolumnNodes').filter(s => !get(s, 'isFixed')); - } + }), - @computed('leftFixedNodes.@each.width', 'rightFixedNodes.@each.width') - get scrollBounds() { + scrollBounds: computed('leftFixedNodes.@each.width', 'rightFixedNodes.@each.width', function() { let { left: containerLeft, right: containerRight } = getInnerClientRect(this.container); containerLeft += get(this, 'leftFixedNodes').reduce((sum, n) => sum + get(n, 'width'), 0); containerRight -= get(this, 'rightFixedNodes').reduce((sum, n) => sum + get(n, 'width'), 0); return { containerLeft, containerRight }; - } + }), - sortColumnsByFixed = () => { + sortColumnsByFixed() { // disable observer if (this._isSorting) { return; @@ -708,9 +671,9 @@ export default class ColumnTree extends EmberObject { } this._isSorting = false; - }; + }, - ensureWidthConstraint = () => { + ensureWidthConstraint() { if (!this.container) { return; } @@ -723,6 +686,7 @@ export default class ColumnTree extends EmberObject { let widthConstraint = get(this, 'widthConstraint'); let fillMode = get(this, 'fillMode'); + let fillColumnIndex = get(this, 'fillColumnIndex'); if ( (widthConstraint === WIDTH_CONSTRAINT.EQ_CONTAINER && treeWidth !== containerWidth) || @@ -734,16 +698,31 @@ export default class ColumnTree extends EmberObject { if (fillMode === FILL_MODE.EQUAL_COLUMN) { set(this, 'root.width', containerWidth); } else if (fillMode === FILL_MODE.FIRST_COLUMN) { - let oldWidth = get(columns, 'firstObject.width'); - - set(columns, 'firstObject.width', oldWidth + delta); + this.resizeColumn(0, delta); } else if (fillMode === FILL_MODE.LAST_COLUMN) { - let oldWidth = get(columns, 'lastObject.width'); - - set(columns, 'lastObject.width', oldWidth + delta); + this.resizeColumn(columns.length - 1, delta); + } else if (fillMode === FILL_MODE.NTH_COLUMN) { + assert( + "fillMode 'nth-column' must have a fillColumnIndex defined", + !isEmpty(fillColumnIndex) + ); + this.resizeColumn(fillColumnIndex, delta); } } - }; + }, + + resizeColumn(index, delta) { + let columns = get(this, 'root.subcolumnNodes'); + + let fillColumn = columns[index]; + assert( + `Invalid column index, ${index}, for a table with ${columns.length} columns`, + fillColumn + ); + + let oldWidth = get(fillColumn, 'width'); + set(fillColumn, 'width', oldWidth + delta); + }, getReorderBounds(node) { let parent = get(node, 'parent'); @@ -794,7 +773,7 @@ export default class ColumnTree extends EmberObject { rightBound = (right - containerLeft) * scale + scrollLeft; return { leftBound, rightBound }; - } + }, registerContainer(container) { this.container = container; @@ -802,8 +781,8 @@ export default class ColumnTree extends EmberObject { get(this, 'root').registerElement(container); - scheduler.schedule('sync', this.ensureWidthConstraint, this.token); - } + scheduler.schedule('sync', this.ensureWidthConstraint.bind(this), this.token); + }, getClosestColumn(column, left, isFixed) { // If the column is fixed, adjust finder method and offset by the scroll @@ -829,7 +808,7 @@ export default class ColumnTree extends EmberObject { } return subcolumns[subcolumns.length - 1]; - } + }, getClosestColumnOffset(column, left, isFixed) { let closestColumn = this.getClosestColumn(column, left, isFixed); @@ -846,7 +825,7 @@ export default class ColumnTree extends EmberObject { } return offsetLeft; - } + }, insertAfterColumn(parent, after, insert) { if (insert === after) { @@ -862,8 +841,8 @@ export default class ColumnTree extends EmberObject { move(subcolumns, insertIndex, afterIndex); - notifyPropertyChange(parent, 'column.subcolumns.[]'); - } + notifyPropertyChange(subcolumns, '[]'); + }, startReorder(node, clientX) { this.clientX = clientX; @@ -874,7 +853,7 @@ export default class ColumnTree extends EmberObject { this._reorderDropIndicator = new DropIndicator(this.container, node.element, bounds); this.container.classList.add('is-reordering'); - } + }, updateReorder(node, clientX) { this.clientX = clientX; @@ -884,7 +863,7 @@ export default class ColumnTree extends EmberObject { if (!get(node, 'isFixed')) { this.updateScroll(node, true, true, this._updateReorder.bind(this)); } - } + }, _updateReorder(node) { let { scrollLeft } = this.container; @@ -906,7 +885,7 @@ export default class ColumnTree extends EmberObject { this.getClosestColumn(node, this._reorderDropIndicator.left, get(node, 'isFixed')), 'element.offsetWidth' ); - } + }, endReorder(node) { let { scrollLeft } = this.container; @@ -936,11 +915,11 @@ export default class ColumnTree extends EmberObject { this.container.classList.remove('is-reordering'); this.sendAction('onReorder', get(node, 'column'), get(closestColumn, 'column')); - } + }, startResize(node, clientX) { this.clientX = clientX; - } + }, updateResize(node, clientX) { let delta = Math.floor( @@ -959,7 +938,7 @@ export default class ColumnTree extends EmberObject { this.container.classList.add('is-resizing'); this._updateResize(node, delta); - } + }, _updateResize(node, delta) { let resizeMode = get(this, 'resizeMode'); @@ -999,8 +978,8 @@ export default class ColumnTree extends EmberObject { set(node, 'width', newWidth); - this.ensureWidthConstraint(); - } + this.ensureWidthConstraint.call(this); + }, endResize(node) { if (this._nextUpdateScroll) { @@ -1011,7 +990,7 @@ export default class ColumnTree extends EmberObject { this.container.classList.remove('is-resizing'); this.sendAction('onResize', get(node, 'column')); - } + }, updateScroll(node, stopAtLeft, stopAtRight, callback) { if (this._nextUpdateScroll) { @@ -1058,5 +1037,5 @@ export default class ColumnTree extends EmberObject { }, this.token ); - } -} + }, +}); diff --git a/addon/-private/sticky/table-sticky-polyfill.js b/addon/-private/sticky/table-sticky-polyfill.js index 3d69513f3..9416f6752 100644 --- a/addon/-private/sticky/table-sticky-polyfill.js +++ b/addon/-private/sticky/table-sticky-polyfill.js @@ -1,10 +1,12 @@ /* global ResizeSensor */ +/* eslint-disable ember/no-observers */ const TABLE_POLYFILL_MAP = new WeakMap(); class TableStickyPolyfill { constructor(element) { this.element = element; + this.maxStickyProportion = 0.5; this.element.style.position = 'static'; this.side = element.tagName === 'THEAD' ? 'top' : 'bottom'; @@ -69,19 +71,123 @@ class TableStickyPolyfill { this.resizeSensors.forEach(([cell, sensor]) => sensor.detach(cell)); }; + /** + Repositions all the `td`|`th` inside each `tr` of the `tfoot`|`thead`. + The `td` and `th` cells must be sticky due to existing Chrome and Edge bugs + that don't apply the sticky to the footer/header: + * Chrome bug: https://bugs.chromium.org/p/chromium/issues/detail?id=702927 + * Edge bug: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/16765952/ + * More details at: https://caniuse.com/#search=fixed + + Calculates the table's scale and scrollable height and, working top-down for header or bottom-up for footer, + sets the cells for each row to be `position:sticky` with a calculated `top` or `bottom` offset so that they + appear correctly fixed in the table. + The calculation takes into account the height of each row as it goes, adjusting the next row's top|bottom offset + accordingly. + + For example, assuming the following table with 2 thead and 2 tfoot rows: + * There will be 2 TableStickyPolyfills created, one for the thead, one for the tfoot + * For the thead TableStickyPolifyill, its `repositionStickyElements` will + start at row 0, setting each of its `th` cells' `top` value to `0px`, then + add row 0's height (25px) to its current offset and move on to row 1, + where it will set each of that row's `th` cells' `top` to the current + offset of `25px`. + * For the tfoot TableStickyPolyfill, its `respositionStickyElements` will + start at the bottom-most row, row 1, and set each of its `td` cells' + `bottom` value to `0px`, then add row 1's height (20px) to its current + offset and move on to the next row, row 0, where it will set each of that + row's `td` cells' `bottom` to the current offset of `20px`. + + +--------------------------------------------+ + |+------------------------------------------+| + ||thead || + ||+----------------------------------------+|| + |||row 0 (height: 25px) top: 0px ||| + ||+----------------------------------------+|| + ||+----------------------------------------+|| + |||row 1 top: 25px ||| + ||+----------------------------------------+|| + |+------------------------------------------+| + | | + | .... tbody .... | + | | + |+------------------------------------------+| + ||tfoot || + ||+----------------------------------------+|| + |||row 0 bottom: 20px ||| + ||+----------------------------------------+|| + ||+----------------------------------------+|| + |||row 1 (height: 20px) bottom: 0px ||| + ||+----------------------------------------+|| + |+------------------------------------------+| + +--------------------------------------------+ + + If a table has enough header|footer rows, they cumulatively add up to greater than the + table's height. In this case, the standard calculation of `stick`ing each + row to its calculated offset from the top|bottom will cause the + header|footer rows to stick over *all* of the scrollable body rows, + preventing them (as well as possibly some header|footer rows) from being + seen. + + To account for this potential situation, the repositioning sets a maximum percentage height + for the header|footer of 50%. If the rows take up greater than that percentage of the table's + height, all of the overflowing rows are positioned using a negative offset so that they + will be visible when scrolling to the top|bottom of the table for thead|tfoot overflow rows, respectively. + + For example, the following table has footer rows totaling 75px, but the table's height + is only 120px. The footer rows take up more than 50% of the table, so the bottom-most + footer row (2) is positioned at (tableHeight - footerHeight = 75 - 120) -45px, the next + footer row (1) is positioned at (-45 + 30) -15px, and the top-most footer row (0) + is at (-15 + 20) 5px. + + The effect is that the top row (0) will be fully visible, row 1 will be partially visible, + and row 2 will be hidden until the table is scrolled all the way to the bottom. + + +-----------------------------------+ ------------^--- + |table | |Table height: 120px + | | | + | | | + | | | + | | | + | | | + |+--------------------------------+ | ^--- | + ||tfoot | | | | + ||+------------------------------+| | |tfoot height | + |||row 0 (20px) bottom: 5px|| | |20+25+30 = 75px | + ||| || | | | + ||| || | | | + ||+------------------------------+| | | | + ||+------------------------------+| | | | + |||row 1 (25px) bottom: -15px|| | | | + ||| || | | | + +-----------------------------------+ | ------------v--- + |+ +| | + |+------------------------------+| | + ||row 2 (30px) bottom: -45px|| | + || || | + |+------------------------------+| | + | | | + +--------------------------------+ v--- + */ repositionStickyElements = () => { let table = this.element.parentNode; let scale = table.offsetHeight / table.getBoundingClientRect().height; + let containerHeight = table.parentNode.offsetHeight; let rows = Array.from(this.element.children); - let orderedRows = this.side === 'top' ? rows : rows.reverse(); - let offset = 0; + let heights = rows.map(r => r.getBoundingClientRect().height * scale); - let heights = orderedRows.map(r => r.getBoundingClientRect().height * scale); + let totalHeight = heights.reduce((sum, h) => (sum += h), 0); + let maxHeight = containerHeight * this.maxStickyProportion; + if (totalHeight > maxHeight) { + offset = maxHeight - totalHeight; + } - for (let i = 0; i < orderedRows.length; i++) { - let row = orderedRows[i]; + for (let i = 0; i < rows.length; i++) { + // Work top-down (index order) for 'top', bottom-up (reverse index + // order) for 'bottom' rows + let row = rows[this.side === 'top' ? i : rows.length - 1 - i]; let height = heights[i]; for (let child of row.children) { diff --git a/addon/-private/utils/computed.js b/addon/-private/utils/computed.js index 5718f59de..e51549ac4 100644 --- a/addon/-private/utils/computed.js +++ b/addon/-private/utils/computed.js @@ -1,6 +1,6 @@ -import EmberObject, { defineProperty, computed, observer } from '@ember/object'; +import EmberObject, { defineProperty, computed } from '@ember/object'; +import { observer } from './observer'; import { alias } from '@ember/object/computed'; -import { macro } from '@ember-decorators/object/computed'; const PROPERTIES = new WeakMap(); @@ -40,12 +40,13 @@ const ClassBasedComputedProperty = EmberObject.extend({ _dependencies: null, init() { + this._super(...arguments); this._redefineProperty(); }, // eslint-disable-next-line _contentDidChange: observer('_content', function() { - if (!this._isUpdating) { + if (!this._isUpdating && !this._context.isDestroyed && !this._context.isDestroying) { this._context.notifyPropertyChange(this._key); } }), @@ -55,7 +56,9 @@ const ClassBasedComputedProperty = EmberObject.extend({ let isDynamicList = this.get('_isDynamicList'); let computed = this._computedFunction( - ...dependencies.map((d, i) => (isDynamicList[i] ? this.get(d) : d)) + ...dependencies.map((d, i) => + isDynamicList[i] ? this.get(`_context.${d}`) : `_context.${d}` + ) ); defineProperty(this, '_content', computed); @@ -83,8 +86,6 @@ function classComputedProperty(isDynamicList, computedFunction) { }; dependencies.forEach((dep, index) => { - extension[dep] = alias(`_context.${dep}`); - if (isDynamicList[index] === true) { // eslint-disable-next-line extension[`${dep}DidChange`] = observer(`_context.${dep}`, function() { @@ -110,12 +111,10 @@ function classComputedProperty(isDynamicList, computedFunction) { }; } -export const dynamicAlias = macro( - classComputedProperty([false, true], function(...segments) { - if (segments.every(s => typeof s === 'string')) { - return alias(segments.join('.')); - } else { - return null; - } - }) -); +export const dynamicAlias = classComputedProperty([false, true], function(...segments) { + if (segments.every(s => typeof s === 'string')) { + return alias(segments.join('.')); + } else { + return null; + } +}); diff --git a/addon/-private/utils/default-to.js b/addon/-private/utils/default-to.js new file mode 100644 index 000000000..b7ccad6c4 --- /dev/null +++ b/addon/-private/utils/default-to.js @@ -0,0 +1,37 @@ +import { computed } from '@ember/object'; + +let VALUES = new WeakMap(); + +function valuesFor(obj) { + if (!VALUES.has(obj)) { + VALUES.set(obj, Object.create(null)); + } + + return VALUES.get(obj); +} + +export default function defaultTo(defaultValue) { + return computed({ + get(key) { + let values = valuesFor(this); + + if (!(key in values)) { + values[key] = typeof defaultValue === 'function' ? defaultValue() : defaultValue; + } + + return values[key]; + }, + + set(key, value) { + let values = valuesFor(this); + + if (value === undefined) { + values[key] = typeof defaultValue === 'function' ? defaultValue() : defaultValue; + } else { + values[key] = value; + } + + return values[key]; + }, + }); +} diff --git a/addon/-private/utils/observer.js b/addon/-private/utils/observer.js new file mode 100644 index 000000000..42a48b25d --- /dev/null +++ b/addon/-private/utils/observer.js @@ -0,0 +1,64 @@ +import { gte } from 'ember-compatibility-helpers'; +import { assert } from '@ember/debug'; + +// eslint-disable-next-line no-restricted-imports +import { observer as emberObserver } from '@ember/object'; + +// eslint-disable-next-line no-restricted-imports +import { + addObserver as emberAddObserver, + removeObserver as emberRemoveObserver, +} from '@ember/object/observers'; + +const USE_ASYNC_OBSERVERS = gte('3.13.0'); + +function asyncObserver(...args) { + let fn = args.pop(); + let dependentKeys = args; + let sync = false; + + return emberObserver({ dependentKeys, fn, sync }); +} + +function asyncAddObserver(...args) { + let obj, path, target, method; + let sync = false; + obj = args[0]; + path = args[1]; + assert( + `Expected 3 or 4 args for addObserver, got ${args.length}`, + args.length === 3 || args.length === 4 + ); + if (args.length === 3) { + target = null; + method = args[2]; + } else if (args.length === 4) { + target = args[2]; + method = args[3]; + } + + return emberAddObserver(obj, path, target, method, sync); +} + +function asyncRemoveObserver(...args) { + let obj, path, target, method; + let sync = false; + obj = args[0]; + path = args[1]; + assert( + `Expected 3 or 4 args for addObserver, got ${args.length}`, + args.length === 3 || args.length === 4 + ); + if (args.length === 3) { + target = null; + method = args[2]; + } else { + target = args[2]; + method = args[3]; + } + return emberRemoveObserver(obj, path, target, method, sync); +} + +export const observer = USE_ASYNC_OBSERVERS ? asyncObserver : emberObserver; +export const addObserver = USE_ASYNC_OBSERVERS ? asyncAddObserver : emberAddObserver; +export const removeObserver = emberRemoveObserver ? asyncRemoveObserver : emberRemoveObserver; diff --git a/addon/-private/utils/reorder-indicators.js b/addon/-private/utils/reorder-indicators.js index 207b85255..4b83ebaac 100644 --- a/addon/-private/utils/reorder-indicators.js +++ b/addon/-private/utils/reorder-indicators.js @@ -33,6 +33,7 @@ class ReorderIndicator { let left = (elementLeft - containerLeft) * this.scale + scrollLeft; let width = elementWidth * this.scale; + this.originLeft = left; this.indicatorElement = createElement(mainClass, { top, left, width }); if (child) { @@ -66,6 +67,14 @@ class ReorderIndicator { newLeft = rightBound - width; } + if (newLeft < this.originLeft) { + this.indicatorElement.classList.remove('et-reorder-direction-right'); + this.indicatorElement.classList.add('et-reorder-direction-left'); + } else { + this.indicatorElement.classList.remove('et-reorder-direction-left'); + this.indicatorElement.classList.add('et-reorder-direction-right'); + } + this.indicatorElement.style.left = `${newLeft}px`; this._left = newLeft; } diff --git a/addon/-private/utils/sort.js b/addon/-private/utils/sort.js index 26a91c6e4..08a996697 100644 --- a/addon/-private/utils/sort.js +++ b/addon/-private/utils/sort.js @@ -37,7 +37,6 @@ function merge(left, right, comparator) { * are not stable, and `_.sortBy` doesn't take a general comparator. Ideally * lodash would add a `_.sort` function whose API would mimic this function's. * - * @function * @param {Array} array The array to be sorted * @param {Comparator} comparator The comparator function to compare elements with. * @returns {Array} A sorted array @@ -54,14 +53,21 @@ export function mergeSort(array, comparator = compare) { return merge(leftArray, rightArray, comparator); } -export function sortMultiple(itemA, itemB, sorts, compare) { +export function sortMultiple(itemA, itemB, sorts, compare, sortEmptyLast) { let compareValue; for (let { valuePath, isAscending } of sorts) { let valueA = get(itemA, valuePath); let valueB = get(itemB, valuePath); - compareValue = isAscending ? compare(valueA, valueB) : -compare(valueA, valueB); + // The option only influences the outcome of an ascending sort. + if (sortEmptyLast) { + sortEmptyLast = isAscending; + } + + compareValue = isAscending + ? compare(valueA, valueB, sortEmptyLast) + : -compare(valueA, valueB, sortEmptyLast); if (compareValue !== 0) { break; @@ -75,30 +81,41 @@ function isExactlyNaN(value) { return typeof value === 'number' && isNaN(value); } +function isEmptyString(value) { + return typeof value === 'string' && value === ''; +} + function isEmpty(value) { - return isNone(value) || isExactlyNaN(value); + return isNone(value) || isExactlyNaN(value) || isEmptyString(value); } -function orderEmptyValues(itemA, itemB) { +function orderEmptyValues(itemA, itemB, sortEmptyLast) { let aIsEmpty = isEmpty(itemA); let bIsEmpty = isEmpty(itemB); + let less = -1; + let more = 1; + + if (sortEmptyLast) { + less = 1; + more = -1; + } if (aIsEmpty && !bIsEmpty) { - return -1; + return less; } else if (bIsEmpty && !aIsEmpty) { - return 1; + return more; } else if (isNone(itemA) && isExactlyNaN(itemB)) { - return -1; + return less; } else if (isExactlyNaN(itemA) && isNone(itemB)) { - return 1; + return more; } else { return 0; } } -export function compareValues(itemA, itemB) { +export function compareValues(itemA, itemB, sortEmptyLast) { if (isEmpty(itemA) || isEmpty(itemB)) { - return orderEmptyValues(itemA, itemB); + return orderEmptyValues(itemA, itemB, sortEmptyLast); } return compare(itemA, itemB); diff --git a/addon/components/-private/base-table-cell.js b/addon/components/-private/base-table-cell.js index 398665265..b95a63dd6 100644 --- a/addon/components/-private/base-table-cell.js +++ b/addon/components/-private/base-table-cell.js @@ -1,11 +1,13 @@ import Component from '@ember/component'; import { equal, bool, or } from '@ember/object/computed'; -import { observer } from '@ember/object'; +import { observer } from '../../-private/utils/observer'; import { scheduleOnce } from '@ember/runloop'; +import { computed } from '@ember/object'; export default Component.extend({ // Provided by subclasses columnMeta: null, + columnValue: null, classNameBindings: [ 'isFirstColumn', @@ -16,6 +18,7 @@ export default Component.extend({ 'hasWidth', 'hasMinWidth', 'hasMaxWidth', + 'textAlign', ], isFirstFixed: or('columnMeta.isFirstFixedLeft', 'columnMeta.isFirstFixedRight'), @@ -27,6 +30,19 @@ export default Component.extend({ hasMinWidth: bool('columnMeta.minWidth'), hasMaxWidth: bool('columnMeta.maxWidth'), + /** + Indicates the text alignment of this cell + */ + textAlign: computed('columnValue.textAlign', function() { + let textAlign = this.get('columnValue.textAlign'); + + if (['left', 'center', 'right'].includes(textAlign)) { + return `ember-table__text-align-${textAlign}`; + } + + return null; + }), + // eslint-disable-next-line scheduleUpdateStyles: observer( 'columnMeta.{width,offsetLeft,offsetRight}', diff --git a/addon/components/-private/row-wrapper.js b/addon/components/-private/row-wrapper.js index 57dac8ae5..f47b3150f 100644 --- a/addon/components/-private/row-wrapper.js +++ b/addon/components/-private/row-wrapper.js @@ -1,23 +1,18 @@ import Component from '@ember/component'; import hbs from 'htmlbars-inline-precompile'; -import EmberObject, { get, set } from '@ember/object'; +import EmberObject, { get, setProperties, computed } from '@ember/object'; import { A as emberA } from '@ember/array'; -import { tagName } from '@ember-decorators/component'; -import { argument } from '@ember-decorators/argument'; - -import { computed } from '@ember-decorators/object'; - import { objectAt } from '../../-private/utils/array'; import { dynamicAlias } from '../../-private/utils/computed'; -class CellWrapper extends EmberObject { - @dynamicAlias('rowValue', 'columnValue.valuePath') - cellValue; +const CellWrapper = EmberObject.extend({ + cellValue: computed('rowValue', 'columnValue.valuePath', function() { + return get(this.get('rowValue'), this.get('columnValue.valuePath')); + }), - @computed('rowMeta', 'columnValue') - get cellMeta() { + cellMeta: computed('rowMeta', 'columnValue', function() { let rowMeta = get(this, 'rowMeta'); let columnValue = get(this, 'columnValue'); @@ -26,49 +21,36 @@ class CellWrapper extends EmberObject { } return rowMeta._cellMetaCache.get(columnValue); - } -} + }), +}); const layout = hbs`{{yield api}}`; -@tagName('') -export default class RowWrapper extends Component { - @argument - rowValue; - - @argument - columns; - - @argument - columnMetaCache; - - @argument - rowMetaCache; +export default Component.extend({ + layout, + tagName: '', - @argument - canSelect; - - @argument - rowSelectionMode; - - @argument - checkboxSelectionMode; + canSelect: undefined, + checkboxSelectionMode: undefined, + columnMetaCache: undefined, + columns: undefined, + rowMetaCache: undefined, + rowSelectionMode: undefined, + rowValue: undefined, init() { - super.init(...arguments); + this._super(...arguments); - this.layout = layout; this._cells = emberA([]); - } + }, destroy() { this._cells.forEach(cell => cell.destroy()); - super.destroy(...arguments); - } + this._super(...arguments); + }, - @computed('rowValue', 'rowMeta', 'cells', 'canSelect', 'rowSelectionMode') - get api() { + api: computed('rowValue', 'rowMeta', 'cells', 'canSelect', 'rowSelectionMode', function() { let rowValue = this.get('rowValue'); let rowMeta = this.get('rowMeta'); let cells = this.get('cells'); @@ -76,60 +58,60 @@ export default class RowWrapper extends Component { let rowSelectionMode = canSelect ? this.get('rowSelectionMode') : 'none'; return { rowValue, rowMeta, cells, rowSelectionMode }; - } + }), - @computed('rowValue') - get rowMeta() { + rowMeta: computed('rowValue', function() { let rowValue = this.get('rowValue'); let rowMetaCache = this.get('rowMetaCache'); return rowMetaCache.get(rowValue); - } + }), - @computed( + cells: computed( 'rowValue', 'rowMeta', 'columns.[]', 'canSelect', 'checkboxSelectionMode', - 'rowSelectionMode' - ) - get cells() { - let columns = this.get('columns'); - let numColumns = get(columns, 'length'); - - let rowValue = this.get('rowValue'); - let rowMeta = this.get('rowMeta'); - let canSelect = this.get('canSelect'); - let checkboxSelectionMode = canSelect ? this.get('checkboxSelectionMode') : 'none'; - let rowSelectionMode = canSelect ? this.get('rowSelectionMode') : 'none'; - - let { _cells } = this; - - if (numColumns !== _cells.length) { - while (_cells.length < numColumns) { - _cells.pushObject(CellWrapper.create()); + 'rowSelectionMode', + function() { + let columns = this.get('columns'); + let numColumns = get(columns, 'length'); + + let rowValue = this.get('rowValue'); + let rowMeta = this.get('rowMeta'); + let canSelect = this.get('canSelect'); + let checkboxSelectionMode = canSelect ? this.get('checkboxSelectionMode') : 'none'; + let rowSelectionMode = canSelect ? this.get('rowSelectionMode') : 'none'; + + let { _cells } = this; + + if (numColumns !== _cells.length) { + while (_cells.length < numColumns) { + _cells.pushObject(CellWrapper.create()); + } + + while (_cells.length > numColumns) { + _cells.popObject().destroy(); + } } - while (_cells.length > numColumns) { - _cells.popObject().destroy(); - } + _cells.forEach((cell, i) => { + let columnValue = objectAt(columns, i); + let columnMeta = this.get('columnMetaCache').get(columnValue); + + // eslint-disable-next-line ember/no-side-effects, ember-best-practices/no-side-effect-cp + setProperties(cell, { + checkboxSelectionMode, + columnMeta, + columnValue, + rowMeta, + rowSelectionMode, + rowValue, + }); + }); + + return _cells; } - - _cells.forEach((cell, i) => { - let columnValue = objectAt(columns, i); - let columnMeta = this.get('columnMetaCache').get(columnValue); - - set(cell, 'checkboxSelectionMode', checkboxSelectionMode); - set(cell, 'rowSelectionMode', rowSelectionMode); - - set(cell, 'columnValue', columnValue); - set(cell, 'columnMeta', columnMeta); - - set(cell, 'rowValue', rowValue); - set(cell, 'rowMeta', rowMeta); - }); - - return _cells; - } -} + ), +}); diff --git a/addon/components/-private/simple-checkbox.js b/addon/components/-private/simple-checkbox.js index e20fc9beb..337c2b1da 100644 --- a/addon/components/-private/simple-checkbox.js +++ b/addon/components/-private/simple-checkbox.js @@ -1,52 +1,30 @@ import Component from '@ember/component'; -import { tagName, attribute } from '@ember-decorators/component'; -import { argument } from '@ember-decorators/argument'; -import { type, optional } from '@ember-decorators/argument/type'; -import { Action } from '@ember-decorators/argument/types'; - -@tagName('input') -export default class SimpleCheckbox extends Component { - value = null; - - @attribute - type = 'checkbox'; - - @argument({ defaultIfUndefined: true }) - @type('boolean') - @attribute - checked = false; - - @argument({ defaultIfUndefined: true }) - @type('boolean') - @attribute - disabled = false; - - @argument({ defaultIfUndefined: true }) - @type('boolean') - @attribute - indeterminate = false; - - @argument - @type('any') - @attribute - value = null; - - @argument - @type(optional(Action)) - onClick = null; - - @argument - @type(optional(Action)) - onChange = null; - - @argument - @type('string') - @attribute('aria-label') - ariaLabel; +import defaultTo from '../../-private/utils/default-to'; + +export default Component.extend({ + tagName: 'input', + + attributeBindings: [ + 'ariaLabel:aria-label', + 'checked', + 'disabled', + 'indeterminate', + 'type', + 'value', + ], + + ariaLabel: undefined, + checked: defaultTo(false), + disabled: defaultTo(false), + indeterminate: defaultTo(false), + onChange: null, + onClick: null, + type: 'checkbox', + value: null, click(event) { this.sendAction('onClick', event); - } + }, change(event) { let checked = this.element.checked; @@ -59,5 +37,5 @@ export default class SimpleCheckbox extends Component { this.element.indeterminate = this.get('indeterminate'); this.sendAction('onChange', checked, { value, indeterminate }, event); - } -} + }, +}); diff --git a/addon/components/ember-table/component.js b/addon/components/ember-table/component.js index 3a2f4fc18..b1c7a4711 100644 --- a/addon/components/ember-table/component.js +++ b/addon/components/ember-table/component.js @@ -1,9 +1,7 @@ import Component from '@ember/component'; +import { computed } from '@ember/object'; import { htmlSafe } from '@ember/string'; - -import { computed } from '@ember-decorators/object'; -import { attribute, classNames } from '@ember-decorators/component'; -import { service } from '@ember-decorators/service'; +import { inject as service } from '@ember/service'; import { setupLegacyStickyPolyfill, @@ -30,27 +28,22 @@ import layout from './template'; ``` - @yield {object} t - the API object yielded by the table - @yield {Component} t.head - The table header component - @yield {Component} t.body - The table body component - @yield {Component} t.foot - The table footer component + @yield {object} table - the API object yielded by the table + @yield {Component} table.head - The table header component + @yield {Component} table.body - The table body component + @yield {Component} table.foot - The table footer component + @class {{ember-table}} + @public */ -@classNames('ember-table') -export default class EmberTable extends Component { - @service - userAgent; - - @attribute('data-test-ember-table') - dataTestEmberTable = true; - - init() { - super.init(...arguments); +export default Component.extend({ + layout, + classNames: ['ember-table'], + userAgent: service(), - this.layout = layout; - } + 'data-test-ember-table': true, didInsertElement() { - super.didInsertElement(...arguments); + this._super(...arguments); let browser = this.get('userAgent.browser'); @@ -67,7 +60,7 @@ export default class EmberTable extends Component { setupTableStickyPolyfill(tfoot); } } - } + }, willDestroyElement() { let browser = this.get('userAgent.browser'); @@ -87,24 +80,22 @@ export default class EmberTable extends Component { } } - super.willDestroyElement(...arguments); - } + this._super(...arguments); + }, - @computed('tableWidth') - get tableStyle() { + tableStyle: computed('tableWidth', function() { return htmlSafe(`width: ${this.get('tableWidth')}px;`); - } + }), - @computed - get api() { + api: computed(function() { return { columns: null, - registerColumnTree: this.registerColumnTree, + registerColumnTree: this.registerColumnTree.bind(this), tableId: this.elementId, }; - } + }), - registerColumnTree = columnTree => { + registerColumnTree(columnTree) { this.set('api.columnTree', columnTree); - }; -} + }, +}); diff --git a/addon/components/ember-tbody/component.js b/addon/components/ember-tbody/component.js index 14b9520f8..aac062b62 100644 --- a/addon/components/ember-tbody/component.js +++ b/addon/components/ember-tbody/component.js @@ -1,20 +1,17 @@ import Component from '@ember/component'; -import { tagName } from '@ember-decorators/component'; -import { computed } from '@ember-decorators/object'; -import { bool, readOnly, or } from '@ember-decorators/object/computed'; - -import { argument } from '@ember-decorators/argument'; -import { required } from '@ember-decorators/argument/validation'; -import { type, optional } from '@ember-decorators/argument/type'; -import { Action } from '@ember-decorators/argument/types'; +import { run } from '@ember/runloop'; +import { computed } from '@ember/object'; +import { observer } from '../../-private/utils/observer'; +import { bool, readOnly, or } from '@ember/object/computed'; import { SUPPORTS_INVERSE_BLOCK } from 'ember-compatibility-helpers'; import CollapseTree, { SELECT_MODE } from '../../-private/collapse-tree'; +import defaultTo from '../../-private/utils/default-to'; import layout from './template'; -import { assert } from '@ember/debug'; +import { assert, runInDebug } from '@ember/debug'; /** The table body component. This component manages the main bulk of the rows of @@ -32,155 +29,173 @@ import { assert } from '@ember/debug'; ``` - @yield {object} b - the API object yielded by the table body - @yield {Component} b.row - The table row component + @yield {object} body - the API object yielded by the table body + @yield {Component} body.row - The table row component - @yield {object} b.rowValue - The value for the currently yielded row - @yield {object} b.rowMeta - The meta for the currently yielded row + @yield {object} body.rowValue - The value for the currently yielded row + @yield {object} body.rowMeta - The meta for the currently yielded row + @class {{ember-tbody}} + @public */ -@tagName('tbody') -export default class EmberTBody extends Component { +export default Component.extend({ + layout, + tagName: 'tbody', + /** The API object passed in by the table - */ - @argument - @required - @type('object') - api; - @or('api.api', 'api') - unwrappedApi; + @argument api + @required + @type object + */ + api: null, - @readOnly('unwrappedApi.columnTree.leaves') - columns; + unwrappedApi: or('api.api', 'api'), - @readOnly('unwrappedApi.columnTree.columnMetaCache') - columnMetaCache; + columns: readOnly('unwrappedApi.columnTree.leaves'), + columnMetaCache: readOnly('unwrappedApi.columnTree.columnMetaCache'), /** Sets which row selection behavior to follow. Possible values are 'none' (clicking on a row does nothing), 'single' (clicking on a row selects it and deselects other rows), and 'multiple' (multiple rows can be selected through ctrl/cmd-click or shift-click). + + @argument checkboxSelectionMode + @type string? ('multiple') */ - @argument({ defaultIfUndefined: true }) - @type('string') - checkboxSelectionMode = SELECT_MODE.MULTIPLE; + checkboxSelectionMode: defaultTo(SELECT_MODE.MULTIPLE), /** Sets which checkbox selection behavior to follow. Possible values are 'none' (clicking on a row does nothing), 'single' (clicking on a row selects it and deselects other rows), and 'multiple' (multiple rows can be selected through ctrl/cmd-click or shift-click). + + @argument rowSelectionMode + @type string? ('multiple') */ - @argument({ defaultIfUndefined: true }) - @type('string') - rowSelectionMode = SELECT_MODE.MULTIPLE; + rowSelectionMode: defaultTo(SELECT_MODE.MULTIPLE), /** When true, this option causes selecting all of a node's children to also select the node itself. + + @argument selectingChildrenSelectsParent + @type boolean */ - @argument({ defaultIfUndefined: true }) - @type('boolean') - selectingChildrenSelectsParent = true; + selectingChildrenSelectsParent: defaultTo(true), /** - The currently selected rows. Can either be an array or and individual row. + The currently selected rows. Can either be an array or an individual row. + + @argument selection + @type array|object|null */ - @argument({ defaultIfUndefined: true }) - @type(optional('object')) - selection = null; + selection: null, /** - An action that triggers when the row selection of the table changes. + An action that is called when the row selection of the table changes. + Will be called with either an array or individual row, depending on the + checkboxSelectionMode. + @argument onSelect + @type Action? @param {object} selection - The new selection */ - @argument - @type(optional(Action)) - onSelect = null; + onSelect: null, /** Estimated height for each row. This number is used to decide how many rows will be rendered at initial rendering. + + @argument estimateRowHeight + @type number? (30) */ - @argument({ defaultIfUndefined: true }) - @type('number') - estimateRowHeight = 30; + estimateRowHeight: defaultTo(30), /** A flag that controls if all rows have same static height or not. By default it is set to false and row height is dependent on its internal content. If it is set to true, all rows have the same height equivalent to estimateRowHeight. + + @argument staticHeight + @type boolean? (false) */ - @argument({ defaultIfUndefined: true }) - @type('boolean') - staticHeight = false; + staticHeight: defaultTo(false), /** The number of extra rows to render on either side of the table's viewport + + @argument bufferSize + @type number? (1) */ - @argument({ defaultIfUndefined: true }) - @type('number') - bufferSize = 20; + bufferSize: defaultTo(1), /** A flag that tells the table to render all of its rows at once. + + @argument renderAll + @type boolean? (false) */ - @argument({ defaultIfUndefined: true }) - @type('boolean') - renderAll = false; + renderAll: defaultTo(false), /** An action that is triggered when the table reaches the first row. + + @argument firstReached + @type Action? */ - @argument - @type(optional(Action)) - firstReached = null; + firstReached: null, /** An action that is triggered when the table reaches the last row. + + @argument lastReached + @type Action? */ - @argument - @type(optional(Action)) - lastReached = null; + lastReached: null, /** An action that is triggered when the first visible row of the table changes. + + @argument firstVisibleChanged + @type Action? */ - @argument - @type(optional(Action)) - firstVisibleChanged = null; + firstVisibleChanged: null, /** An action that is triggered when the last visible row of the table changes. + + @argument lastVisibleChanged + @type Action? */ - @argument - @type(optional(Action)) - lastVisibleChanged = null; + lastVisibleChanged: null, /** Boolean flag that enables tree behavior if items have a `children` property + + @argument enableTree + @type boolean? (true) */ - @argument({ defaultIfUndefined: true }) - @type('boolean') - enableTree = true; + enableTree: defaultTo(true), /** Boolean flag that enables collapsing tree nodes + + @argument enableCollapse + @type boolean? (true) */ - @argument({ defaultIfUndefined: true }) - @type('boolean') - enableCollapse = true; + enableCollapse: defaultTo(true), /** The row items that the table should display + + @argument rows + @type array? ([]) */ - @argument({ defaultIfUndefined: true }) - @type('object') - rows = []; + rows: defaultTo(() => []), /** This key is the property used by the collection to determine whether an @@ -188,40 +203,44 @@ export default class EmberTBody extends Component { the key that is passed to the actions, and can be used to restore scroll position with `idForFirstItem`. This is passed through to the vertical-collection. + + @argument key + @type string? ('@identity') */ - @argument({ defaultIfUndefined: true }) - @type('string') - key = '@identity'; + key: defaultTo('@identity'), /** The property is passed through to the vertical-collection. If set, upon initialization the scroll position will be set such that the item with the provided id is at the top left on screen. If the item with id cannot be found, scrollTop is set to 0. + + @argument idForFirstItem + @type string? */ - @argument({ defaultIfUndefined: true }) - @type(optional('string')) - idForFirstItem = null; + idForFirstItem: null, /** A selector string that will select the element from which to calculate the viewable height. + + @argument containerSelector + @type string? () */ - @argument({ defaultIfUndefined: true }) - @type('string') - containerSelector = ''; + containerSelector: defaultTo(''), /** Whether or not the table can select, is true if an `onSelect` action was passed to the table. */ - @bool('onSelect') - canSelect; + canSelect: bool('onSelect'), - init() { - super.init(...arguments); + dataTestRowCount: null, - this.layout = layout; + 'data-test-row-count': readOnly('dataTestRowCount'), + + init() { + this._super(...arguments); /** The map that contains row meta information for this table. @@ -239,35 +258,48 @@ export default class EmberTBody extends Component { this._updateCollapseTree(); - this.addObserver('unwrappedApi.sorts', this._updateCollapseTree); - this.addObserver('unwrappedApi.sortFunction', this._updateCollapseTree); - this.addObserver('unwrappedApi.compareFunction', this._updateCollapseTree); - - this.addObserver('enableCollapse', this._updateCollapseTree); - this.addObserver('enableTree', this._updateCollapseTree); - this.addObserver('selection', this._updateCollapseTree); - this.addObserver('selectingChildrenSelectsParent', this._updateCollapseTree); - this.addObserver('onSelect', this._updateCollapseTree); + runInDebug(() => { + let scheduleUpdate = (this._scheduleUpdate = () => { + run.scheduleOnce('actions', this, this._updateDataTestRowCount); + }); + this.collapseTree.addObserver('rows', scheduleUpdate); + this.collapseTree.addObserver('[]', scheduleUpdate); + }); assert( 'You must create an {{ember-thead}} with columns before creating an {{ember-tbody}}', !!this.get('unwrappedApi.columnTree') ); - } - - _updateCollapseTree() { - this.collapseTree.set('sorts', this.get('unwrappedApi.sorts')); - this.collapseTree.set('sortFunction', this.get('unwrappedApi.sortFunction')); - this.collapseTree.set('compareFunction', this.get('unwrappedApi.compareFunction')); - - this.collapseTree.set('enableCollapse', this.get('enableCollapse')); - this.collapseTree.set('enableTree', this.get('enableTree')); - this.collapseTree.set('selection', this.get('selection')); - this.collapseTree.set( - 'selectingChildrenSelectsParent', - this.get('selectingChildrenSelectsParent') - ); - } + }, + + _updateDataTestRowCount() { + this.set('dataTestRowCount', this.get('collapseTree.length')); + }, + + // eslint-disable-next-line + _updateCollapseTree: observer( + 'unwrappedApi.{sorts,sortFunction,compareFunction,sortEmptyLast}', + 'enableCollapse', + 'enableTree', + 'selection', + 'selectingChildrenSelectsParent', + 'onSelect', + + function() { + this.collapseTree.set('sorts', this.get('unwrappedApi.sorts')); + this.collapseTree.set('sortFunction', this.get('unwrappedApi.sortFunction')); + this.collapseTree.set('compareFunction', this.get('unwrappedApi.compareFunction')); + this.collapseTree.set('sortEmptyLast', this.get('unwrappedApi.sortEmptyLast')); + + this.collapseTree.set('enableCollapse', this.get('enableCollapse')); + this.collapseTree.set('enableTree', this.get('enableTree')); + this.collapseTree.set('selection', this.get('selection')); + this.collapseTree.set( + 'selectingChildrenSelectsParent', + this.get('selectingChildrenSelectsParent') + ); + } + ), willDestroy() { for (let [row, meta] of this.rowMetaCache.entries()) { @@ -275,39 +307,39 @@ export default class EmberTBody extends Component { this.rowMetaCache.delete(row); } + runInDebug(() => { + this.collapseTree.removeObserver('rows', this._scheduleUpdate); + this.collapseTree.removeObserver('[]', this._scheduleUpdate); + }); this.collapseTree.destroy(); - } + }, /** Computed property which updates the CollapseTree and erases caches. This is - a computed for 1.11 compatibility, otherwise it would make sense to use + a computed for 1.12 compatibility, otherwise it would make sense to use lifecycle hooks instead. */ - @computed('rows') - get wrappedRows() { + wrappedRows: computed('rows', function() { let rows = this.get('rows'); this.collapseTree.set('rowMetaCache', this.rowMetaCache); this.collapseTree.set('rows', rows); return this.collapseTree; - } + }), /** Computed property which calculates container selector for vertical collection. It can be a custom selector provided directly to {{ember-tbody}}. If not, it will be equal to parent {{ember-table}} `id`. */ - @computed('containerSelector', 'unwrappedApi.tableId') - get _containerSelector() { + _containerSelector: computed('containerSelector', 'unwrappedApi.tableId', function() { return this.get('containerSelector') || `#${this.get('unwrappedApi.tableId')}`; - } + }), /** * Determines if the component can yield-to-inverse based on * the version compatability. */ - get shouldYieldToInverse() { - return SUPPORTS_INVERSE_BLOCK; - } -} + shouldYieldToInverse: SUPPORTS_INVERSE_BLOCK, +}); diff --git a/addon/components/ember-td/component.js b/addon/components/ember-td/component.js index 589f61ab5..2a32c27fa 100644 --- a/addon/components/ember-td/component.js +++ b/addon/components/ember-td/component.js @@ -1,110 +1,94 @@ import BaseTableCell from '../-private/base-table-cell'; -import { action, computed } from '@ember-decorators/object'; -import { alias, readOnly } from '@ember-decorators/object/computed'; -import { tagName } from '@ember-decorators/component'; -import { argument } from '@ember-decorators/argument'; -import { type, optional } from '@ember-decorators/argument/type'; -import { Action } from '@ember-decorators/argument/types'; +import { computed } from '@ember/object'; +import { alias, readOnly } from '@ember/object/computed'; import layout from './template'; import { SELECT_MODE } from '../../-private/collapse-tree'; /** - The table cell component. This component manages cell level concerns, yields - the cell value, column value, row value, and all of their associated meta - objects. - - ```hbs - - - - - - - - - - - - ``` - - @yield {any} cellValue - The value of the cell - @yield {object} columnValue - The column definition - @yield {object} rowValue - The row definition - - @yield {object} cellMeta - The meta object associated with the cell - @yield {object} columnMeta - The meta object associated with the column - @yield {object} rowMeta - The meta object associated with the row -*/ -@tagName('td') -export default class EmberTd extends BaseTableCell { + The table cell component. This component manages cell level concerns, yields + the cell value, column value, row value, and all of their associated meta + objects. + + ```hbs + + + + + + + + + + + + ``` + + @yield {any} cellValue - The value of the cell + @yield {object} columnValue - The column definition + @yield {object} rowValue - The row definition + + @yield {object} cellMeta - The meta object associated with the cell + @yield {object} columnMeta - The meta object associated with the column + @yield {object} rowMeta - The meta object associated with the row + @class {{ember-td}} + @public + */ +export default BaseTableCell.extend({ + layout, + tagName: 'td', + /** - The API object passed in by the table row + The API object passed in by the table row + @argument api + @required + @type object */ - @argument - @type('object') - api; + api: null, /** - Action sent when the user clicks this element + Action sent when the user clicks this element + @argument onClick + @type Action? */ - @argument - @type(optional(Action)) - onClick; + onClick: null, /** - Action sent when the user double clicks this element + Action sent when the user double clicks this element + @argument onDoubleClick + @type Action? */ - @argument - @type(optional(Action)) - onDoubleClick; + onDoubleClick: null, - @computed('api') // only watch `api` due to a bug in Ember - get unwrappedApi() { + // only watch `api` due to a bug in Ember + unwrappedApi: computed('api', function() { return this.get('api.api') || this.get('api'); - } - - @alias('unwrappedApi.cellValue') - cellValue; - - @readOnly('unwrappedApi.cellMeta') - cellMeta; + }), - @readOnly('unwrappedApi.columnValue') - columnValue; + cellValue: alias('unwrappedApi.cellValue'), - @readOnly('unwrappedApi.columnMeta') - columnMeta; + cellMeta: readOnly('unwrappedApi.cellMeta'), - @readOnly('unwrappedApi.rowValue') - rowValue; + columnValue: readOnly('unwrappedApi.columnValue'), - @readOnly('unwrappedApi.rowMeta') - rowMeta; + columnMeta: readOnly('unwrappedApi.columnMeta'), - @readOnly('unwrappedApi.rowSelectionMode') - rowSelectionMode; + rowValue: readOnly('unwrappedApi.rowValue'), - @readOnly('unwrappedApi.checkboxSelectionMode') - checkboxSelectionMode; + rowMeta: readOnly('unwrappedApi.rowMeta'), - @readOnly('rowMeta.canCollapse') - canCollapse; + rowSelectionMode: readOnly('unwrappedApi.rowSelectionMode'), - init() { - super.init(...arguments); + checkboxSelectionMode: readOnly('unwrappedApi.checkboxSelectionMode'), - this.layout = layout; - } + canCollapse: readOnly('rowMeta.canCollapse'), - @computed('rowMeta.depth') - get depthClass() { + depthClass: computed('rowMeta.depth', function() { return `depth-${this.get('rowMeta.depth')}`; - } + }), - @computed('shouldShowCheckbox', 'rowSelectionMode') - get canSelect() { + canSelect: computed('shouldShowCheckbox', 'rowSelectionMode', function() { let rowSelectionMode = this.get('rowSelectionMode'); let shouldShowCheckbox = this.get('shouldShowCheckbox'); @@ -113,50 +97,49 @@ export default class EmberTd extends BaseTableCell { rowSelectionMode === SELECT_MODE.MULTIPLE || rowSelectionMode === SELECT_MODE.SINGLE ); - } + }), - @computed('checkboxSelectionMode') - get shouldShowCheckbox() { + shouldShowCheckbox: computed('checkboxSelectionMode', function() { let checkboxSelectionMode = this.get('checkboxSelectionMode'); return ( checkboxSelectionMode === SELECT_MODE.MULTIPLE || checkboxSelectionMode === SELECT_MODE.SINGLE ); - } + }), - @action - onSelectionToggled(event) { - let rowMeta = this.get('rowMeta'); - let checkboxSelectionMode = this.get('checkboxSelectionMode') || this.get('rowSelectionMode'); + actions: { + onSelectionToggled(event) { + let rowMeta = this.get('rowMeta'); + let checkboxSelectionMode = this.get('checkboxSelectionMode') || this.get('rowSelectionMode'); - if (rowMeta && checkboxSelectionMode === SELECT_MODE.MULTIPLE) { - let toggle = true; - let range = event.shiftKey; + if (rowMeta && checkboxSelectionMode === SELECT_MODE.MULTIPLE) { + let toggle = true; + let range = event.shiftKey; - rowMeta.select({ toggle, range }); - } else if (rowMeta && checkboxSelectionMode === SELECT_MODE.SINGLE) { - rowMeta.select(); - } + rowMeta.select({ toggle, range }); + } else if (rowMeta && checkboxSelectionMode === SELECT_MODE.SINGLE) { + rowMeta.select(); + } - this.sendFullAction('onSelect'); - } + this.sendFullAction('onSelect'); + }, - @action - onCollapseToggled() { - let rowMeta = this.get('rowMeta'); + onCollapseToggled() { + let rowMeta = this.get('rowMeta'); - rowMeta.toggleCollapse(); + rowMeta.toggleCollapse(); - this.sendFullAction('onCollapse'); - } + this.sendFullAction('onCollapse'); + }, + }, click(event) { this.sendFullAction('onClick', { event }); - } + }, doubleClick(event) { this.sendFullAction('onDoubleClick', { event }); - } + }, sendFullAction(action, values = {}) { // If the action doesn't exist, it's not being used. Do nothing @@ -185,5 +168,5 @@ export default class EmberTd extends BaseTableCell { }); this.sendAction(action, values); - } -} + }, +}); diff --git a/addon/components/ember-tfoot/component.js b/addon/components/ember-tfoot/component.js index a0f206581..cb792a0d3 100644 --- a/addon/components/ember-tfoot/component.js +++ b/addon/components/ember-tfoot/component.js @@ -1,8 +1,7 @@ import EmberTBody from '../ember-tbody/component'; import { A as emberA } from '@ember/array'; -import { computed } from '@ember-decorators/object'; -import { tagName } from '@ember-decorators/component'; +import { computed } from '@ember/object'; import layout from './template'; @@ -23,22 +22,18 @@ import layout from './template'; ``` - @yield {object} f - the API object yielded by the table footer - @yield {Component} f.row - The table row component - - @yield {object} f.rowValue - The value for the currently yielded row - @yield {object} f.rowMeta - The meta for the currently yielded row + @yield {object} foot - the API object yielded by the table footer + @yield {Component} foot.row - The table row component + @yield {object} foot.rowValue - The value for the currently yielded row + @yield {object} foot.rowMeta - The meta for the currently yielded row + @class {{ember-tfoot}} + @public */ -@tagName('tfoot') -export default class EmberTFoot extends EmberTBody { - init() { - super.init(...arguments); - - this.layout = layout; - } +export default EmberTBody.extend({ + layout, + tagName: 'tfoot', - @computed('wrappedRows.[]') - get wrappedRowArray() { + wrappedRowArray: computed('wrappedRows.[]', function() { let wrappedRows = this.get('wrappedRows'); let wrappedRowsLength = wrappedRows.get('length'); @@ -49,5 +44,5 @@ export default class EmberTFoot extends EmberTBody { } return emberA(arr); - } -} + }), +}); diff --git a/addon/components/ember-th/component.js b/addon/components/ember-th/component.js index 5f78e8777..f07843c1e 100644 --- a/addon/components/ember-th/component.js +++ b/addon/components/ember-th/component.js @@ -2,13 +2,7 @@ import BaseTableCell from '../-private/base-table-cell'; import { next } from '@ember/runloop'; -import { action } from '@ember-decorators/object'; -import { readOnly } from '@ember-decorators/object/computed'; -import { attribute, className, tagName } from '@ember-decorators/component'; -import { argument } from '@ember-decorators/argument'; -import { required } from '@ember-decorators/argument/validation'; -import { type } from '@ember-decorators/argument/type'; - +import { readOnly } from '@ember/object/computed'; import { closest } from '../../-private/utils/element'; import layout from './template'; @@ -37,87 +31,73 @@ const COLUMN_REORDERING = 2; ``` @yield {object} columnValue - The column definition @yield {object} columnMeta - The meta object associated with this column + @class {{ember-th}} + @public */ -@tagName('th') -export default class EmberTh extends BaseTableCell { - layout = layout; +export default BaseTableCell.extend({ + layout, + tagName: 'th', + attributeBindings: ['columnSpan:colspan', 'rowSpan:rowspan'], + classNameBindings: ['isSortable', 'isResizable', 'isReorderable'], /** The API object passed in by the table row + @argument api + @required + @type object */ - @argument - @required - @type('object') - api; + api: null, - @readOnly('api.columnValue') - columnValue; + /** + Action sent when the user clicks right this element + @argument onContextMenu + @type Action? + */ + onContextMenu: null, - @readOnly('api.columnMeta') - columnMeta; + columnValue: readOnly('api.columnValue'), + + columnMeta: readOnly('api.columnMeta'), /** Any sorts applied to the table. */ - @readOnly('api.sorts') - sorts; + sorts: readOnly('api.sorts'), /** Whether or not the column is sortable. Is true IFF the column is a leaf node onUpdateSorts is set on the thead. */ - @className - @readOnly('columnMeta.isSortable') - isSortable; + isSortable: readOnly('columnMeta.isSortable'), /** Indicates if this column can be resized. */ - @className - @readOnly('columnMeta.isResizable') - isResizable; + isResizable: readOnly('columnMeta.isResizable'), /** Indicates if this column can be reordered. */ - @className - @readOnly('columnMeta.isReorderable') - isReorderable; - - @readOnly('columnMeta.sortIndex') - sortIndex; + isReorderable: readOnly('columnMeta.isReorderable'), - @readOnly('columnMeta.isSorted') - isSorted; + columnSpan: readOnly('columnMeta.columnSpan'), - @readOnly('columnMeta.isMultiSorted') - isMultiSorted; - - @readOnly('columnMeta.isSortedAsc') - isSortedAsc; - - @attribute('colspan') - @readOnly('columnMeta.columnSpan') - columnSpan; - - @attribute('rowspan') - @readOnly('columnMeta.rowSpan') - rowSpan; + rowSpan: readOnly('columnMeta.rowSpan'), /** A variable used for column resizing & ordering. When user press mouse at a point that's close to column boundary (using some threshold), this variable set whether it's the left or right column. */ - _columnState = COLUMN_INACTIVE; + _columnState: COLUMN_INACTIVE, /** An object that listens to touch/ press/ drag events. */ - _hammer = null; + _hammer: null, didInsertElement() { - super.didInsertElement(...arguments); + this._super(...arguments); this.get('columnMeta').registerElement(this.element); @@ -125,13 +105,13 @@ export default class EmberTh extends BaseTableCell { hammer.add(new Hammer.Press({ time: 0 })); - hammer.on('press', this.pressHandler); - hammer.on('panstart', this.panStartHandler); - hammer.on('panmove', this.panMoveHandler); - hammer.on('panend', this.panEndHandler); + hammer.on('press', this.pressHandler.bind(this)); + hammer.on('panstart', this.panStartHandler.bind(this)); + hammer.on('panmove', this.panMoveHandler.bind(this)); + hammer.on('panend', this.panEndHandler.bind(this)); this._hammer = hammer; - } + }, willDestroyElement() { let hammer = this._hammer; @@ -143,13 +123,14 @@ export default class EmberTh extends BaseTableCell { hammer.destroy(); - super.willDestroyElement(...arguments); - } + this._super(...arguments); + }, - @action - sendDropdownAction(...args) { - this.sendAction('onDropdownAction', ...args); - } + actions: { + sendDropdownAction(...args) { + this.sendAction('onDropdownAction', ...args); + }, + }, click(event) { let isSortable = this.get('isSortable'); @@ -160,7 +141,12 @@ export default class EmberTh extends BaseTableCell { this.updateSort({ toggle }); } - } + }, + + contextMenu(event) { + this.sendAction('onContextMenu', event); + return false; + }, keyUp(event) { let isSortable = this.get('isSortable'); @@ -174,7 +160,7 @@ export default class EmberTh extends BaseTableCell { ) { this.updateSort(); } - } + }, updateSort({ toggle }) { let valuePath = this.get('columnValue.valuePath'); @@ -190,16 +176,16 @@ export default class EmberTh extends BaseTableCell { } this.get('api').sendUpdateSort(newSortings); - } + }, - pressHandler = event => { + pressHandler(event) { let [{ clientX, target }] = event.pointers; this._originalClientX = clientX; this._originalTargetWasResize = target.classList.contains('et-header-resize-area'); - }; + }, - panStartHandler = event => { + panStartHandler(event) { let isResizable = this.get('isResizable'); let isReorderable = this.get('isReorderable'); @@ -214,9 +200,9 @@ export default class EmberTh extends BaseTableCell { this.get('columnMeta').startReorder(clientX); } - }; + }, - panMoveHandler = event => { + panMoveHandler(event) { let [{ clientX }] = event.pointers; if (this._columnState === COLUMN_RESIZING) { @@ -226,9 +212,9 @@ export default class EmberTh extends BaseTableCell { this.get('columnMeta').updateReorder(clientX); this._columnState = COLUMN_REORDERING; } - }; + }, - panEndHandler = () => { + panEndHandler() { if (this._columnState === COLUMN_RESIZING) { this.get('columnMeta').endResize(); } else if (this._columnState === COLUMN_REORDERING) { @@ -236,5 +222,5 @@ export default class EmberTh extends BaseTableCell { } next(() => (this._columnState = COLUMN_INACTIVE)); - }; -} + }, +}); diff --git a/addon/components/ember-th/resize-handle/component.js b/addon/components/ember-th/resize-handle/component.js new file mode 100644 index 000000000..66baae5c6 --- /dev/null +++ b/addon/components/ember-th/resize-handle/component.js @@ -0,0 +1,40 @@ +import Component from '@ember/component'; +import layout from './template'; + +import { readOnly } from '@ember/object/computed'; + +/** + The table header cell resize handle component. This component renders an area to grab to resize a column. + + ```hbs + + + + + {{columnValue.name}} + + + + + + + + + ``` + @class {{ember-th/resize-handle}} + @public +*/ +export default Component.extend({ + layout, + tagName: '', + + /** + The API object passed in by the table header cell + @argument columnMeta + @required + @type object + */ + columnMeta: null, + + isResizable: readOnly('columnMeta.isResizable'), +}); diff --git a/addon/components/ember-th/resize-handle/template.hbs b/addon/components/ember-th/resize-handle/template.hbs new file mode 100644 index 000000000..6bf621609 --- /dev/null +++ b/addon/components/ember-th/resize-handle/template.hbs @@ -0,0 +1,4 @@ + {{#if isResizable}} +
+
+ {{/if}} \ No newline at end of file diff --git a/addon/components/ember-th/sort-indicator/component.js b/addon/components/ember-th/sort-indicator/component.js new file mode 100644 index 000000000..d96d6a527 --- /dev/null +++ b/addon/components/ember-th/sort-indicator/component.js @@ -0,0 +1,49 @@ +import Component from '@ember/component'; +import layout from './template'; + +import { readOnly } from '@ember/object/computed'; + +/** + The table header cell sort indicator component. This component renders the state of the sort on the column (ascending/descending/none). + + ```hbs + + + + + {{columnValue.name}} + + + + + + + + + ``` + @yield {object} columnMeta - The meta object associated with this column + @class {{ember-th/sort-indicator}} +*/ + +export default Component.extend({ + layout, + tagName: '', + + /** + The API object passed in by the table header cell + @argument columnMeta + @required + @type object + */ + columnMeta: null, + + isSortable: readOnly('columnMeta.isSortable'), + + isSorted: readOnly('columnMeta.isSorted'), + + isSortedAsc: readOnly('columnMeta.isSortedAsc'), + + isMultiSorted: readOnly('columnMeta.isMultiSorted'), + + sortIndex: readOnly('columnMeta.sortIndex'), +}); diff --git a/addon/components/ember-th/sort-indicator/template.hbs b/addon/components/ember-th/sort-indicator/template.hbs new file mode 100644 index 000000000..e196e69d3 --- /dev/null +++ b/addon/components/ember-th/sort-indicator/template.hbs @@ -0,0 +1,15 @@ +{{#if isSorted}} + + {{#if hasBlock}} + {{yield columnMeta}} + {{else}} + {{#if isMultiSorted}} + {{sortIndex}} + {{/if}} + {{/if}} + +{{/if}} + +{{#if isSortable}} + +{{/if}} \ No newline at end of file diff --git a/addon/components/ember-th/template.hbs b/addon/components/ember-th/template.hbs index 7e535f8b6..03b5733e6 100644 --- a/addon/components/ember-th/template.hbs +++ b/addon/components/ember-th/template.hbs @@ -4,19 +4,6 @@ {{columnValue.name}} {{/if}} -{{#if isSorted}} - - {{#if isMultiSorted}} - {{sortIndex}} - {{/if}} - -{{/if}} - -{{#if isSortable}} - -{{/if}} +{{ember-th/sort-indicator columnMeta=columnMeta}} -{{#if isResizable}} -
-
-{{/if}} +{{ember-th/resize-handle columnMeta=columnMeta}} diff --git a/addon/components/ember-thead/component.js b/addon/components/ember-thead/component.js index 82d2d6ea9..02672f779 100644 --- a/addon/components/ember-thead/component.js +++ b/addon/components/ember-thead/component.js @@ -2,17 +2,14 @@ import Component from '@ember/component'; import { bind } from '@ember/runloop'; import { A as emberA } from '@ember/array'; - -import { argument } from '@ember-decorators/argument'; -import { required } from '@ember-decorators/argument/validation'; -import { type, optional } from '@ember-decorators/argument/type'; -import { Action } from '@ember-decorators/argument/types'; -import { computed } from '@ember-decorators/object'; -import { notEmpty, or } from '@ember-decorators/object/computed'; -import { tagName } from '@ember-decorators/component'; +import defaultTo from '../../-private/utils/default-to'; +import { addObserver } from '../../-private/utils/observer'; +import { computed } from '@ember/object'; +import { notEmpty, or, readOnly } from '@ember/object/computed'; import { closest } from '../../-private/utils/element'; import { sortMultiple, compareValues } from '../../-private/utils/sort'; +import { scheduleOnce } from '@ember/runloop'; import ColumnTree, { RESIZE_MODE, FILL_MODE, WIDTH_CONSTRAINT } from '../../-private/column-tree'; @@ -33,133 +30,165 @@ import layout from './template'; ``` - @yield {object} h - the API object yielded by the table header - @yield {Component} h.row - The table row component + @yield {object} head - the API object yielded by the table header + @yield {Component} head.row - The table row component + @class {{ember-thead}} + @public */ -@tagName('thead') -export default class EmberTHead extends Component { + +export default Component.extend({ + layout, + tagName: 'thead', + /** The API object passed in by the table + + @argument api + @required + @type object */ - @argument - @required - @type('object') - api; + api: null, - @or('api.api', 'api') - unwrappedApi; + unwrappedApi: or('api.api', 'api'), /** The column definitions for the table + + @argument columns + @required + @type array? ([]) */ - @argument - @required - @type(Array) - columns; + columns: defaultTo(() => []), /** An ordered array of the sorts applied to the table + @argument sorts + @type array? ([]) */ - @argument({ defaultIfUndefined: true }) - @type(Array) - sorts = []; + sorts: defaultTo(() => []), /** - An optional sort + An optional sort. If not specified, defaults to , which + sorts by each `sort` in `sorts`, in order. + @argument sortFunction + @type function? () */ - @argument({ defaultIfUndefined: true }) - @type(optional(Function)) - sortFunction = sortMultiple; + sortFunction: defaultTo(() => sortMultiple), /** An ordered array of the sorts applied to the table + @argument compareFunction + @type function? () */ - @argument({ defaultIfUndefined: true }) - @type(optional(Function)) - compareFunction = compareValues; + compareFunction: defaultTo(() => compareValues), + + /** + Flag that allows to sort empty values after non empty ones + @argument sortEmptyLast + @type boolean? (false) + */ + sortEmptyLast: defaultTo(false), /** Flag that toggles reordering in the table + @argument enableReorder + @type boolean? (true) */ - @argument({ defaultIfUndefined: true }) - @type('boolean') - enableReorder = true; + enableReorder: defaultTo(true), /** Flag that toggles resizing in the table + @argument enableResize + @type boolean? (true) */ - @argument({ defaultIfUndefined: true }) - @type('boolean') - enableResize = true; + enableResize: defaultTo(true), /** Sets which column resizing behavior to use. Possible values are `standard` (resizing a column pushes or pulls all other columns) and `fluid` (resizing a column subtracts width from neighboring columns). + @argument resizeMode + @type string? ('standard') */ - @argument({ defaultIfUndefined: true }) - @type('string') - resizeMode = RESIZE_MODE.STANDARD; + resizeMode: defaultTo(RESIZE_MODE.STANDARD), /** A configuration that controls how columns shrink (or extend) when total column width does not - match table width. Behavior of column modification is as follow: + match table width. Behavior of column modification is as follows: * "equal-column": extra space is distributed equally among all columns * "first-column": extra space is added into the first column. * "last-column": extra space is added into the last column. + * "nth-column": extra space is added into the column defined by `fillColumnIndex`. + + @argument fillMode + @type string? ('equal-column') + */ + fillMode: defaultTo(FILL_MODE.EQUAL_COLUMN), + + /** + A configuration that controls which column shrinks (or extends) when `fillMode` is + 'nth-column'. This is zero indexed. + + @argument fillColumnIndex + @type number? */ - @argument({ defaultIfUndefined: true }) - @type('string') - fillMode = FILL_MODE.EQUAL_COLUMN; + fillColumnIndex: null, /** Sets a constraint on the table's size, such that it must be greater than, less than, or equal to the size of the containing element. + Valid values: + * 'none' + * 'eq-container' + * 'gte-container' + * 'lte-container' + + @argument widthConstraint + @type string? ('none') */ - @argument({ defaultIfUndefined: true }) - @type('string') - widthConstraint = WIDTH_CONSTRAINT.NONE; + widthConstraint: defaultTo(WIDTH_CONSTRAINT.NONE), /** A numeric adjustment to be applied to the constraint on the table's size. + + @argument containerWidthAdjustment + @type number? */ - @argument - @type(optional('number')) - containerWidthAdjustment = null; + containerWidthAdjustment: null, /** An action that is sent when sorts is updated + @argument onHeaderAction + @type Action? */ - @argument - @type(optional(Action)) - onHeaderAction = null; + onHeaderAction: null, /** An action that is sent when sorts is updated + @argument onUpdateSorts + @type Action? */ - @argument - @type(optional(Action)) - onUpdateSorts = null; + onUpdateSorts: null, /** An action that is sent when columns are reordered + @argument onReorder + @type Action? */ - @argument - @type(optional(Action)) - onReorder = null; + onReorder: null, /** An action that is sent when columns are resized + @argument onResize + @type Action? */ - @argument - @type(optional(Action)) - onResize = null; + onResize: null, - init() { - super.init(...arguments); + 'data-test-row-count': readOnly('wrappedRows.length'), - this.layout = layout; + init() { + this._super(...arguments); /** * A sensor object that sends events to this table component when table size changes. When table @@ -185,49 +214,60 @@ export default class EmberTHead extends Component { this._updateApi(); this._updateColumnTree(); - this.addObserver('sorts', this._updateApi); - this.addObserver('sortFunction', this._updateApi); - this.addObserver('reorderFunction', this._updateApi); + addObserver(this, 'sorts', this._updateApi); + addObserver(this, 'sortFunction', this._updateApi); + addObserver(this, 'reorderFunction', this._updateApi); - this.addObserver('sorts', this._updateColumnTree); - this.addObserver('columns', this._updateColumnTree); - this.addObserver('fillMode', this._updateColumnTree); - this.addObserver('resizeMode', this._updateColumnTree); - this.addObserver('widthConstraint', this._updateColumnTree); + addObserver(this, 'sorts', this._updateColumnTree); + addObserver(this, 'columns.[]', this._onColumnsChange); + addObserver(this, 'fillMode', this._updateColumnTree); + addObserver(this, 'fillColumnIndex', this._updateColumnTree); + addObserver(this, 'resizeMode', this._updateColumnTree); + addObserver(this, 'widthConstraint', this._updateColumnTree); - this.addObserver('enableSort', this._updateColumnTree); - this.addObserver('enableResize', this._updateColumnTree); - this.addObserver('enableReorder', this._updateColumnTree); - } + addObserver(this, 'enableSort', this._updateColumnTree); + addObserver(this, 'enableResize', this._updateColumnTree); + addObserver(this, 'enableReorder', this._updateColumnTree); + }, _updateApi() { this.set('unwrappedApi.columnTree', this.columnTree); this.set('unwrappedApi.sorts', this.get('sorts')); this.set('unwrappedApi.sortFunction', this.get('sortFunction')); this.set('unwrappedApi.compareFunction', this.get('compareFunction')); - } + this.set('unwrappedApi.sortEmptyLast', this.get('sortEmptyLast')); + }, _updateColumnTree() { this.columnTree.set('sorts', this.get('sorts')); this.columnTree.set('columns', this.get('columns')); this.columnTree.set('fillMode', this.get('fillMode')); + this.columnTree.set('fillColumnIndex', this.get('fillColumnIndex')); this.columnTree.set('resizeMode', this.get('resizeMode')); this.columnTree.set('widthConstraint', this.get('widthConstraint')); this.columnTree.set('enableSort', this.get('enableSort')); this.columnTree.set('enableResize', this.get('enableResize')); this.columnTree.set('enableReorder', this.get('enableReorder')); - } + }, + + _onColumnsChange() { + if (this.get('columns.length') === 0) { + return; + } + this._updateColumnTree(); + scheduleOnce('actions', this, this.fillupHandler); + }, didInsertElement() { - super.didInsertElement(...arguments); + this._super(...arguments); this._container = closest(this.element, '.ember-table'); this.columnTree.registerContainer(this._container); - this._tableResizeSensor = new ResizeSensor(this._container, bind(this.fillupHandler)); - } + this._tableResizeSensor = new ResizeSensor(this._container, bind(this, this.fillupHandler)); + }, willDestroyElement() { this._tableResizeSensor.detach(this._container); @@ -240,47 +280,52 @@ export default class EmberTHead extends Component { this.columnMetaCache.delete(column); } - super.willDestroyElement(...arguments); - } - - @notEmpty('onUpdateSorts') - enableSort; - - @computed('columnTree.rows.[]', 'sorts.[]', 'headerActions.[]', 'fillMode') - get wrappedRows() { - let rows = this.get('columnTree.rows'); - let sorts = this.get('sorts'); - let columnMetaCache = this.get('columnMetaCache'); - - return emberA( - rows.map(row => { - let cells = emberA( - row.map(columnValue => { - let columnMeta = columnMetaCache.get(columnValue); - - return { - columnValue, - columnMeta, - sorts, - sendUpdateSort: this.sendUpdateSort, - }; - }) - ); - - return { cells, isHeader: true }; - }) - ); - } - - sendUpdateSort = newSorts => { + this._super(...arguments); + }, + + enableSort: notEmpty('onUpdateSorts'), + + wrappedRows: computed( + 'columnTree.rows.[]', + 'sorts.[]', + 'headerActions.[]', + 'fillMode', + 'fillColumnIndex', + function() { + let rows = this.get('columnTree.rows'); + let sorts = this.get('sorts'); + let columnMetaCache = this.get('columnMetaCache'); + + return emberA( + rows.map(row => { + let cells = emberA( + row.map(columnValue => { + let columnMeta = columnMetaCache.get(columnValue); + + return { + columnValue, + columnMeta, + sorts, + sendUpdateSort: this.sendUpdateSort.bind(this), + }; + }) + ); + + return { cells, isHeader: true }; + }) + ); + } + ), + + sendUpdateSort(newSorts) { this.sendAction('onUpdateSorts', newSorts); - }; + }, - fillupHandler = () => { + fillupHandler() { if (this.isDestroying) { return; } this.get('columnTree').ensureWidthConstraint(); - }; -} + }, +}); diff --git a/addon/components/ember-tr/component.js b/addon/components/ember-tr/component.js index 65baaa5f2..1660c229b 100644 --- a/addon/components/ember-tr/component.js +++ b/addon/components/ember-tr/component.js @@ -1,12 +1,6 @@ import Component from '@ember/component'; -import { computed } from '@ember-decorators/object'; -import { readOnly } from '@ember-decorators/object/computed'; -import { className, classNames, tagName } from '@ember-decorators/component'; - -import { argument } from '@ember-decorators/argument'; -import { required } from '@ember-decorators/argument/validation'; -import { type, optional } from '@ember-decorators/argument/type'; -import { Action } from '@ember-decorators/argument/types'; +import { computed } from '@ember/object'; +import { readOnly } from '@ember/object/computed'; import { closest } from '../../-private/utils/element'; @@ -35,79 +29,68 @@ import { SELECT_MODE } from '../../-private/collapse-tree'; ``` - @yield {object} r - the API object yielded by the table row - @yield {Component} r.cell - The table cell contextual component + @yield {object} row - the API object yielded by the table row + @yield {Component} row.cell - The table cell contextual component + + @yield {any} row.cellValue - The value for the currently yielded cell + @yield {object} row.cellMeta - The meta for the currently yielded cell - @yield {any} r.cellValue - The value for the currently yielded cell - @yield {object} r.cellMeta - The meta for the currently yielded cell + @yield {object} row.columnValue - The value for the currently yielded column + @yield {object} row.columnMeta - The meta for the currently yielded column - @yield {object} r.columnValue - The value for the currently yielded column - @yield {object} r.columnMeta - The meta for the currently yielded column + @yield {object} row.rowValue - The value for the currently yielded row + @yield {object} row.rowMeta - The meta for the currently yielded row - @yield {object} r.rowValue - The value for the currently yielded row - @yield {object} r.rowMeta - The meta for the currently yielded row + @class {{ember-tr}} + @public */ -@tagName('tr') -@classNames('et-tr') -export default class EmberTr extends Component { +export default Component.extend({ + layout, + tagName: 'tr', + classNames: ['et-tr'], + classNameBindings: ['isSelected', 'isGroupSelected', 'isSelectable'], + /** The API object passed in by the table body, header, or footer + @argument api + @required + @type object */ - @argument - @required - @type('object') - api; + api: null, /** Action sent when the user clicks this element + @argument onClick + @type Action? */ - @argument - @type(optional(Action)) - onClick; + onClick: null, /** Action sent when the user double clicks this element + @argument onDoubleClick + @type Action? */ - @argument - @type(optional(Action)) - onDoubleClick; - - @readOnly('api.rowValue') - rowValue; - - @readOnly('api.rowMeta') - rowMeta; + onDoubleClick: null, - @readOnly('api.cells') - cells; + rowValue: readOnly('api.rowValue'), - @readOnly('api.rowSelectionMode') - rowSelectionMode; + rowMeta: readOnly('api.rowMeta'), - @readOnly('api.isHeader') - isHeader; + cells: readOnly('api.cells'), - @className - @readOnly('rowMeta.isSelected') - isSelected; + rowSelectionMode: readOnly('api.rowSelectionMode'), - @className - @readOnly('rowMeta.isGroupSelected') - isGroupSelected; + isHeader: readOnly('api.isHeader'), - init() { - super.init(...arguments); + isSelected: readOnly('rowMeta.isSelected'), - this.layout = layout; - } + isGroupSelected: readOnly('rowMeta.isGroupSelected'), - @className - @computed('rowSelectionMode') - get isSelectable() { + isSelectable: computed('rowSelectionMode', function() { let rowSelectionMode = this.get('rowSelectionMode'); return rowSelectionMode === SELECT_MODE.MULTIPLE || rowSelectionMode === SELECT_MODE.SINGLE; - } + }), click(event) { let rowSelectionMode = this.get('rowSelectionMode'); @@ -127,11 +110,11 @@ export default class EmberTr extends Component { } this.sendEventAction('onClick', event); - } + }, doubleClick(event) { this.sendEventAction('onDoubleClick', event); - } + }, sendEventAction(action, event) { let rowValue = this.get('rowValue'); @@ -143,5 +126,5 @@ export default class EmberTr extends Component { rowValue, rowMeta, }); - } -} + }, +}); diff --git a/addon/styles/addon.scss b/addon/styles/addon.scss index 7cb2b8abf..823173b74 100644 --- a/addon/styles/addon.scss +++ b/addon/styles/addon.scss @@ -22,6 +22,18 @@ position: sticky; left: 0; } + + &.ember-table__text-align-left { + text-align: left; + } + + &.ember-table__text-align-center { + text-align: center; + } + + &.ember-table__text-align-right { + text-align: right; + } } th { diff --git a/app/components/ember-th/resize-handle.js b/app/components/ember-th/resize-handle.js new file mode 100644 index 000000000..151233512 --- /dev/null +++ b/app/components/ember-th/resize-handle.js @@ -0,0 +1 @@ +export { default } from 'ember-table/components/ember-th/resize-handle/component'; diff --git a/app/components/ember-th/sort-indicator.js b/app/components/ember-th/sort-indicator.js new file mode 100644 index 000000000..3f8e00b9d --- /dev/null +++ b/app/components/ember-th/sort-indicator.js @@ -0,0 +1 @@ +export { default } from 'ember-table/components/ember-th/sort-indicator/component'; diff --git a/config/ember-try.js b/config/ember-try.js index c5ca4e3d5..7d82658f7 100644 --- a/config/ember-try.js +++ b/config/ember-try.js @@ -116,6 +116,7 @@ module.exports = function() { devDependencies: { 'ember-source': null, 'ember-angle-bracket-invocation-polyfill': null, + 'ember-cli-addon-docs': null, }, }, }, @@ -124,6 +125,7 @@ module.exports = function() { npm: { devDependencies: { 'ember-source': '~2.12.0', + 'ember-cli-addon-docs': null, }, }, }, @@ -132,6 +134,7 @@ module.exports = function() { npm: { devDependencies: { 'ember-source': '~2.16.0', + 'ember-cli-addon-docs': null, }, }, }, @@ -140,6 +143,25 @@ module.exports = function() { npm: { devDependencies: { 'ember-source': '~2.18.0', + 'ember-cli-addon-docs': null, + }, + }, + }, + { + name: 'ember-lts-3.4', + npm: { + devDependencies: { + 'ember-source': '~3.4.0', + 'ember-cli-addon-docs': null, + }, + }, + }, + { + name: 'ember-lts-3.8', + npm: { + devDependencies: { + 'ember-source': '~3.8.0', + 'ember-cli-addon-docs': null, }, }, }, @@ -148,6 +170,7 @@ module.exports = function() { npm: { devDependencies: { 'ember-source': urls[0], + 'ember-cli-addon-docs': null, }, }, }, @@ -156,6 +179,7 @@ module.exports = function() { npm: { devDependencies: { 'ember-source': urls[1], + 'ember-cli-addon-docs': null, }, }, }, @@ -164,6 +188,7 @@ module.exports = function() { npm: { devDependencies: { 'ember-source': urls[2], + 'ember-cli-addon-docs': null, }, }, }, diff --git a/ember-cli-build.js b/ember-cli-build.js index 4aaa05cbb..c9050f1c7 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -2,36 +2,16 @@ const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); -const UnwatchedDir = require('broccoli-source').UnwatchedDir; -const MergeTrees = require('broccoli-merge-trees'); -const Funnel = require('broccoli-funnel'); - -function generateDefaultProject() { - // We need to be very careful to avoid triggering a watch on the addon root here - // because of https://github.com/nodejs/node/issues/15683 - let packageDetails = new Funnel(new UnwatchedDir('.'), { - include: ['package.json', 'README.md'], - }); - - let addonFiles = new Funnel('addon', { - exclude: ['**/-private/**'], - }); - - return new MergeTrees([packageDetails, addonFiles]); -} - module.exports = function(defaults) { let app = new EmberAddon(defaults, { 'ember-cli-babel': { includePolyfill: true, }, babel: { - plugins: ['transform-object-rest-spread'], + plugins: ['@babel/plugin-proposal-object-rest-spread'], }, - 'ember-cli-addon-docs': { - projects: { - main: generateDefaultProject(), - }, + 'ember-faker': { + enabled: true, // Always enable for dummy app because the docs examples use faker }, }); diff --git a/jsconfig.json b/jsconfig.json index 01d7c5e95..278f8bbbe 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,13 +1,7 @@ { "compilerOptions": { - "target": "ES6", - "experimentalDecorators": true + "target": "ES6", + "experimentalDecorators": true }, - "include": [ - "addon/**/*" - ], - "exclude": [ - "node_modules", - "tmp" - ] + "exclude": ["node_modules", "tmp", "dist"] } diff --git a/package.json b/package.json index f1df7cf4a..d77041dd8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ember-table", - "version": "2.0.0-beta.7", + "version": "2.2.3", "description": "An addon to support large data set and a number of features around table.", "keywords": [ "ember-addon" @@ -20,87 +20,87 @@ "test:all": "ember try:each", "lint": "ember adde-lint", "lint:sass": "ember adde-lint --sass", - "lint:files": "ember adde-lint --file-names" + "lint:files": "ember adde-lint --file-names", + "release": "release-it", + "docs:deploy": "ember deploy production" }, "dependencies": { - "@ember-decorators/argument": "^0.8.16", - "@ember-decorators/babel-transforms": "^0.1.1", - "@html-next/vertical-collection": "1.0.0-beta.12", + "@html-next/vertical-collection": "^1.0.0-beta.14", "broccoli-string-replace": "^0.1.2", "css-element-queries": "^0.4.0", "ember-assign-polyfill": "^2.2.0", - "ember-cli-babel": "^6.7.1", - "ember-cli-htmlbars": "^2.0.1", - "ember-cli-htmlbars-inline-precompile": "^1.0.2", + "ember-classy-page-object": "^0.6.1", + "ember-cli-babel": "^7.12.0", + "ember-cli-htmlbars": "^3.0.1", + "ember-cli-htmlbars-inline-precompile": "^2.1.0", "ember-cli-node-assets": "^0.2.2", "ember-cli-sass": "^7.0.0", - "ember-cli-version-checker": "^2.1.2", - "ember-compatibility-helpers": "^1.0.0", - "ember-decorators": "^2.2.0", + "ember-cli-version-checker": "^3.0.1", + "ember-compatibility-helpers": "^1.2.0", "ember-getowner-polyfill": "^2.2.0", - "ember-legacy-class-shim": "^1.0.0", "ember-raf-scheduler": "^0.1.0", - "ember-test-selectors": "^0.3.9", + "ember-test-selectors": "^2.1.0", "ember-useragent": "^0.6.0", "hammerjs": "^2.0.8" }, "devDependencies": { "@addepar/ember-toolbox": "^0.3.2", - "@addepar/eslint-config": "^4.0.0", + "@addepar/eslint-config": "^4.0.2", "@addepar/prettier-config": "^1.0.0", "@addepar/sass-lint-config": "^2.0.1", - "@addepar/style-toolbox": "^0.5.0", + "@addepar/style-toolbox": "~0.7.0", + "@babel/plugin-proposal-object-rest-spread": "^7.6.2", "@types/ember": "^2.8.22", - "babel-eslint": "^8.2.3", - "babel-plugin-transform-object-rest-spread": "^6.26.0", - "broccoli-asset-rev": "^2.4.5", - "broccoli-funnel": "^2.0.1", - "broccoli-merge-trees": "^3.0.0", - "broccoli-source": "^1.1.0", + "babel-eslint": "^10.0.1", + "broccoli-asset-rev": "^3.0.0", "ember-a11y-testing": "^0.5.0", - "ember-ajax": "^3.0.0", - "ember-angle-bracket-invocation-polyfill": "^1.1.4", - "ember-auto-import": "^1.0.1", - "ember-classy-page-object": "^0.5.0", + "ember-ajax": "^4.0.2", + "ember-angle-bracket-invocation-polyfill": "^1.3.1", "ember-cli": "~3.1.4", - "ember-cli-addon-docs": "^0.5.0", - "ember-cli-addon-docs-esdoc": "^0.2.1", - "ember-cli-dependency-checker": "^2.0.0", + "ember-cli-addon-docs": "0.6.13", + "ember-cli-addon-docs-yuidoc": "^0.2.1", + "ember-cli-dependency-checker": "^3.2.0", "ember-cli-deploy": "^1.0.2", "ember-cli-deploy-build": "^1.1.1", "ember-cli-deploy-git": "^1.3.3", "ember-cli-deploy-git-ci": "^1.0.1", - "ember-cli-eslint": "^4.2.1", - "ember-cli-inject-live-reload": "^1.4.1", - "ember-cli-qunit": "^4.1.1", + "ember-cli-eslint": "^5.1.0", + "ember-cli-inject-live-reload": "^2.0.1", "ember-cli-shims": "^1.2.0", "ember-cli-sri": "^2.1.0", "ember-cli-uglify": "^2.0.0", + "ember-debug-handlers-polyfill": "^1.1.1", "ember-disable-prototype-extensions": "^1.1.2", "ember-export-application-global": "^2.0.0", - "ember-fetch": "^5.0.0", - "ember-load-initializers": "^1.0.0", - "ember-math-helpers": "^2.4.1", + "ember-faker": "^1.5.0", + "ember-fetch": "^6.4.0", + "ember-load-initializers": "^2.0.0", + "ember-math-helpers": "^2.10.0", "ember-maybe-import-regenerator": "^0.1.6", - "ember-native-dom-helpers": "0.6.1", + "ember-native-dom-helpers": "0.6.2", + "ember-qunit": "^4.5.1", "ember-radio-button": "^1.2.3", - "ember-resolver": "^4.0.0", - "ember-source": "~3.1.0", + "ember-resolver": "^5.1.1", + "ember-source": "~3.12", "ember-source-channel-url": "^1.0.1", "ember-truth-helpers": "^2.0.0", - "ember-try": "^0.2.23", - "eslint-plugin-ember": "^5.0.0", - "eslint-plugin-node": "^6.0.1", + "ember-try": "^1.1.0", + "eslint-plugin-ember": "^6.2.0", + "eslint-plugin-node": "^8.0.1", "eslint-plugin-prettier": "2.6.0", - "faker": "^4.1.0", - "husky": "^0.14.3", - "loader.js": "^4.2.3" + "husky": "^1.3.1", + "loader.js": "^4.2.3", + "release-it": "^12.3.4" }, "engines": { - "node": "6.* || >= 7.*" + "node": "8.* || >= 10.*" }, "ember-addon": { "configPath": "tests/dummy/config" }, - "homepage": "https://Addepar.github.io/ember-table" + "homepage": "https://Addepar.github.io/ember-table", + "resolutions": { + "ember-cli-page-object": "1.15.1", + "prettier": "1.18.2" + } } diff --git a/tests/acceptance/docs-test.js b/tests/acceptance/docs-test.js new file mode 100644 index 000000000..8aba61b5a --- /dev/null +++ b/tests/acceptance/docs-test.js @@ -0,0 +1,113 @@ +import { module, test as qunitTest, skip as qunitSkip } from 'qunit'; +import { visit, currentURL, click } from '@ember/test-helpers'; +import { setupApplicationTest } from 'ember-qunit'; +import config from 'dummy/config/environment'; +import TablePage from 'ember-table/test-support/pages/ember-table'; + +let skip = (msg, ...args) => + qunitSkip(`Skip because ember-cli-addon-docs is not installed. ${msg}`, ...args); +let test = config.ADDON_DOCS_INSTALLED ? qunitTest : skip; + +// The nav that contains buttons to show each snippet in a +// `{{docs.demo}}`. See https://github.com/ember-learn/ember-cli-addon-docs/blob/a00a28e33acea463d82c64fa0a712913d70de3f1/addon/components/docs-demo/template.hbs#L11 +const DOCS_DEMO_SNIPPET_NAV_SELECTOR = '.docs-demo__snippets-nav'; + +module('Acceptance | docs', function(hooks) { + setupApplicationTest(hooks); + + test('visiting / redirects to /docs', async function(assert) { + await visit('/'); + + assert.equal(currentURL(), '/docs'); + }); + + test('pages linked to by /docs nav all render', async function(assert) { + await visit('/docs'); + + let nav = this.element.querySelector('nav'); + assert.ok(!!nav, 'nav exists'); + + let links = Array.from(nav.querySelectorAll('a')).filter(link => + link.getAttribute('href').startsWith('/docs') + ); + assert.ok(links.length > 0, `${links.length} nav links found`); + for (let link of links) { + let href = link.getAttribute('href'); + await visit(href); + assert.ok(true, `Visited ${href} successfully`); + + let buttonCount = 0; + let docsNavs = Array.from(this.element.querySelectorAll(DOCS_DEMO_SNIPPET_NAV_SELECTOR)); + for (let nav of docsNavs) { + let buttons = Array.from(nav.querySelectorAll('button')); + for (let button of buttons) { + await click(button); + buttonCount++; + } + } + assert.ok(true, `Clicked ${buttonCount} snippet buttons on "${href}"`); + + await visit('/docs'); // start over + } + }); + + test('subcolumns docs renders cell content', async function(assert) { + let DemoTable = TablePage.extend({ + scope: '[data-test-demo="docs-example-subcolumns"] [data-test-ember-table]', + }); + + await visit('/docs/guides/header/subcolumns'); + let table = new DemoTable(); + assert.equal(table.header.headers.objectAt(0).text, 'A', 'first header cell renders correctly'); + assert.equal( + table.body.rows.objectAt(0).cells.objectAt(0).text, + 'A A', + 'first body cell renders correclty' + ); + }); + + test('autogenerated API docs are present', async function(assert) { + await visit('/docs'); + + let nav = this.element.querySelector('nav'); + assert.ok(!!nav, 'nav exists'); + + let navItems = Array.from(nav.querySelectorAll('li')); + + let expectedNavItems = ['API REFERENCE', '{{ember-table}}']; + + expectedNavItems.forEach(expectedText => { + assert.ok( + navItems.some(li => li.innerText.includes(expectedText)), + `"${expectedText}" nav item is exists` + ); + }); + }); + + test('sorting: 2-state sorting works as expected', async function(assert) { + await visit('/docs/guides/header/sorting'); + let DemoTable = TablePage.extend({ + scope: '[data-test-demo="docs-example-2-state-sortings"] [data-test-ember-table]', + }); + + let table = new DemoTable(); + let header = table.headers.objectAt(0); + + assert.ok(!header.sortIndicator.isPresent, 'precond - sortIndicator is not present'); + + await header.click(); + assert.ok( + header.sortIndicator.isPresent && header.sortIndicator.isDescending, + 'sort descending' + ); + + await header.click(); + assert.ok(header.sortIndicator.isPresent && header.sortIndicator.isAscending, 'sort ascending'); + + await header.click(); + assert.ok( + header.sortIndicator.isPresent && header.sortIndicator.isDescending, + 'sort cycles back to descending' + ); + }); +}); diff --git a/tests/dummy/app/app.js b/tests/dummy/app/app.js index 496dc02d2..b12b53636 100644 --- a/tests/dummy/app/app.js +++ b/tests/dummy/app/app.js @@ -1,16 +1,14 @@ -/* globals define */ +/* globals define, require */ import Application from '@ember/application'; import Resolver from './resolver'; import loadInitializers from 'ember-load-initializers'; import config from './config/environment'; import EmberRouter from '@ember/routing/router'; -import { lte } from 'ember-compatibility-helpers'; - // Including ember-cli-addon-docs breaks certain versions of Ember when testing // but they also break if we remove it. This defines a stub router which should // prevent breakage. -if (lte('2.5.0')) { +if (!require.entries['ember-cli-addon-docs/router']) { define('ember-cli-addon-docs/router', () => { return EmberRouter; }); diff --git a/tests/dummy/app/pods/application/controller.js b/tests/dummy/app/pods/application/controller.js index 933f0b469..2f00de9b2 100644 --- a/tests/dummy/app/pods/application/controller.js +++ b/tests/dummy/app/pods/application/controller.js @@ -1,11 +1,9 @@ import Controller from '@ember/controller'; - -import { computed } from '@ember-decorators/object'; import { gte } from 'ember-compatibility-helpers'; +import { computed } from '@ember/object'; -export default class ApplicationController extends Controller { - @computed - get canShowAddonDocs() { +export default Controller.extend({ + canShowAddonDocs: computed(function() { return gte('2.8.0'); - } -} + }), +}); diff --git a/tests/dummy/app/pods/components/custom-cell/component.js b/tests/dummy/app/pods/components/custom-cell/component.js index 2bf0c1c13..849b79eac 100644 --- a/tests/dummy/app/pods/components/custom-cell/component.js +++ b/tests/dummy/app/pods/components/custom-cell/component.js @@ -1,9 +1,6 @@ -import { tagName } from '@ember-decorators/component'; -import { argument } from '@ember-decorators/argument'; import Component from '@ember/component'; -@tagName('') -export default class CustomCell extends Component { - @argument - color; -} +export default Component.extend({ + tagName: '', + color: null, +}); diff --git a/tests/dummy/app/pods/components/custom-cell/template.hbs b/tests/dummy/app/pods/components/custom-cell/template.hbs index a16e85f77..b9e5631d1 100644 --- a/tests/dummy/app/pods/components/custom-cell/template.hbs +++ b/tests/dummy/app/pods/components/custom-cell/template.hbs @@ -1,5 +1,5 @@ {{! BEGIN-SNIPPET custom-cell.hbs }} -
+
Cell {{yield}}
{{! END-SNIPPET }} diff --git a/tests/dummy/app/pods/components/custom-header/component.js b/tests/dummy/app/pods/components/custom-header/component.js index d9b71fab7..849b79eac 100644 --- a/tests/dummy/app/pods/components/custom-header/component.js +++ b/tests/dummy/app/pods/components/custom-header/component.js @@ -1,9 +1,6 @@ -import { tagName } from '@ember-decorators/component'; -import { argument } from '@ember-decorators/argument'; import Component from '@ember/component'; -@tagName('') -export default class CustomHeader extends Component { - @argument - color; -} +export default Component.extend({ + tagName: '', + color: null, +}); diff --git a/tests/dummy/app/pods/components/custom-header/template.hbs b/tests/dummy/app/pods/components/custom-header/template.hbs index 53860bf41..4cf5fc26e 100644 --- a/tests/dummy/app/pods/components/custom-header/template.hbs +++ b/tests/dummy/app/pods/components/custom-header/template.hbs @@ -1,5 +1,5 @@ {{! BEGIN-SNIPPET custom-header.hbs }} -
+
Column {{yield}}
{{! END-SNIPPET }} diff --git a/tests/dummy/app/pods/components/custom-row/component.js b/tests/dummy/app/pods/components/custom-row/component.js index fbac6dc7b..308d11d8c 100644 --- a/tests/dummy/app/pods/components/custom-row/component.js +++ b/tests/dummy/app/pods/components/custom-row/component.js @@ -1,5 +1,5 @@ import EmberTableRow from '../../../components/ember-tr'; -import { classNames } from '@ember-decorators/component'; -@classNames('custom-row') -export default class CustomRow extends EmberTableRow {} +export default EmberTableRow.extend({ + classNames: ['custom-row'], +}); diff --git a/tests/dummy/app/pods/docs/guides/body/occlusion/controller.js b/tests/dummy/app/pods/docs/guides/body/occlusion/controller.js index 53539d62b..dc7510ad0 100644 --- a/tests/dummy/app/pods/docs/guides/body/occlusion/controller.js +++ b/tests/dummy/app/pods/docs/guides/body/occlusion/controller.js @@ -1,20 +1,18 @@ import Controller from '@ember/controller'; -import { computed } from '@ember-decorators/object'; +import { computed } from '@ember/object'; -export default class SimpleController extends Controller { +export default Controller.extend({ // BEGIN-SNIPPET docs-example-occlusion.js - @computed - get columns() { + columns: computed(function() { return [ { name: 'A', valuePath: 'A', width: 180 }, { name: 'B', valuePath: 'B', width: 180 }, { name: 'C', valuePath: 'C', width: 180 }, { name: 'D', valuePath: 'D', width: 180 }, ]; - } + }), - @computed - get rows() { + rows: computed(function() { return [ { A: 'A', B: 'B', C: 'C', D: 'D' }, { A: 'A', B: 'B', C: 'C', D: 'D' }, @@ -28,6 +26,6 @@ export default class SimpleController extends Controller { { A: 'A', B: 'B', C: 'C', D: 'D' }, { A: 'A', B: 'B', C: 'C', D: 'D' }, ]; - } + }), // END-SNIPPET -} +}); diff --git a/tests/dummy/app/pods/docs/guides/body/row-selection/controller.js b/tests/dummy/app/pods/docs/guides/body/row-selection/controller.js index e02dd2759..2a32b4aae 100644 --- a/tests/dummy/app/pods/docs/guides/body/row-selection/controller.js +++ b/tests/dummy/app/pods/docs/guides/body/row-selection/controller.js @@ -1,20 +1,18 @@ import Controller from '@ember/controller'; -import { computed } from '@ember-decorators/object'; +import { computed } from '@ember/object'; -export default class SimpleController extends Controller { +export default Controller.extend({ // BEGIN-SNIPPET docs-example-row-selection.js - @computed - get columns() { + columns: computed(function() { return [ { name: 'A', valuePath: 'A', width: 180 }, { name: 'B', valuePath: 'B', width: 180 }, { name: 'C', valuePath: 'C', width: 180 }, { name: 'D', valuePath: 'D', width: 180 }, ]; - } + }), - @computed - get rows() { + rows: computed(function() { return [ { A: 'A', B: 'B', C: 'C', D: 'D' }, { A: 'A', B: 'B', C: 'C', D: 'D' }, @@ -28,20 +26,19 @@ export default class SimpleController extends Controller { { A: 'A', B: 'B', C: 'C', D: 'D' }, { A: 'A', B: 'B', C: 'C', D: 'D' }, ]; - } + }), // END-SNIPPET // BEGIN-SNIPPET docs-example-selected-rows.js - constructor() { - super(...arguments); + init() { + this._super(...arguments); let [rowWithChildren] = this.get('rowWithChildren'); this.preselection = [rowWithChildren]; - } + }, - @computed - get rowWithChildren() { + rowWithChildren: computed(function() { return [ { A: 'A', @@ -53,57 +50,64 @@ export default class SimpleController extends Controller { { A: 'A', B: 'B', C: 'C', D: 'D' }, { A: 'A', B: 'B', C: 'C', D: 'D' }, { A: 'A', B: 'B', C: 'C', D: 'D' }, - ], - }, - ]; - } - // END-SNIPPET - - // BEGIN-SNIPPET docs-example-selection-modes.js - rowSelectionMode = 'multiple'; - checkboxSelectionMode = 'multiple'; - selectingChildrenSelectsParent = true; - - @computed - get rowsWithChildren() { - return [ - { - A: 'A', - B: 'B', - C: 'C', - D: 'D', - - children: [ { A: 'A', B: 'B', C: 'C', D: 'D' }, { A: 'A', B: 'B', C: 'C', D: 'D' }, { A: 'A', B: 'B', C: 'C', D: 'D' }, - ], - }, - { - A: 'A', - B: 'B', - C: 'C', - D: 'D', - - children: [ + { A: 'A', B: 'B', C: 'C', D: 'D' }, + { A: 'A', B: 'B', C: 'C', D: 'D' }, + { A: 'A', B: 'B', C: 'C', D: 'D' }, { A: 'A', B: 'B', C: 'C', D: 'D' }, { A: 'A', B: 'B', C: 'C', D: 'D' }, { A: 'A', B: 'B', C: 'C', D: 'D' }, ], }, - { - A: 'A', + ]; + }), + // END-SNIPPET + + // BEGIN-SNIPPET docs-example-selection-modes.js + rowSelectionMode: 'multiple', + checkboxSelectionMode: 'multiple', + selectingChildrenSelectsParent: true, + + rowsWithChildren: computed(function() { + let makeRow = (id, { children } = { children: [] }) => { + return { + A: `A${id}`, B: 'B', C: 'C', D: 'D', - + children, + }; + }; + return [ + makeRow(1, { children: [ - { A: 'A', B: 'B', C: 'C', D: 'D' }, - { A: 'A', B: 'B', C: 'C', D: 'D' }, - { A: 'A', B: 'B', C: 'C', D: 'D' }, + makeRow(2, { + children: [makeRow(3), makeRow(4), makeRow(5)], + }), + makeRow(6), + makeRow(7), + makeRow(8, { + children: [makeRow(9), makeRow(10), makeRow(11)], + }), ], - }, + }), ]; - } + }), + + currentSelection: computed('demoSelection', function() { + let selection = this.demoSelection; + if (!selection || selection.length === 0) { + return 'Nothing selected'; + } else { + if (Array.isArray(selection)) { + return `Array: [${selection.map(row => row.A).join(',')}]`; + } else { + let row = selection; + return `Single: ${row.A}`; + } + } + }), // END-SNIPPET -} +}); diff --git a/tests/dummy/app/pods/docs/guides/body/row-selection/template.md b/tests/dummy/app/pods/docs/guides/body/row-selection/template.md index 89e20b419..3f8f7de02 100644 --- a/tests/dummy/app/pods/docs/guides/body/row-selection/template.md +++ b/tests/dummy/app/pods/docs/guides/body/row-selection/template.md @@ -5,7 +5,7 @@ table body will activate selection, and you can pass in the `selection` property to control the selection using DDAU: {{#docs-demo as |demo|}} - {{#demo.example}} + {{#demo.example name="docs-example-row-selection"}}
{{! BEGIN-SNIPPET docs-example-row-selection.hbs }} @@ -25,7 +25,7 @@ to control the selection using DDAU: {{demo.snippet label='component.js' name='docs-example-row-selection.js'}} {{/docs-demo}} -# Selected Rows +## Selected Rows `selection` can either be a single row, or a group of rows. Selecting a row also marks all of its children as selected. @@ -62,12 +62,12 @@ in the `selection` group. It makes other tasks much easier though, like finding all of the groups that are selected, and selecting a group manually, external to the table. -# Selection Modes +## Selection Modes There are three different properties you can use to control the behavior of row selection: -1. `checkboxSelectionMode`: This controls the behavior of the checkbox which +1. `checkboxSelectionMode`: This controls the behavior of the checkbox that appears in the first cell of a row. It can be either `multiple`, `single`, or `none`. Checkbox selection is always a group selection - it will always pass an array to `onSelect`. In `multiple` mode it allows more than one checkbox to be @@ -81,14 +81,14 @@ It can be either `multiple`, `single`, or `none`. If it is either `multiple` or marks the row as selected, but is not considered a group selection, so the checkbox will _not_ be checked. -3. `selectingChildrenSelectsParent`: This is a boolean flag which tells toggles -whether or not selecting all of the children of a given row also selects the row +3. `selectingChildrenSelectsParent`: This is a boolean flag that determines +whether selecting all of the children of a given row also selects the row itself. {{#docs-demo as |demo|}} {{#demo.example name='selection-modes'}} {{! BEGIN-SNIPPET docs-example-selection-modes.hbs }} -
+
@@ -99,65 +99,32 @@ itself. @checkboxSelectionMode={{checkboxSelectionMode}} @selectingChildrenSelectsParent={{selectingChildrenSelectsParent}} - @onSelect={{action (mut selection)}} - @selection={{selection}} + @onSelect={{action (mut demoSelection)}} + @selection={{demoSelection}} />
- -
-

rowSelectionMode

- -
- - - - - -
- -

checkboxSelectionMode

- -
- - - - - -
- -

selectingChildrenSelectsParent

- -
- - - -
+
+

Current selection

+
{{currentSelection}}
+
+

rowSelectionMode

+ + + +
+
+

checkboxSelectionMode

+ + + +
+
+

selectingChildrenSelectsParent

+ +
+ {{! END-SNIPPET }} {{/demo.example}} diff --git a/tests/dummy/app/pods/docs/guides/body/rows-and-trees/controller.js b/tests/dummy/app/pods/docs/guides/body/rows-and-trees/controller.js index 9f7cc1a24..58ba8f316 100644 --- a/tests/dummy/app/pods/docs/guides/body/rows-and-trees/controller.js +++ b/tests/dummy/app/pods/docs/guides/body/rows-and-trees/controller.js @@ -1,20 +1,18 @@ import Controller from '@ember/controller'; -import { computed } from '@ember-decorators/object'; +import { computed } from '@ember/object'; -export default class SimpleController extends Controller { +export default Controller.extend({ // BEGIN-SNIPPET docs-example-rows.js - @computed - get columns() { + columns: computed(function() { return [ { name: 'A', valuePath: 'A', width: 180 }, { name: 'B', valuePath: 'B', width: 180 }, { name: 'C', valuePath: 'C', width: 180 }, { name: 'D', valuePath: 'D', width: 180 }, ]; - } + }), - @computed - get rows() { + rows: computed(function() { return [ { A: 'A', B: 'B', C: 'C', D: 'D' }, { A: 'A', B: 'B', C: 'C', D: 'D' }, @@ -28,14 +26,13 @@ export default class SimpleController extends Controller { { A: 'A', B: 'B', C: 'C', D: 'D' }, { A: 'A', B: 'B', C: 'C', D: 'D' }, ]; - } + }), // END-SNIPPET // BEGIN-SNIPPET docs-example-tree-rows.js - treeEnabled = true; + treeEnabled: true, - @computed - get rowsWithChildren() { + rowsWithChildren: computed(function() { return [ { A: 'A', @@ -74,14 +71,13 @@ export default class SimpleController extends Controller { ], }, ]; - } + }), // END-SNIPPET // BEGIN-SNIPPET docs-example-rows-with-collapse.js - collapseEnabled = true; + collapseEnabled: true, - @computed - get rowsWithCollapse() { + rowsWithCollapse: computed(function() { return [ { A: 'A', @@ -123,6 +119,6 @@ export default class SimpleController extends Controller { ], }, ]; - } + }), // END-SNIPPET -} +}); diff --git a/tests/dummy/app/pods/docs/guides/body/rows-and-trees/template.md b/tests/dummy/app/pods/docs/guides/body/rows-and-trees/template.md index 49bc2fd2f..917a6b3d6 100644 --- a/tests/dummy/app/pods/docs/guides/body/rows-and-trees/template.md +++ b/tests/dummy/app/pods/docs/guides/body/rows-and-trees/template.md @@ -49,7 +49,7 @@ the table body. {{#docs-demo as |demo|}} {{#demo.example name="trees"}} {{! BEGIN-SNIPPET docs-example-tree-rows.hbs }} -
+