├── .bowerrc ├── .editorconfig ├── .ember-cli ├── .eslintignore ├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .ls-lint.yml ├── .npmignore ├── .npmrc ├── .prettierrc.js ├── .release-it.json ├── .sass-lint.yml ├── .vscode └── settings.json ├── .watchmanconfig ├── .yarnrc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── addon-test-support ├── helpers │ ├── element.js │ └── mouse.js ├── index.js └── pages │ ├── -private │ ├── ember-table-body.js │ ├── ember-table-footer.js │ ├── ember-table-header.js │ └── ember-table-loading-more.js │ └── ember-table.js ├── addon ├── -private │ ├── collapse-tree.js │ ├── column-tree.js │ ├── meta-cache.js │ ├── sticky │ │ └── table-sticky-polyfill.js │ └── utils │ │ ├── array.js │ │ ├── default-to.js │ │ ├── element.js │ │ ├── ember.js │ │ ├── observer.js │ │ ├── reorder-indicators.js │ │ └── sort.js ├── components │ ├── -private │ │ ├── base-table-cell.js │ │ ├── row-wrapper.js │ │ └── scroll-indicators │ │ │ ├── component.js │ │ │ └── template.hbs │ ├── ember-table-loading-more │ │ └── component.js │ ├── ember-table-simple-checkbox.js │ ├── ember-table │ │ ├── component.js │ │ └── template.hbs │ ├── ember-tbody │ │ ├── component.js │ │ └── template.hbs │ ├── ember-td │ │ ├── component.js │ │ └── template.hbs │ ├── ember-tfoot │ │ ├── component.js │ │ └── template.hbs │ ├── ember-th │ │ ├── component.js │ │ ├── resize-handle │ │ │ ├── component.js │ │ │ └── template.hbs │ │ ├── sort-indicator │ │ │ ├── component.js │ │ │ └── template.hbs │ │ └── template.hbs │ ├── ember-thead │ │ ├── component.js │ │ └── template.hbs │ └── ember-tr │ │ ├── component.js │ │ └── template.hbs └── styles │ └── addon.css ├── app ├── components │ ├── ember-table-loading-more.js │ ├── ember-table-private │ │ ├── row-wrapper.js │ │ └── scroll-indicators.js │ ├── ember-table-simple-checkbox.js │ ├── ember-table.js │ ├── ember-tbody.js │ ├── ember-td.js │ ├── ember-tfoot.js │ ├── ember-th.js │ ├── ember-th │ │ ├── resize-handle.js │ │ └── sort-indicator.js │ ├── ember-thead.js │ └── ember-tr.js └── styles │ └── ember-table │ └── default.scss ├── config ├── addon-docs.js └── deploy.js ├── ember-cli-build.js ├── index.js ├── jsconfig.json ├── package.json ├── testem.js ├── tests ├── acceptance │ └── docs-test.js ├── dummy │ ├── app │ │ ├── app.js │ │ ├── components │ │ │ ├── custom-cell │ │ │ │ ├── component.js │ │ │ │ └── template.hbs │ │ │ ├── custom-header │ │ │ │ ├── component.js │ │ │ │ └── template.hbs │ │ │ ├── custom-row │ │ │ │ └── component.js │ │ │ ├── examples │ │ │ │ ├── aborting-a-selection │ │ │ │ │ ├── component.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── fixed-columns-dynamic │ │ │ │ │ ├── component.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── infinite-scroll │ │ │ │ │ ├── component.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── row-selection │ │ │ │ │ ├── component.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── selected-rows │ │ │ │ │ ├── component.js │ │ │ │ │ └── template.hbs │ │ │ │ ├── selection-modes │ │ │ │ │ ├── component.js │ │ │ │ │ └── template.hbs │ │ │ │ └── sorting-empty-values │ │ │ │ │ ├── component.js │ │ │ │ │ └── template.hbs │ │ │ ├── index-content │ │ │ │ └── template.md │ │ │ └── main │ │ │ │ └── table-meta-data │ │ │ │ └── cell-selection │ │ │ │ ├── component.js │ │ │ │ └── template.hbs │ │ ├── controllers │ │ │ ├── docs │ │ │ │ └── guides │ │ │ │ │ ├── body │ │ │ │ │ ├── occlusion.js │ │ │ │ │ ├── row-selection.js │ │ │ │ │ └── rows-and-trees.js │ │ │ │ │ ├── header │ │ │ │ │ ├── columns.js │ │ │ │ │ ├── fixed-columns.js │ │ │ │ │ ├── scroll-indicators.js │ │ │ │ │ ├── size-constraints.js │ │ │ │ │ ├── sorting.js │ │ │ │ │ └── subcolumns.js │ │ │ │ │ └── main │ │ │ │ │ ├── basic-table.js │ │ │ │ │ ├── styling-the-table.js │ │ │ │ │ ├── table-customization.js │ │ │ │ │ └── table-meta-data.js │ │ │ └── scenarios │ │ │ │ ├── performance.js │ │ │ │ └── simple.js │ │ ├── index.html │ │ ├── router.js │ │ ├── routes │ │ │ └── index.js │ │ ├── styles │ │ │ ├── app.scss │ │ │ ├── fixes.scss │ │ │ └── tables.scss │ │ ├── templates │ │ │ ├── .gitkeep │ │ │ ├── application.hbs │ │ │ ├── docs.hbs │ │ │ ├── docs │ │ │ │ ├── guides │ │ │ │ │ ├── body │ │ │ │ │ │ ├── occlusion.md │ │ │ │ │ │ ├── row-selection.md │ │ │ │ │ │ └── rows-and-trees.md │ │ │ │ │ ├── header │ │ │ │ │ │ ├── column-keys.md │ │ │ │ │ │ ├── columns.md │ │ │ │ │ │ ├── fixed-columns.md │ │ │ │ │ │ ├── scroll-indicators.md │ │ │ │ │ │ ├── size-constraints.md │ │ │ │ │ │ ├── sorting.md │ │ │ │ │ │ └── subcolumns.md │ │ │ │ │ └── main │ │ │ │ │ │ ├── basic-table.md │ │ │ │ │ │ ├── infinite-scroll.md │ │ │ │ │ │ ├── styling-the-table.md │ │ │ │ │ │ ├── table-customization.md │ │ │ │ │ │ └── table-meta-data.md │ │ │ │ ├── index.md │ │ │ │ ├── quickstart.md │ │ │ │ ├── testing │ │ │ │ │ └── test-page.md │ │ │ │ └── why-ember-table.md │ │ │ ├── not-found.hbs │ │ │ ├── scenarios.hbs │ │ │ └── scenarios │ │ │ │ ├── blank.hbs │ │ │ │ ├── performance.hbs │ │ │ │ └── simple.hbs │ │ └── utils │ │ │ ├── base-26.js │ │ │ └── generators.js │ ├── config │ │ ├── ember-try.js │ │ ├── environment.js │ │ ├── optional-features.json │ │ └── targets.js │ └── public │ │ ├── 404.html │ │ ├── assets │ │ └── images │ │ │ ├── checkmark.svg │ │ │ ├── favicon.ico │ │ │ ├── logo.png │ │ │ └── spinner.gif │ │ └── robots.txt ├── helpers │ ├── .gitkeep │ ├── base-26.js │ ├── generate-table.js │ ├── module.js │ └── warn-handlers.js ├── index.html ├── integration │ └── components │ │ ├── basic-test.js │ │ ├── cell-test.js │ │ ├── footer-test.js │ │ ├── headers │ │ ├── cell-test.js │ │ ├── ember-th-test.js │ │ ├── ember-thead-test.js │ │ ├── main-test.js │ │ ├── reorder-test.js │ │ ├── resize-handle-test.js │ │ ├── resize-test.js │ │ └── sort-indicator-test.js │ │ ├── loading-more-test.js │ │ ├── meta-test.js │ │ ├── row-test.js │ │ ├── scroll-indicators-test.js │ │ ├── selection-test.js │ │ ├── sort-test.js │ │ └── tree-test.js ├── test-helper.js └── unit │ ├── -private │ ├── collapse-tree-test.js │ ├── column-tree-test.js │ ├── element-utils-test.js │ ├── meta-cache-test.js │ └── table-sticky-polyfill-test.js │ └── .gitkeep ├── tsconfig.json ├── types ├── components │ ├── ember-table-loading-more │ │ └── component.d.ts │ ├── ember-table │ │ └── component.d.ts │ ├── ember-tbody │ │ └── component.d.ts │ ├── ember-td │ │ └── component.d.ts │ ├── ember-tfoot │ │ └── component.d.ts │ ├── ember-th │ │ ├── component.d.ts │ │ ├── resize-handle │ │ │ └── component.d.ts │ │ └── sort-indicator │ │ │ └── component.d.ts │ ├── ember-thead │ │ └── component.d.ts │ └── ember-tr │ │ └── component.d.ts ├── index.d.ts └── template-registry.d.ts ├── vendor └── .gitkeep └── yarn.lock /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | 4 | # compiled output 5 | /declarations/ 6 | /dist/ 7 | 8 | # misc 9 | /coverage/ 10 | !.* 11 | .*/ 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@babel/eslint-parser', 4 | parserOptions: { 5 | ecmaVersion: 'latest', 6 | sourceType: 'module', 7 | requireConfigFile: false, 8 | babelOptions: { 9 | plugins: [ 10 | [ 11 | '@babel/plugin-proposal-decorators', 12 | { 13 | decoratorsBeforeExport: true, 14 | }, 15 | ], 16 | ], 17 | }, 18 | }, 19 | plugins: ['ember'], 20 | extends: [ 21 | 'eslint:recommended', 22 | 'plugin:ember/recommended', 23 | 'plugin:prettier/recommended', 24 | '@addepar', 25 | '@addepar/eslint-config/ember', 26 | ], 27 | env: { 28 | browser: true, 29 | }, 30 | rules: { 31 | 'ember/no-classic-classes': 'off', // 41 errors 32 | 'ember/no-classic-components': 'off', // 20 errors 33 | 'ember/no-component-lifecycle-hooks': 'off', // 12 errors 34 | 'ember/no-get': 'off', // 406 errors 35 | 'ember/no-runloop': 'off', // 26 errors 36 | 'ember/require-computed-property-dependencies': 'off', // 23 errors 37 | 'ember/require-tagless-components': 'off', // 14 errors 38 | 'ember-best-practices/require-dependent-keys': 'off', // 56 errors 39 | 'no-restricted-imports': [ 40 | 'error', 41 | { 42 | paths: [ 43 | { 44 | name: '@ember/object', 45 | importNames: ['observer'], 46 | message: 'For compatibility, use `import { observer } from "-private/utils/observer"`', 47 | }, 48 | { 49 | name: '@ember/object/observers', 50 | importNames: ['addObserver', 'removeObserver'], 51 | message: 52 | 'For compatibility, use `import { addObserver, removeObserver } from "-private/utils/observer"`', 53 | }, 54 | ], 55 | }, 56 | ], 57 | }, 58 | overrides: [ 59 | // node files 60 | { 61 | files: [ 62 | './.eslintrc.js', 63 | './.prettierrc.js', 64 | './.stylelintrc.js', 65 | './.template-lintrc.js', 66 | './ember-cli-build.js', 67 | './index.js', 68 | './testem.js', 69 | './blueprints/*/index.js', 70 | './config/**/*.js', 71 | './tests/dummy/config/**/*.js', 72 | ], 73 | parserOptions: { 74 | sourceType: 'script', 75 | }, 76 | env: { 77 | browser: false, 78 | node: true, 79 | }, 80 | extends: ['plugin:n/recommended'], 81 | }, 82 | { 83 | // test files 84 | files: ['tests/**/*-test.{js,ts}'], 85 | extends: ['plugin:qunit/recommended'], 86 | }, 87 | ], 88 | }; 89 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 2 | 3 | version: 2 4 | updates: 5 | - package-ecosystem: "npm" # See documentation for possible values 6 | directory: "/" # Location of package manifests 7 | versioning-strategy: lockfile-only 8 | schedule: 9 | interval: "weekly" 10 | open-pull-requests-limit: 4 11 | labels: 12 | - "dependencies" 13 | groups: 14 | babel: 15 | applies-to: version-updates 16 | patterns: 17 | - "@babel/*" 18 | embroider: 19 | applies-to: version-updates 20 | patterns: 21 | - "@embroider/*" 22 | eslint: 23 | applies-to: version-updates 24 | patterns: 25 | - "@eslint/*" 26 | - eslint 27 | - eslint-* 28 | - package-ecosystem: "github-actions" 29 | # Workflow files stored in the 30 | # default location of `.github/workflows` 31 | directory: "/" 32 | schedule: 33 | interval: "weekly" 34 | open-pull-requests-limit: 2 35 | labels: 36 | - "dependencies" 37 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Ember-Table CI 2 | on: 3 | pull_request: 4 | types: [opened, synchronize, reopened, ready_for_review] 5 | 6 | jobs: 7 | test: 8 | name: Run Tests 9 | runs-on: ubuntu-latest 10 | timeout-minutes: 7 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Use Volta 17 | uses: volta-cli/action@v4 18 | 19 | - name: Node Modules Cache 20 | id: cache-npm 21 | uses: actions/cache@v4 22 | with: 23 | path: '**/node_modules' 24 | key: ci-yarn-${{ hashFiles('**/yarn.lock') }} 25 | 26 | - name: Install Dependencies 27 | if: steps.cache-npm.outputs.cache-hit != 'true' 28 | run: yarn install --frozen-lockfile 29 | 30 | - name: Lint 31 | run: yarn lint 32 | 33 | - name: Run Tests 34 | run: yarn test 35 | env: 36 | CI: true 37 | 38 | test-ember-try: 39 | name: Run Tests 40 | runs-on: ubuntu-latest 41 | timeout-minutes: 10 42 | 43 | strategy: 44 | fail-fast: false 45 | matrix: 46 | ember-version: 47 | [ 48 | embroider-safe, 49 | # embroider-optimized, 50 | ember-lts-3.28, 51 | ember-lts-4.4, 52 | ember-lts-4.12, 53 | ember-lts-5.4, 54 | ember-lts-5.12, 55 | ember-release, 56 | # ember-production, 57 | ember-default-docs 58 | ] 59 | 60 | steps: 61 | - name: Checkout 62 | uses: actions/checkout@v4 63 | 64 | - name: Use Volta 65 | uses: volta-cli/action@v4 66 | 67 | - name: Stash yarn.lock for cache key 68 | run: cp yarn.lock __cache-key 69 | 70 | - name: Node Modules Cache 71 | id: cache-npm 72 | uses: actions/cache@v4 73 | with: 74 | path: | 75 | node_modules 76 | package.json 77 | yarn.lock 78 | __env 79 | key: ci-yarn-v3-${{ matrix.ember-version }}-${{ hashFiles('tests/dummy/config/ember-try.js', '__cache-key') }} 80 | restore-keys: | 81 | ci-yarn-${{ hashFiles('yarn.lock') }} 82 | 83 | - name: Install Dependencies 84 | if: steps.cache-npm.outputs.cache-hit != 'true' 85 | run: yarn install --frozen-lockfile 86 | 87 | - name: Run Tests 88 | run: yarn test:ember-compatibility ${{ matrix.ember-version }} --skip-cleanup 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.DS_Store 13 | /.eslintcache 14 | /.sass-cache 15 | /connect.lock 16 | /coverage/* 17 | /libpeerconnection.log 18 | npm-debug.log* 19 | yarn-error.log 20 | testem.log 21 | -------------------------------------------------------------------------------- /.ls-lint.yml: -------------------------------------------------------------------------------- 1 | # Docs on ls-lint: https://ls-lint.org/ 2 | # The linter can be run locally by `npx ls-lint [files]` 3 | # Pass `--debug` flag to see the list of all files/dirs that are linted 4 | ls: 5 | .js: kebab-case 6 | .d.ts: kebab-case 7 | .ts: kebab-case 8 | .html: kebab-case 9 | .md: kebab-case | screamingsnakecase 10 | .css: kebab-case 11 | .scss: kebab-case 12 | .dir: kebab-case 13 | 14 | ignore: 15 | - .git 16 | - node_modules 17 | - dist 18 | - ./.* 19 | - yarn.lock 20 | - yarn-error.log 21 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /bower_components 2 | /dist 3 | /tests 4 | /tmp 5 | **/.gitkeep 6 | .bowerrc 7 | .editorconfig 8 | .ember-cli 9 | .eslintcache 10 | .eslintrc.js 11 | .gitignore 12 | .watchmanconfig 13 | .travis.yml 14 | bower.json 15 | ember-cli-build.js 16 | testem.js 17 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @addepar:registry = "https://registry.npmjs.org/" 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line n/no-unpublished-require 2 | module.exports = require('@addepar/prettier-config'); 3 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "git": { 3 | "tagName": "v${version}" 4 | }, 5 | "github": { 6 | "release": false 7 | }, 8 | "hooks": { 9 | "after:release": "yarn run docs:deploy", 10 | "after:bump": "npx auto-changelog -p" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.sass-lint.yml: -------------------------------------------------------------------------------- 1 | options: 2 | config-file: node_modules/@addepar/sass-lint-config/config.yml 3 | rules: 4 | # Name Formats 5 | class-name-format: 6 | - 2 7 | - 8 | convention: strictbem 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.enable": true 3 | } -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry "https://registry.npmjs.org/" 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2017-2020 Addepar, Inc. All Rights Reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | 3. "Addepar" or "Addepar, Inc." may not be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY ADDEPAR, INC. "AS IS" AND ANY EXPRESS OR IMPLIED 17 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 18 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 19 | EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 20 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 21 | OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 22 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 23 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 24 | IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY 25 | OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /addon-test-support/helpers/element.js: -------------------------------------------------------------------------------- 1 | import { getScale } from 'ember-table/-private/utils/element'; 2 | 3 | export { getScale }; 4 | -------------------------------------------------------------------------------- /addon-test-support/helpers/mouse.js: -------------------------------------------------------------------------------- 1 | import { triggerEvent } from '@ember/test-helpers'; 2 | 3 | export async function mouseDown(target, x, y) { 4 | await triggerEvent(target, 'pointerdown', { 5 | clientX: x, 6 | clientY: y, 7 | button: 0, 8 | }); 9 | } 10 | 11 | export async function mouseMove(target, x, y) { 12 | await triggerEvent(target, 'pointermove', { 13 | clientX: x, 14 | clientY: y, 15 | button: 0, 16 | }); 17 | } 18 | 19 | export async function mouseUp(target, x, y) { 20 | await triggerEvent(target, 'pointerup', { 21 | clientX: x, 22 | clientY: y, 23 | button: 0, 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /addon-test-support/index.js: -------------------------------------------------------------------------------- 1 | import TablePage from './pages/ember-table'; 2 | import registerWaiter from 'ember-raf-scheduler/test-support/register-waiter'; 3 | import { setSetupRowCountForTest } from 'ember-table/components/ember-tbody/component'; 4 | import { setupTHeadForTest } from 'ember-table/components/ember-thead/component'; 5 | import { setSimpleCheckboxForTest } from 'ember-table/components/ember-td/component'; 6 | 7 | function setupForTest() { 8 | registerWaiter(); 9 | setSetupRowCountForTest(true); 10 | setupTHeadForTest(true); 11 | setSimpleCheckboxForTest(true); 12 | } 13 | 14 | export { TablePage, setupForTest }; 15 | -------------------------------------------------------------------------------- /addon-test-support/pages/-private/ember-table-body.js: -------------------------------------------------------------------------------- 1 | import PageObject, { 2 | alias, 3 | triggerable, 4 | collection, 5 | hasClass, 6 | property, 7 | } from 'ember-classy-page-object'; 8 | import { findElement } from 'ember-classy-page-object/extend'; 9 | 10 | import { click } from '@ember/test-helpers'; 11 | 12 | /** 13 | * Page object for single table `td` cell; also used by the footer page object 14 | * to represent footer cells. 15 | */ 16 | export const BodyCell = PageObject.extend({ 17 | scope: 'td', 18 | 19 | doubleClick: triggerable('dblclick'), 20 | 21 | isFirstColumn: hasClass('is-first-column'), 22 | isLastColumn: hasClass('is-last-column'), 23 | isSlack: hasClass('is-slack'), 24 | isResizing: hasClass('is-resizing'), 25 | }); 26 | 27 | export default PageObject.extend({ 28 | scope: 'tbody', 29 | 30 | /** 31 | Returns the height of the entire tbody element. 32 | */ 33 | get height() { 34 | return findElement(this).offsetHeight; 35 | }, 36 | 37 | /** 38 | Returns the number of rows in the body. 39 | */ 40 | get rowCount() { 41 | let element = findElement(this); 42 | if (!element.hasAttribute('data-test-row-count')) { 43 | throw new Error( 44 | 'data-test-row-count attribute not found on the Ember Table tbody. Perhaps you need to run setupForTest? See https://github.com/Addepar/ember-table/blob/master/README.md' 45 | ); 46 | } 47 | return Number(element.getAttribute('data-test-row-count')); 48 | }, 49 | 50 | /** 51 | List of rows in table body. Each of property/function in this collections is the property/func 52 | of a single row selected by using calling rows.objectAt(index). 53 | */ 54 | rows: collection({ 55 | scope: 'tr', 56 | 57 | /** 58 | List of all cells for the selected row. 59 | */ 60 | cells: collection('td:not([data-test-ember-table-slack])', BodyCell), 61 | 62 | /** 63 | Slack cell from this row, if present. 64 | */ 65 | slackCell: BodyCell.extend({ 66 | scope: 'td[data-test-ember-table-slack]', 67 | }), 68 | 69 | /** 70 | Returns the height of selected row. 71 | */ 72 | get height() { 73 | return findElement(this).offsetHeight; 74 | }, 75 | 76 | checkbox: { 77 | scope: '[data-test-select-row]', 78 | isChecked: property('checked'), 79 | 80 | async clickWith(options) { 81 | await click(findElement(this), options); 82 | }, 83 | }, 84 | 85 | checkboxContainer: { 86 | scope: '[data-test-select-row-container]', 87 | 88 | isHidden: hasClass('et-speech-only'), 89 | }, 90 | 91 | toggleSelect: alias('checkbox.click'), 92 | 93 | collapse: { 94 | scope: '[data-test-collapse-row]', 95 | isCollapsed: property('checked'), 96 | }, 97 | 98 | toggleCollapse: alias('collapse.click'), 99 | 100 | isSelected: hasClass('is-selected'), 101 | 102 | /** 103 | Helper function to click with options like the meta key and ctrl key set 104 | 105 | @param {Object} options - click event options 106 | */ 107 | async clickWith(options) { 108 | await click(findElement(this), options); 109 | }, 110 | 111 | doubleClick: triggerable('dblclick'), 112 | }), 113 | 114 | /** 115 | A shortcut to return cell page object specified by row & column indexes. 116 | */ 117 | getCell(rowIndex, columnIndex) { 118 | return this.rows.objectAt(rowIndex).cells.objectAt(columnIndex); 119 | }, 120 | }); 121 | -------------------------------------------------------------------------------- /addon-test-support/pages/-private/ember-table-footer.js: -------------------------------------------------------------------------------- 1 | import { collection } from 'ember-classy-page-object'; 2 | import EmberTableBody, { BodyCell } from './ember-table-body'; 3 | 4 | export default EmberTableBody.extend({ 5 | scope: 'tfoot', 6 | 7 | footers: collection('td:not([data-test-ember-table-slack])', BodyCell), 8 | slackFooters: collection('td[data-test-ember-table-slack]', BodyCell), 9 | }); 10 | -------------------------------------------------------------------------------- /addon-test-support/pages/-private/ember-table-loading-more.js: -------------------------------------------------------------------------------- 1 | import PageObject from 'ember-classy-page-object'; 2 | import { findElement } from 'ember-classy-page-object/extend'; 3 | 4 | /** 5 | * Page object for "Loading More" component that renders beneath the body. 6 | */ 7 | export default PageObject.extend({ 8 | scope: '[data-test-ember-table-loading-more]', 9 | 10 | /** 11 | * Returns the pixel value of the `translateX` transform applied to center 12 | * the indicator in the scroll viewport. 13 | */ 14 | get translateX() { 15 | let transform = findElement(this).style.transform; 16 | let result = transform.match(/translateX\((\d+)px\)/); 17 | return result ? parseInt(result[1]) : 0; 18 | }, 19 | 20 | /** 21 | * Returns if the LoadingMore component is occupying space in the layout. 22 | */ 23 | get isIncludedInLayout() { 24 | return findElement(this).style.display !== 'none'; 25 | }, 26 | 27 | /** 28 | * Returns if the LoadingMore component and user block is visible. 29 | */ 30 | get isShown() { 31 | return findElement(this).style.visibility !== 'hidden'; 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /addon/-private/meta-cache.js: -------------------------------------------------------------------------------- 1 | import { get } from '@ember/object'; 2 | 3 | export function getOrCreate(obj, cache, Class) { 4 | if (cache.has(obj) === false) { 5 | cache.set(obj, Class.create ? Class.create() : new Class()); 6 | } 7 | 8 | return cache.get(obj); 9 | } 10 | 11 | /** 12 | * Substitute for `Map` that allows non-identical object keys to share 13 | * identical values by specifying a key path for the associating keys. 14 | * 15 | * If no key path is specified, it behaves like a `Map`. 16 | * 17 | * @class MetaCache 18 | * @constructor 19 | * @param {Object} options 20 | */ 21 | export default class MetaCache { 22 | constructor({ keyPath } = {}) { 23 | this.keyPath = keyPath; 24 | 25 | // in order to prevent memory leaks, we need to be able to clean the cache 26 | // manually when the table is destroyed or updated; this is why we use a 27 | // Map instead of WeakMap 28 | this._map = new Map(); 29 | } 30 | 31 | get(obj) { 32 | let key = this._keyFor(obj); 33 | let entry = this._map.get(key); 34 | return entry ? entry[1] : entry; 35 | } 36 | 37 | getOrCreate(obj, Class) { 38 | return getOrCreate(obj, this, Class); 39 | } 40 | 41 | set(obj, meta) { 42 | let key = this._keyFor(obj); 43 | this._map.set(key, [obj, meta]); 44 | } 45 | 46 | has(obj) { 47 | let key = this._keyFor(obj); 48 | return this._map.has(key); 49 | } 50 | 51 | delete(obj) { 52 | let key = this._keyFor(obj); 53 | this._map.delete(key); 54 | } 55 | 56 | entries() { 57 | return this._map.values(); 58 | } 59 | 60 | _keyFor(obj) { 61 | // falls back to `obj` as key if a legitimate key cannot be produced 62 | if (!obj || !this.keyPath) { 63 | return obj; 64 | } 65 | 66 | let key = get(obj, this.keyPath); 67 | return key ? key : obj; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /addon/-private/utils/array.js: -------------------------------------------------------------------------------- 1 | import { isArray } from '@ember/array'; 2 | import { assert } from '@ember/debug'; 3 | 4 | /** 5 | Genericizes `objectAt` so it can be run against a normal array or an Ember array 6 | 7 | @param {object|Array} arr 8 | @param {number} index 9 | @return {any} 10 | */ 11 | export function objectAt(arr, index) { 12 | assert( 13 | 'arr must be an instance of a Javascript Array or implement `objectAt`', 14 | isArray(arr) || typeof arr.objectAt === 'function' 15 | ); 16 | 17 | if (typeof arr.objectAt === 'function') { 18 | return arr.objectAt(index); 19 | } 20 | 21 | return arr[index]; 22 | } 23 | 24 | export function splice(items, start, count, ...add) { 25 | if (typeof items.replace === 'function' && typeof items.objectAt === 'function') { 26 | return items.replace(start, count, add); 27 | } 28 | 29 | return items.splice(start, count, ...add); 30 | } 31 | 32 | /** 33 | * Cycle shift an internal [start..end] to [start + 1...end, start]. 34 | */ 35 | export function move(items, start, end) { 36 | let sourceItem = objectAt(items, start); 37 | 38 | splice(items, start, 1); 39 | splice(items, end, 0, sourceItem); 40 | } 41 | -------------------------------------------------------------------------------- /addon/-private/utils/default-to.js: -------------------------------------------------------------------------------- 1 | import { computed } from '@ember/object'; 2 | 3 | let VALUES = new WeakMap(); 4 | 5 | function valuesFor(obj) { 6 | if (!VALUES.has(obj)) { 7 | VALUES.set(obj, Object.create(null)); 8 | } 9 | 10 | return VALUES.get(obj); 11 | } 12 | 13 | export default function defaultTo(defaultValue) { 14 | return computed({ 15 | get(key) { 16 | let values = valuesFor(this); 17 | 18 | if (!(key in values)) { 19 | values[key] = typeof defaultValue === 'function' ? defaultValue() : defaultValue; 20 | } 21 | 22 | return values[key]; 23 | }, 24 | 25 | set(key, value) { 26 | let values = valuesFor(this); 27 | 28 | if (value === undefined) { 29 | values[key] = typeof defaultValue === 'function' ? defaultValue() : defaultValue; 30 | } else { 31 | values[key] = value; 32 | } 33 | 34 | return values[key]; 35 | }, 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /addon/-private/utils/ember.js: -------------------------------------------------------------------------------- 1 | import { notifyPropertyChange as _notifyPropertyChange } from '@ember/object'; 2 | 3 | export const notifyPropertyChange = _notifyPropertyChange; 4 | -------------------------------------------------------------------------------- /addon/-private/utils/observer.js: -------------------------------------------------------------------------------- 1 | import { gte } from 'ember-compatibility-helpers'; 2 | import { assert } from '@ember/debug'; 3 | 4 | // eslint-disable-next-line no-restricted-imports 5 | import { observer as emberObserver } from '@ember/object'; 6 | 7 | import { 8 | // eslint-disable-next-line no-restricted-imports 9 | addObserver as emberAddObserver, 10 | // eslint-disable-next-line no-restricted-imports 11 | removeObserver as emberRemoveObserver, 12 | } from '@ember/object/observers'; 13 | 14 | const USE_ASYNC_OBSERVERS = gte('3.13.0'); 15 | 16 | function asyncObserver(...args) { 17 | let fn = args.pop(); 18 | let dependentKeys = args; 19 | let sync = false; 20 | 21 | // eslint-disable-next-line ember/no-observers 22 | return emberObserver({ dependentKeys, fn, sync }); 23 | } 24 | 25 | function asyncAddObserver(...args) { 26 | let obj, path, target, method; 27 | let sync = false; 28 | obj = args[0]; 29 | path = args[1]; 30 | assert( 31 | `Expected 3 or 4 args for addObserver, got ${args.length}`, 32 | args.length === 3 || args.length === 4 33 | ); 34 | if (args.length === 3) { 35 | target = null; 36 | method = args[2]; 37 | } else if (args.length === 4) { 38 | target = args[2]; 39 | method = args[3]; 40 | } 41 | 42 | // eslint-disable-next-line ember/no-observers 43 | return emberAddObserver(obj, path, target, method, sync); 44 | } 45 | 46 | function asyncRemoveObserver(...args) { 47 | let obj, path, target, method; 48 | let sync = false; 49 | obj = args[0]; 50 | path = args[1]; 51 | assert( 52 | `Expected 3 or 4 args for addObserver, got ${args.length}`, 53 | args.length === 3 || args.length === 4 54 | ); 55 | if (args.length === 3) { 56 | target = null; 57 | method = args[2]; 58 | } else { 59 | target = args[2]; 60 | method = args[3]; 61 | } 62 | return emberRemoveObserver(obj, path, target, method, sync); 63 | } 64 | 65 | export const observer = USE_ASYNC_OBSERVERS ? asyncObserver : emberObserver; 66 | export const addObserver = USE_ASYNC_OBSERVERS ? asyncAddObserver : emberAddObserver; 67 | export const removeObserver = emberRemoveObserver ? asyncRemoveObserver : emberRemoveObserver; 68 | -------------------------------------------------------------------------------- /addon/-private/utils/reorder-indicators.js: -------------------------------------------------------------------------------- 1 | import { getOuterClientRect, getInnerClientRect } from './element'; 2 | 3 | function createElement(mainClass, dimensions) { 4 | let element = document.createElement('div'); 5 | 6 | element.classList.add(mainClass); 7 | 8 | for (let key in dimensions) { 9 | element.style[key] = `${dimensions[key]}px`; 10 | } 11 | 12 | return element; 13 | } 14 | 15 | class ReorderIndicator { 16 | constructor(container, scale, element, bounds, mainClass, child) { 17 | this.container = container; 18 | this.element = element; 19 | this.bounds = bounds; 20 | this.child = child; 21 | 22 | let scrollTop = this.container.scrollTop; 23 | let scrollLeft = this.container.scrollLeft; 24 | 25 | let { top: containerTop, left: containerLeft } = getInnerClientRect(this.container, scale); 26 | 27 | let { top: elementTop, left: elementLeft, width: elementWidth } = getOuterClientRect( 28 | this.element 29 | ); 30 | 31 | let top = (elementTop - containerTop) * scale + scrollTop; 32 | let left = (elementLeft - containerLeft) * scale + scrollLeft; 33 | let width = elementWidth * scale; 34 | 35 | this.originLeft = left; 36 | this.indicatorElement = createElement(mainClass, { top, left, width }); 37 | 38 | if (child) { 39 | this.indicatorElement.appendChild(child); 40 | } 41 | 42 | this.container.appendChild(this.indicatorElement); 43 | this._left = left; 44 | } 45 | 46 | destroy() { 47 | this.container.removeChild(this.indicatorElement); 48 | } 49 | 50 | set width(newWidth) { 51 | this.indicatorElement.style.width = `${newWidth}px`; 52 | } 53 | 54 | get left() { 55 | return this._left; 56 | } 57 | 58 | set left(newLeft) { 59 | let { leftBound, rightBound } = this.bounds; 60 | 61 | let width = this.indicatorElement.offsetWidth; 62 | 63 | if (newLeft < leftBound) { 64 | newLeft = leftBound; 65 | } else if (newLeft + width > rightBound) { 66 | newLeft = rightBound - width; 67 | } 68 | 69 | if (newLeft < this.originLeft) { 70 | this.indicatorElement.classList.remove('et-reorder-direction-right'); 71 | this.indicatorElement.classList.add('et-reorder-direction-left'); 72 | } else { 73 | this.indicatorElement.classList.remove('et-reorder-direction-left'); 74 | this.indicatorElement.classList.add('et-reorder-direction-right'); 75 | } 76 | 77 | this.indicatorElement.style.left = `${newLeft}px`; 78 | this._left = newLeft; 79 | } 80 | } 81 | 82 | export class MainIndicator extends ReorderIndicator { 83 | constructor(container, scale, element, bounds) { 84 | let child = element.cloneNode(true); 85 | 86 | super(container, scale, element, bounds, 'et-reorder-main-indicator', child); 87 | } 88 | } 89 | 90 | export class DropIndicator extends ReorderIndicator { 91 | constructor(container, scale, element, bounds) { 92 | super(container, scale, element, bounds, 'et-reorder-drop-indicator'); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /addon/-private/utils/sort.js: -------------------------------------------------------------------------------- 1 | import { compare, isNone } from '@ember/utils'; 2 | import { get } from '@ember/object'; 3 | 4 | function merge(left, right, comparator) { 5 | let mergedArray = []; 6 | let leftIndex = 0; 7 | let rightIndex = 0; 8 | 9 | while (leftIndex < left.length && rightIndex < right.length) { 10 | let comparison = comparator(left[leftIndex], right[rightIndex]); 11 | 12 | if (comparison <= 0) { 13 | mergedArray.push(left[leftIndex]); 14 | leftIndex++; 15 | } else { 16 | mergedArray.push(right[rightIndex]); 17 | rightIndex++; 18 | } 19 | } 20 | 21 | if (leftIndex < left.length) { 22 | mergedArray.splice(mergedArray.length, 0, ...left.slice(leftIndex)); 23 | } 24 | 25 | if (rightIndex < right.length) { 26 | mergedArray.splice(mergedArray.length, 0, ...right.slice(rightIndex)); 27 | } 28 | 29 | return mergedArray; 30 | } 31 | 32 | /** 33 | * An implementation of the standard merge sort algorithm. 34 | * 35 | * This is necessary because we need a stable sorting algorithm that accepts 36 | * a general comparator. The built in sort function and Ember's sort functions 37 | * are not stable, and `_.sortBy` doesn't take a general comparator. Ideally 38 | * lodash would add a `_.sort` function whose API would mimic this function's. 39 | * 40 | * @param {Array} array The array to be sorted 41 | * @param {Comparator} comparator The comparator function to compare elements with. 42 | * @returns {Array} A sorted array 43 | */ 44 | export function mergeSort(array, comparator = compare) { 45 | if (array.length <= 1) { 46 | return array; 47 | } 48 | 49 | let middleIndex = Math.floor(array.length / 2); 50 | let leftArray = mergeSort(array.slice(0, middleIndex), comparator); 51 | let rightArray = mergeSort(array.slice(middleIndex), comparator); 52 | 53 | return merge(leftArray, rightArray, comparator); 54 | } 55 | 56 | export function sortMultiple(itemA, itemB, sorts, compare, sortEmptyLast) { 57 | let compareValue; 58 | 59 | for (let { valuePath, isAscending } of sorts) { 60 | let valueA = get(itemA, valuePath); 61 | let valueB = get(itemB, valuePath); 62 | 63 | // The option only influences the outcome of an ascending sort. 64 | if (sortEmptyLast) { 65 | sortEmptyLast = isAscending; 66 | } 67 | 68 | compareValue = isAscending 69 | ? compare(valueA, valueB, sortEmptyLast) 70 | : -compare(valueA, valueB, sortEmptyLast); 71 | 72 | if (compareValue !== 0) { 73 | break; 74 | } 75 | } 76 | 77 | return compareValue; 78 | } 79 | 80 | function isExactlyNaN(value) { 81 | return typeof value === 'number' && isNaN(value); 82 | } 83 | 84 | function isEmptyString(value) { 85 | return typeof value === 'string' && value === ''; 86 | } 87 | 88 | function isEmpty(value) { 89 | return isNone(value) || isExactlyNaN(value) || isEmptyString(value); 90 | } 91 | 92 | function orderEmptyValues(itemA, itemB, sortEmptyLast) { 93 | let aIsEmpty = isEmpty(itemA); 94 | let bIsEmpty = isEmpty(itemB); 95 | let less = -1; 96 | let more = 1; 97 | 98 | if (sortEmptyLast) { 99 | less = 1; 100 | more = -1; 101 | } 102 | 103 | if (aIsEmpty && !bIsEmpty) { 104 | return less; 105 | } else if (bIsEmpty && !aIsEmpty) { 106 | return more; 107 | } else if (isNone(itemA) && isExactlyNaN(itemB)) { 108 | return less; 109 | } else if (isExactlyNaN(itemA) && isNone(itemB)) { 110 | return more; 111 | } else { 112 | return 0; 113 | } 114 | } 115 | 116 | export function compareValues(itemA, itemB, sortEmptyLast) { 117 | if (isEmpty(itemA) || isEmpty(itemB)) { 118 | return orderEmptyValues(itemA, itemB, sortEmptyLast); 119 | } 120 | 121 | return compare(itemA, itemB); 122 | } 123 | -------------------------------------------------------------------------------- /addon/components/-private/base-table-cell.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { equal, readOnly } from '@ember/object/computed'; 3 | import { observer } from '../../-private/utils/observer'; 4 | import { scheduleOnce } from '@ember/runloop'; 5 | import { computed } from '@ember/object'; 6 | 7 | export default Component.extend({ 8 | // Provided by subclasses 9 | columnMeta: null, 10 | columnValue: null, 11 | 12 | attributeBindings: ['slackAttribute:data-test-ember-table-slack'], 13 | classNameBindings: [ 14 | 'isFirstColumn', 15 | 'isLastColumn', 16 | 'isFixedLeft', 17 | 'isFixedRight', 18 | 'textAlign', 19 | 'isSlack', 20 | ], 21 | 22 | isFirstColumn: equal('columnMeta.index', 0), 23 | isLastColumn: readOnly('columnMeta.isLastRendered'), 24 | isFixedLeft: equal('columnMeta.isFixed', 'left'), 25 | isFixedRight: equal('columnMeta.isFixed', 'right'), 26 | isSlack: readOnly('columnMeta.isSlack'), 27 | 28 | // prevents `data-test-ember-table-slack="false"` on non-slack cells in Ember 2.4 29 | slackAttribute: computed('isSlack', function() { 30 | return this.get('isSlack') ? true : null; 31 | }), 32 | 33 | /** 34 | Indicates the text alignment of this cell 35 | */ 36 | textAlign: computed('columnValue.textAlign', function() { 37 | let textAlign = this.get('columnValue.textAlign'); 38 | 39 | if (['left', 'center', 'right'].includes(textAlign)) { 40 | return `ember-table__text-align-${textAlign}`; 41 | } 42 | 43 | return null; 44 | }), 45 | 46 | // eslint-disable-next-line 47 | scheduleUpdateStyles: observer( 48 | 'columnMeta.{width,offsetLeft,offsetRight}', 49 | 'isFixedLeft', 50 | 'isFixedRight', 51 | 52 | function() { 53 | scheduleOnce('actions', this, 'updateStyles'); 54 | } 55 | ), 56 | 57 | updateStyles() { 58 | if (typeof FastBoot === 'undefined' && this.element) { 59 | let width = `${this.get('columnMeta.width')}px`; 60 | 61 | this.element.style.width = width; 62 | this.element.style.minWidth = width; 63 | this.element.style.maxWidth = width; 64 | 65 | this.element.style.left = this.get('isFixedLeft') 66 | ? `${Math.round(this.get('columnMeta.offsetLeft'))}px` 67 | : null; 68 | this.element.style.right = this.get('isFixedRight') 69 | ? `${Math.round(this.get('columnMeta.offsetRight'))}px` 70 | : null; 71 | 72 | if (this.get('isSlack')) { 73 | this.element.style.paddingLeft = 0; 74 | this.element.style.paddingRight = 0; 75 | this.element.style.display = width === '0px' ? 'none' : 'table-cell'; 76 | } 77 | } 78 | }, 79 | 80 | didInsertElement() { 81 | this._super(...arguments); 82 | this.updateStyles(); 83 | }, 84 | }); 85 | -------------------------------------------------------------------------------- /addon/components/-private/row-wrapper.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import hbs from 'htmlbars-inline-precompile'; 3 | 4 | import EmberObject, { get, setProperties, computed, defineProperty } from '@ember/object'; 5 | import { alias } from '@ember/object/computed'; 6 | import { A as emberA } from '@ember/array'; 7 | 8 | import { notifyPropertyChange } from '../../-private/utils/ember'; 9 | import { objectAt } from '../../-private/utils/array'; 10 | import { observer } from '../../-private/utils/observer'; 11 | 12 | const CellWrapper = EmberObject.extend({ 13 | /* eslint-disable-next-line ember/no-observers, ember-best-practices/no-observers */ 14 | columnValueValuePathDidChange: observer('columnValue.valuePath', function() { 15 | let columnValuePath = get(this, 'columnValue.valuePath'); 16 | let cellValue = columnValuePath ? alias(`rowValue.${columnValuePath}`) : null; 17 | 18 | defineProperty(this, 'cellValue', cellValue); 19 | notifyPropertyChange(this, 'cellValue'); 20 | }), 21 | 22 | cellMeta: computed('rowMeta', 'columnValue', function() { 23 | let rowMeta = get(this, 'rowMeta'); 24 | let columnValue = get(this, 'columnValue'); 25 | 26 | if (!rowMeta._cellMetaCache.has(columnValue)) { 27 | rowMeta._cellMetaCache.set(columnValue, EmberObject.create()); 28 | } 29 | 30 | return rowMeta._cellMetaCache.get(columnValue); 31 | }), 32 | }); 33 | 34 | const layout = hbs`{{yield this.api}}`; 35 | 36 | export default Component.extend({ 37 | layout, 38 | tagName: '', 39 | 40 | canSelect: undefined, 41 | checkboxSelectionMode: undefined, 42 | columnMetaCache: undefined, 43 | columns: undefined, 44 | rowMetaCache: undefined, 45 | rowSelectionMode: undefined, 46 | rowToggleMode: undefined, 47 | rowValue: undefined, 48 | rowsCount: undefined, 49 | 50 | init() { 51 | this._super(...arguments); 52 | 53 | this._cells = emberA([]); 54 | }, 55 | 56 | destroy() { 57 | this._cells.forEach(cell => cell.destroy()); 58 | 59 | this._super(...arguments); 60 | }, 61 | 62 | api: computed( 63 | 'rowValue', 64 | 'rowMeta', 65 | 'cells', 66 | 'canSelect', 67 | 'rowSelectionMode', 68 | 'rowToggleMode', 69 | 'rowsCount', 70 | function() { 71 | let rowValue = this.get('rowValue'); 72 | let rowMeta = this.get('rowMeta'); 73 | let cells = this.get('cells'); 74 | let canSelect = this.get('canSelect'); 75 | let rowSelectionMode = canSelect ? this.get('rowSelectionMode') : 'none'; 76 | let rowToggleMode = this.get('rowToggleMode'); 77 | let rowsCount = this.get('rowsCount'); 78 | 79 | return { rowValue, rowMeta, cells, rowSelectionMode, rowToggleMode, rowsCount }; 80 | } 81 | ), 82 | 83 | rowMeta: computed('rowValue', function() { 84 | let rowValue = this.get('rowValue'); 85 | let rowMetaCache = this.get('rowMetaCache'); 86 | 87 | return rowMetaCache.get(rowValue); 88 | }), 89 | 90 | cells: computed( 91 | 'rowValue', 92 | 'rowMeta', 93 | 'columns.[]', 94 | 'canSelect', 95 | 'checkboxSelectionMode', 96 | 'rowSelectionMode', 97 | function() { 98 | let columns = this.get('columns'); 99 | let numColumns = get(columns, 'length'); 100 | 101 | let rowValue = this.get('rowValue'); 102 | let rowMeta = this.get('rowMeta'); 103 | let rowsCount = this.get('rowsCount'); 104 | let canSelect = this.get('canSelect'); 105 | let checkboxSelectionMode = canSelect ? this.get('checkboxSelectionMode') : 'none'; 106 | let rowSelectionMode = canSelect ? this.get('rowSelectionMode') : 'none'; 107 | 108 | let { _cells } = this; 109 | 110 | if (numColumns !== _cells.length) { 111 | while (_cells.length < numColumns) { 112 | _cells.pushObject(CellWrapper.create()); 113 | } 114 | 115 | while (_cells.length > numColumns) { 116 | _cells.popObject().destroy(); 117 | } 118 | } 119 | 120 | _cells.forEach((cell, i) => { 121 | let columnValue = objectAt(columns, i); 122 | let columnMeta = this.get('columnMetaCache').get(columnValue); 123 | 124 | // eslint-disable-next-line ember/no-side-effects, ember-best-practices/no-side-effect-cp 125 | setProperties(cell, { 126 | checkboxSelectionMode, 127 | columnMeta, 128 | columnValue, 129 | rowMeta, 130 | rowSelectionMode, 131 | rowValue, 132 | rowsCount, 133 | }); 134 | }); 135 | 136 | return _cells; 137 | } 138 | ), 139 | }); 140 | -------------------------------------------------------------------------------- /addon/components/-private/scroll-indicators/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if this.showLeft}} 2 |
7 | {{/if}} 8 | {{#if this.showRight}} 9 |
14 | {{/if}} 15 | {{#if this.showTop}} 16 |
21 | {{/if}} 22 | {{#if this.showBottom}} 23 |
28 | {{/if}} 29 | -------------------------------------------------------------------------------- /addon/components/ember-table-simple-checkbox.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import defaultTo from '../-private/utils/default-to'; 3 | 4 | export default Component.extend({ 5 | tagName: 'input', 6 | 7 | attributeBindings: [ 8 | 'ariaLabel:aria-label', 9 | 'checked', 10 | 'disabled', 11 | 'indeterminate', 12 | 'type', 13 | 'value', 14 | 'dataTestSelectRow:data-test-select-row', 15 | 'dataTestCollapseRow:data-test-collapse-row', 16 | ], 17 | 18 | ariaLabel: undefined, 19 | checked: defaultTo(false), 20 | disabled: defaultTo(false), 21 | indeterminate: defaultTo(false), 22 | onChange: null, 23 | onClick: null, 24 | type: 'checkbox', 25 | value: null, 26 | 27 | click(event) { 28 | this.onClick?.(event); 29 | }, 30 | 31 | change(event) { 32 | let checked = this.element.checked; 33 | let indeterminate = this.element.indeterminate; 34 | let value = this.get('value'); 35 | 36 | // Checked and indeterminate state have been changed, but that's not DDAU! 37 | // Reset the change, send the action and wait for it to be changed manually 38 | this.element.checked = this.get('checked'); 39 | this.element.indeterminate = this.get('indeterminate'); 40 | 41 | this.onChange?.(checked, { value, indeterminate }, event); 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /addon/components/ember-table/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { computed } from '@ember/object'; 3 | import { htmlSafe } from '@ember/template'; 4 | import { 5 | setupTableStickyPolyfill, 6 | teardownTableStickyPolyfill, 7 | } from '../../-private/sticky/table-sticky-polyfill'; 8 | 9 | import layout from './template'; 10 | 11 | /** 12 | The primary Ember Table component. This component represents the root of the 13 | table, and manages high level state of all of its subcomponents. It does not 14 | have any arguments or actions itself - instead, all of those concerns are 15 | delegated to its children, who communicate to each other via the API. 16 | 17 | ```hbs 18 | 19 | 20 | 21 | 22 | 23 | ``` 24 | 25 | @yield {object} table - the API object yielded by the table 26 | @yield {Component} table.head - The table header component 27 | @yield {Component} table.body - The table body component 28 | @yield {Component} table.foot - The table footer component 29 | @class 30 | @public 31 | */ 32 | export default Component.extend({ 33 | layout, 34 | classNames: ['ember-table'], 35 | attributeBindings: ['dataTestEmberTable:data-test-ember-table'], 36 | dataTestEmberTable: true, 37 | 38 | didInsertElement() { 39 | this._super(...arguments); 40 | 41 | let thead = this.element.querySelector('thead'); 42 | let tfoot = this.element.querySelector('tfoot'); 43 | 44 | if (thead) { 45 | setupTableStickyPolyfill(thead); 46 | } 47 | if (tfoot) { 48 | setupTableStickyPolyfill(tfoot); 49 | } 50 | }, 51 | 52 | willDestroyElement() { 53 | let thead = this.element.querySelector('thead'); 54 | let tfoot = this.element.querySelector('tfoot'); 55 | 56 | if (thead) { 57 | teardownTableStickyPolyfill(this.element.querySelector('thead')); 58 | } 59 | 60 | if (tfoot) { 61 | teardownTableStickyPolyfill(this.element.querySelector('tfoot')); 62 | } 63 | 64 | this._super(...arguments); 65 | }, 66 | 67 | tableStyle: computed('tableWidth', function() { 68 | return htmlSafe(`width: ${this.get('tableWidth')}px;`); 69 | }), 70 | 71 | api: computed(function() { 72 | return { 73 | columns: null, 74 | registerColumnTree: this.registerColumnTree.bind(this), 75 | tableId: `${this.elementId}-overflow`, 76 | }; 77 | }), 78 | 79 | registerColumnTree(columnTree) { 80 | this.set('api.columnTree', columnTree); 81 | }, 82 | }); 83 | -------------------------------------------------------------------------------- /addon/components/ember-table/template.hbs: -------------------------------------------------------------------------------- 1 |
6 | 7 | {{yield (hash 8 | api=this.api 9 | head=(component "ember-thead" api=this.api) 10 | body=(component "ember-tbody" api=this.api) 11 | foot=(component "ember-tfoot" api=this.api) 12 | loadingMore=(component "ember-table-loading-more" api=this.api) 13 | )}} 14 |
15 |
16 | 17 | -------------------------------------------------------------------------------- /addon/components/ember-tbody/template.hbs: -------------------------------------------------------------------------------- 1 | {{#vertical-collection 2 | items=this.wrappedRows 3 | containerSelector=this._containerSelector 4 | 5 | estimateHeight=this.estimateRowHeight 6 | key=this.key 7 | staticHeight=this.staticHeight 8 | bufferSize=this.bufferSize 9 | renderAll=this.renderAll 10 | 11 | firstReached=this.firstReached 12 | lastReached=this.lastReached 13 | firstVisibleChanged=this.firstVisibleChanged 14 | lastVisibleChanged=this.lastVisibleChanged 15 | idForFirstItem=this.idForFirstItem 16 | 17 | as |rowValue| 18 | }} 19 | 30 | {{#if (has-block)}} 31 | {{yield (hash 32 | rowValue=api.rowValue 33 | rowMeta=api.rowMeta 34 | cells=api.cells 35 | rowSelectionMode=api.rowSelectionMode 36 | rowToggleMode=api.rowToggleMode 37 | rowsCount=api.rowsCount 38 | 39 | row=(component "ember-tr" api=api) 40 | )}} 41 | {{else}} 42 | 43 | {{/if}} 44 | 45 | 46 | {{else}} 47 | {{yield to='inverse'}} 48 | {{/vertical-collection}} 49 | -------------------------------------------------------------------------------- /addon/components/ember-td/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if this.isFirstColumn}} 2 |
3 | {{#if this.canSelect}} 4 | 8 | 14 | 15 | 16 | {{/if}} 17 | 18 | {{#if this.canCollapse}} 19 | 20 | 26 | 27 | 28 | {{else}} 29 |
30 | {{/if}} 31 | 32 |
33 | {{#if (has-block)}} 34 | {{yield this.cellValue this.columnValue this.rowValue this.cellMeta this.columnMeta this.rowMeta this.rowsCount}} 35 | {{else}} 36 | {{this.cellValue}} 37 | {{/if}} 38 |
39 |
40 | {{else}} 41 | {{#if (has-block)}} 42 | {{yield this.cellValue this.columnValue this.rowValue this.cellMeta this.columnMeta this.rowMeta this.rowsCount}} 43 | {{else}} 44 | {{this.cellValue}} 45 | {{/if}} 46 | {{/if}} 47 | -------------------------------------------------------------------------------- /addon/components/ember-tfoot/component.js: -------------------------------------------------------------------------------- 1 | import EmberTBody from '../ember-tbody/component'; 2 | import { A as emberA } from '@ember/array'; 3 | 4 | import { computed } from '@ember/object'; 5 | 6 | import layout from './template'; 7 | 8 | /** 9 | The table footer component. This component manages any footer rows which may 10 | be attached to the table, and has the same API as EmberTBody. It does not 11 | provide occlusion, because the number of footer rows is expected to be 12 | relatively small. 13 | 14 | ```hbs 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ``` 24 | 25 | @yield {object} foot - the API object yielded by the table footer 26 | @yield {Component} foot.row - The table row component 27 | @yield {object} foot.rowValue - The value for the currently yielded row 28 | @yield {object} foot.rowMeta - The meta for the currently yielded row 29 | @class 30 | @public 31 | */ 32 | export default EmberTBody.extend({ 33 | layout, 34 | tagName: 'tfoot', 35 | 36 | wrappedRowArray: computed('wrappedRows.[]', function() { 37 | let wrappedRows = this.get('wrappedRows'); 38 | let wrappedRowsLength = wrappedRows.get('length'); 39 | 40 | let arr = []; 41 | 42 | for (let i = 0; i < wrappedRowsLength; i++) { 43 | arr.push(wrappedRows.objectAt(i)); 44 | } 45 | 46 | return emberA(arr); 47 | }), 48 | }); 49 | -------------------------------------------------------------------------------- /addon/components/ember-tfoot/template.hbs: -------------------------------------------------------------------------------- 1 | {{#each this.wrappedRowArray as |rowValue|}} 2 | 12 | {{#if (has-block)}} 13 | {{yield (hash 14 | rowValue=api.rowValue 15 | rowMeta=api.rowMeta 16 | cells=api.cells 17 | rowSelectionMode=api.rowSelectionMode 18 | rowsCount=api.rowsCount 19 | 20 | row=(component "ember-tr" api=api) 21 | )}} 22 | {{else}} 23 | 24 | {{/if}} 25 | 26 | {{/each}} 27 | -------------------------------------------------------------------------------- /addon/components/ember-th/resize-handle/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import layout from './template'; 3 | 4 | import { readOnly } from '@ember/object/computed'; 5 | 6 | /** 7 | The table header cell resize handle component. This component renders an area to grab to resize a column. 8 | 9 | ```hbs 10 | 11 | 12 | 13 | 14 | {{columnValue.name}} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ``` 24 | @class 25 | @public 26 | */ 27 | export default Component.extend({ 28 | layout, 29 | tagName: '', 30 | 31 | /** 32 | The API object passed in by the table header cell 33 | @argument columnMeta 34 | @required 35 | @type object 36 | */ 37 | columnMeta: null, 38 | 39 | isResizable: readOnly('columnMeta.isResizable'), 40 | }); 41 | -------------------------------------------------------------------------------- /addon/components/ember-th/resize-handle/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if this.isResizable}} 2 |
3 |
4 | {{/if}} -------------------------------------------------------------------------------- /addon/components/ember-th/sort-indicator/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import layout from './template'; 3 | 4 | import { readOnly } from '@ember/object/computed'; 5 | 6 | /** 7 | The table header cell sort indicator component. This component renders the state of the sort on the column (ascending/descending/none). 8 | 9 | ```hbs 10 | 11 | 12 | 13 | 14 | {{columnValue.name}} 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ``` 24 | @yield {object} columnMeta - The meta object associated with this column 25 | @class 26 | */ 27 | 28 | export default Component.extend({ 29 | layout, 30 | tagName: '', 31 | 32 | /** 33 | The API object passed in by the table header cell 34 | @argument columnMeta 35 | @required 36 | @type object 37 | */ 38 | columnMeta: null, 39 | 40 | isSortable: readOnly('columnMeta.isSortable'), 41 | 42 | isSorted: readOnly('columnMeta.isSorted'), 43 | 44 | isSortedAsc: readOnly('columnMeta.isSortedAsc'), 45 | 46 | isMultiSorted: readOnly('columnMeta.isMultiSorted'), 47 | 48 | sortIndex: readOnly('columnMeta.sortIndex'), 49 | }); 50 | -------------------------------------------------------------------------------- /addon/components/ember-th/sort-indicator/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if this.isSorted}} 2 | 3 | {{#if (has-block)}} 4 | {{yield this.columnMeta}} 5 | {{else}} 6 | {{#if this.isMultiSorted}} 7 | {{this.sortIndex}} 8 | {{/if}} 9 | {{/if}} 10 | 11 | {{/if}} 12 | 13 | {{#if this.isSortable}} 14 | 15 | {{/if}} 16 | -------------------------------------------------------------------------------- /addon/components/ember-th/template.hbs: -------------------------------------------------------------------------------- 1 | {{#if (has-block)}} 2 | {{yield this.columnValue this.columnMeta this.rowMeta}} 3 | {{else}} 4 | {{this.columnValue.name}} 5 | 6 | 7 | 8 | 9 | {{/if}} 10 | -------------------------------------------------------------------------------- /addon/components/ember-thead/template.hbs: -------------------------------------------------------------------------------- 1 | {{#each this.wrappedRows as |api|}} 2 | {{#if (has-block)}} 3 | {{yield (hash 4 | cells=api.cells 5 | isHeader=api.isHeader 6 | rowsCount=api.rowsCount 7 | 8 | row=(component "ember-tr" api=api) 9 | )}} 10 | {{else}} 11 | 12 | {{/if}} 13 | {{/each}} 14 | -------------------------------------------------------------------------------- /addon/components/ember-tr/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { computed } from '@ember/object'; 3 | import { readOnly } from '@ember/object/computed'; 4 | 5 | import { closest } from '../../-private/utils/element'; 6 | 7 | import layout from './template'; 8 | import { SELECT_MODE } from '../../-private/collapse-tree'; 9 | 10 | /** 11 | The table row component. This component manages row level concerns, and yields 12 | an API object that contains the cell component, the cell/column/row values, 13 | and the cell/column/row meta objects. It is used in both the header and the 14 | body, mirroring the structure of native HTML tables. 15 | 16 | ```hbs 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ``` 31 | 32 | @yield {object} row - the API object yielded by the table row 33 | @yield {Component} row.cell - The table cell contextual component 34 | 35 | @yield {any} row.cellValue - The value for the currently yielded cell 36 | @yield {object} row.cellMeta - The meta for the currently yielded cell 37 | 38 | @yield {object} row.columnValue - The value for the currently yielded column 39 | @yield {object} row.columnMeta - The meta for the currently yielded column 40 | 41 | @yield {object} row.rowValue - The value for the currently yielded row 42 | @yield {object} row.rowMeta - The meta for the currently yielded row 43 | 44 | @class 45 | @public 46 | */ 47 | export default Component.extend({ 48 | layout, 49 | tagName: 'tr', 50 | classNames: ['et-tr'], 51 | classNameBindings: ['isSelected', 'isGroupSelected', 'isSelectable'], 52 | 53 | /** 54 | The API object passed in by the table body, header, or footer 55 | @argument api 56 | @required 57 | @type object 58 | */ 59 | api: null, 60 | 61 | /** 62 | Action sent when the user clicks this element 63 | @argument onClick 64 | @type Action? 65 | */ 66 | onClick: null, 67 | 68 | /** 69 | Action sent when the user double clicks this element 70 | @argument onDoubleClick 71 | @type Action? 72 | */ 73 | onDoubleClick: null, 74 | 75 | rowValue: readOnly('api.rowValue'), 76 | 77 | rowMeta: readOnly('api.rowMeta'), 78 | 79 | cells: readOnly('api.cells'), 80 | 81 | rowSelectionMode: readOnly('api.rowSelectionMode'), 82 | 83 | rowToggleMode: readOnly('api.rowToggleMode'), 84 | 85 | isHeader: readOnly('api.isHeader'), 86 | 87 | isSelected: readOnly('rowMeta.isSelected'), 88 | 89 | isGroupSelected: readOnly('rowMeta.isGroupSelected'), 90 | 91 | isSelectable: computed('rowSelectionMode', function() { 92 | let rowSelectionMode = this.get('rowSelectionMode'); 93 | 94 | return rowSelectionMode === SELECT_MODE.MULTIPLE || rowSelectionMode === SELECT_MODE.SINGLE; 95 | }), 96 | 97 | click(event) { 98 | let rowSelectionMode = this.get('rowSelectionMode'); 99 | let inputParent = closest(event.target, 'input, button, label, a, select'); 100 | 101 | if (!inputParent) { 102 | let rowMeta = this.get('rowMeta'); 103 | 104 | if (rowMeta && rowSelectionMode === SELECT_MODE.MULTIPLE) { 105 | let toggle = event.ctrlKey || event.metaKey || this.get('rowToggleMode'); 106 | let range = event.shiftKey; 107 | 108 | rowMeta.select({ toggle, range }); 109 | } else if (rowMeta && rowSelectionMode === SELECT_MODE.SINGLE) { 110 | rowMeta.select({ single: true }); 111 | } 112 | } 113 | 114 | this.sendEventAction('onClick', event); 115 | }, 116 | 117 | doubleClick(event) { 118 | this.sendEventAction('onDoubleClick', event); 119 | }, 120 | 121 | sendEventAction(action, event) { 122 | let rowValue = this.get('rowValue'); 123 | let rowMeta = this.get('rowMeta'); 124 | 125 | let closureAction = this[action]; 126 | 127 | closureAction?.({ 128 | event, 129 | rowValue, 130 | rowMeta, 131 | }); 132 | }, 133 | }); 134 | -------------------------------------------------------------------------------- /addon/components/ember-tr/template.hbs: -------------------------------------------------------------------------------- 1 | {{#each this.cells as |api|}} 2 | {{#if (has-block)}} 3 | {{#if this.isHeader}} 4 | {{yield (hash 5 | columnValue=api.columnValue 6 | columnMeta=api.columnMeta 7 | 8 | sorts=api.sorts 9 | sendUpdateSort=api.sendUpdateSort 10 | 11 | rowMeta=api.rowMeta 12 | rowsCount=api.rowsCount 13 | 14 | cell=(component "ember-th" api=api) 15 | )}} 16 | {{else}} 17 | {{yield (hash 18 | api=api 19 | 20 | cellValue=api.cellValue 21 | cellMeta=api.cellMeta 22 | 23 | columnValue=api.columnValue 24 | columnMeta=api.columnMeta 25 | 26 | rowValue=api.rowValue 27 | rowMeta=api.rowMeta 28 | 29 | rowsCount=api.rowsCount 30 | 31 | cell=(component "ember-td" api=api) 32 | )}} 33 | {{/if}} 34 | {{else if this.isHeader}} 35 | 36 | {{else}} 37 | 38 | {{/if}} 39 | {{/each}} 40 | -------------------------------------------------------------------------------- /addon/styles/addon.css: -------------------------------------------------------------------------------- 1 | .ember-table { 2 | position: relative; 3 | height: 100%; 4 | width: 100%; 5 | box-sizing: border-box; 6 | } 7 | 8 | .ember-table .ember-table-overflow { 9 | overflow: auto; 10 | max-height: 100%; 11 | max-width: 100%; 12 | } 13 | 14 | .ember-table table { 15 | border-spacing: 0; 16 | table-layout: fixed; 17 | box-sizing: border-box; 18 | } 19 | 20 | .ember-table td, 21 | .ember-table th { 22 | box-sizing: border-box; 23 | } 24 | 25 | .ember-table td.is-fixed-left, 26 | .ember-table td.is-fixed-right, 27 | .ember-table th.is-fixed-left, 28 | .ember-table th.is-fixed-right { 29 | position: -webkit-sticky; 30 | position: sticky; 31 | left: 0; 32 | } 33 | 34 | .ember-table td.ember-table__text-align-left, 35 | .ember-table th.ember-table__text-align-left { 36 | text-align: left; 37 | } 38 | 39 | .ember-table td.ember-table__text-align-center, 40 | .ember-table th.ember-table__text-align-center { 41 | text-align: center; 42 | } 43 | 44 | .ember-table td.ember-table__text-align-right, 45 | .ember-table th.ember-table__text-align-right { 46 | text-align: right; 47 | } 48 | 49 | .ember-table th { 50 | z-index: 2; 51 | } 52 | 53 | .ember-table th:not(.is-fixed-right) .et-header-resize-area { 54 | right: 0; 55 | } 56 | 57 | .ember-table th.is-fixed-right .et-header-resize-area { 58 | left: 0; 59 | } 60 | 61 | .ember-table td.is-fixed-left, 62 | .ember-table td.is-fixed-right { 63 | z-index: 3; 64 | } 65 | 66 | .ember-table th.is-fixed-left, 67 | .ember-table th.is-fixed-right { 68 | z-index: 4; 69 | } 70 | 71 | .ember-table th.is-sortable { 72 | cursor: pointer; 73 | } 74 | 75 | .ember-table thead { 76 | position: -webkit-sticky; 77 | position: sticky; 78 | top: 0; 79 | z-index: 2; 80 | box-sizing: border-box; 81 | } 82 | 83 | .ember-table tbody { 84 | box-sizing: border-box; 85 | } 86 | 87 | .ember-table tfoot { 88 | position: -webkit-sticky; 89 | position: sticky; 90 | bottom: 0; 91 | z-index: 2; 92 | box-sizing: border-box; 93 | } 94 | 95 | .ember-table .scroll-indicator { 96 | position: absolute; 97 | z-index: 5; 98 | } 99 | 100 | .ember-table .scroll-indicator__left, 101 | .ember-table .scroll-indicator__right { 102 | top: 0; 103 | width: 8px; 104 | height: 100%; 105 | } 106 | 107 | .ember-table .scroll-indicator__left { 108 | background: linear-gradient(to right, rgba(168, 168, 168, 0.4), rgba(168, 168, 168, 0)); 109 | } 110 | 111 | .ember-table .scroll-indicator__right { 112 | right: 0; 113 | background: linear-gradient(to left, rgba(168, 168, 168, 0.4), rgba(168, 168, 168, 0)); 114 | } 115 | 116 | .ember-table .scroll-indicator__top, 117 | .ember-table .scroll-indicator__bottom { 118 | left: 0; 119 | width: 100%; 120 | height: 8px; 121 | } 122 | 123 | .ember-table .scroll-indicator__top { 124 | top: 0; 125 | background: linear-gradient(to bottom, rgba(168, 168, 168, 0.4), rgba(168, 168, 168, 0)); 126 | } 127 | 128 | .ember-table .scroll-indicator__bottom { 129 | bottom: 0; 130 | background: linear-gradient(to top, rgba(168, 168, 168, 0.4), rgba(168, 168, 168, 0)); 131 | } 132 | 133 | .ember-table.et-unselectable { 134 | user-select: none; 135 | } 136 | 137 | .ember-table .et-header-resize-area { 138 | cursor: col-resize; 139 | width: 10px; 140 | height: 100%; 141 | position: absolute; 142 | top: 0; 143 | } 144 | 145 | .ember-table .et-speech-only { 146 | display: none !important; 147 | } 148 | 149 | @media speech { 150 | .ember-table .et-speech-only { 151 | display: block !important; 152 | } 153 | } 154 | 155 | .ember-table .ember-table-loading-more { 156 | display: inline-block; 157 | } 158 | -------------------------------------------------------------------------------- /app/components/ember-table-loading-more.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-table/components/ember-table-loading-more/component'; 2 | -------------------------------------------------------------------------------- /app/components/ember-table-private/row-wrapper.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-table/components/-private/row-wrapper'; 2 | -------------------------------------------------------------------------------- /app/components/ember-table-private/scroll-indicators.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-table/components/-private/scroll-indicators/component'; 2 | -------------------------------------------------------------------------------- /app/components/ember-table-simple-checkbox.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-table/components/ember-table-simple-checkbox'; 2 | -------------------------------------------------------------------------------- /app/components/ember-table.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-table/components/ember-table/component'; 2 | -------------------------------------------------------------------------------- /app/components/ember-tbody.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-table/components/ember-tbody/component'; 2 | -------------------------------------------------------------------------------- /app/components/ember-td.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-table/components/ember-td/component'; 2 | -------------------------------------------------------------------------------- /app/components/ember-tfoot.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-table/components/ember-tfoot/component'; 2 | -------------------------------------------------------------------------------- /app/components/ember-th.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-table/components/ember-th/component'; 2 | -------------------------------------------------------------------------------- /app/components/ember-th/resize-handle.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-table/components/ember-th/resize-handle/component'; 2 | -------------------------------------------------------------------------------- /app/components/ember-th/sort-indicator.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-table/components/ember-th/sort-indicator/component'; 2 | -------------------------------------------------------------------------------- /app/components/ember-thead.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-table/components/ember-thead/component'; 2 | -------------------------------------------------------------------------------- /app/components/ember-tr.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-table/components/ember-tr/component'; 2 | -------------------------------------------------------------------------------- /app/styles/ember-table/default.scss: -------------------------------------------------------------------------------- 1 | /************************************************************************************************/ 2 | /* Table default style */ 3 | /************************************************************************************************/ 4 | $table-header-background-color: #f8f8f8; 5 | $table-border-color: #dcdcdc; 6 | $table-hover-color: #e5edf8; 7 | 8 | .ember-table { 9 | border: solid 1px #dddddd; 10 | 11 | th, 12 | td { 13 | white-space: nowrap; 14 | text-overflow: ellipsis; 15 | overflow: hidden; 16 | font-size: 20px; 17 | padding: 4px 10px; 18 | } 19 | 20 | tbody { 21 | td { 22 | border-top: none; 23 | border-left: none; 24 | border-bottom: $table-border-color 1px dotted; 25 | border-right: $table-border-color 1px solid; 26 | background-color: #ffffff; 27 | 28 | &:last-child { 29 | border-right: none; 30 | } 31 | 32 | &.is-fixed-right { 33 | border-left: solid 1px $table-border-color; 34 | } 35 | } 36 | } 37 | 38 | th, 39 | tfoot td { 40 | padding: 5px 0 3px; 41 | background-color: $table-header-background-color; 42 | font-family: 'Univers LT W01 65 Bold'; 43 | line-height: 1.4; 44 | font-weight: bold; 45 | text-align: center; 46 | box-sizing: border-box; 47 | } 48 | 49 | tfoot td { 50 | border-top: 1px solid $table-border-color; 51 | border-right: solid 1px $table-border-color; 52 | 53 | &:last-child { 54 | border-right: none; 55 | } 56 | } 57 | 58 | thead th { 59 | border-bottom: 1px solid $table-border-color; 60 | border-right: solid 1px $table-border-color; 61 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); 62 | position: relative; 63 | overflow: hidden; 64 | 65 | &:last-child { 66 | border-right: none; 67 | } 68 | 69 | &.is-fixed-right { 70 | border-left: solid 1px $table-border-color; 71 | 72 | .et-header-resize-area { 73 | left: 0; 74 | } 75 | } 76 | } 77 | 78 | tr:hover { 79 | th { 80 | cursor: pointer; 81 | } 82 | 83 | td { 84 | background-color: $table-hover-color; 85 | } 86 | } 87 | 88 | tr.is-selected { 89 | td, 90 | th { 91 | background-color: #227ecb; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /config/addon-docs.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | // eslint-disable-next-line 5 | const AddonDocsConfig = require('ember-cli-addon-docs/lib/config'); 6 | 7 | module.exports = class extends AddonDocsConfig { 8 | // See https://ember-learn.github.io/ember-cli-addon-docs/latest/docs/deploying 9 | // for details on configuration you can override here. 10 | }; 11 | -------------------------------------------------------------------------------- /config/deploy.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | module.exports = function(deployTarget) { 5 | let ENV = { 6 | build: {}, 7 | // include other plugin configuration that applies to all deploy targets here 8 | }; 9 | 10 | if (deployTarget === 'development') { 11 | ENV.build.environment = 'development'; 12 | // configure other plugins for development deploy target here 13 | } 14 | 15 | if (deployTarget === 'staging') { 16 | ENV.build.environment = 'production'; 17 | // configure other plugins for staging deploy target here 18 | } 19 | 20 | if (deployTarget === 'production') { 21 | ENV.build.environment = 'production'; 22 | // configure other plugins for production deploy target here 23 | } 24 | 25 | // Note: if you need to build some configuration asynchronously, you can return 26 | // a promise that resolves with the ENV object instead of returning the 27 | // ENV object synchronously. 28 | return ENV; 29 | }; 30 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function(defaults) { 6 | let app = new EmberAddon(defaults, { 7 | 'ember-cli-babel': { 8 | includePolyfill: false, 9 | }, 10 | babel: { 11 | plugins: [ 12 | '@babel/plugin-proposal-object-rest-spread', 13 | '@babel/plugin-proposal-optional-chaining', 14 | '@babel/plugin-proposal-nullish-coalescing-operator', 15 | '@babel/plugin-proposal-numeric-separator', 16 | '@babel/plugin-proposal-optional-catch-binding', 17 | ], 18 | }, 19 | 'ember-faker': { 20 | /* Always enable for dummy app because the docs examples use faker */ 21 | enabled: true, 22 | }, 23 | }); 24 | 25 | /* 26 | This build file specifies the options for the dummy test app of this 27 | addon, located in `/tests/dummy` 28 | This build file does *not* influence how the addon or the app using it 29 | behave. You most likely want to be modifying `./index.js` or app's build file 30 | */ 31 | 32 | let { maybeEmbroider } = require('@embroider/test-setup'); 33 | return maybeEmbroider(app); 34 | }; 35 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const VersionChecker = require('ember-cli-version-checker'); 3 | 4 | module.exports = { 5 | name: 'ember-table', 6 | 7 | options: { 8 | babel: { 9 | plugins: [ 10 | '@babel/plugin-proposal-object-rest-spread', 11 | '@babel/plugin-proposal-optional-chaining', 12 | '@babel/plugin-proposal-nullish-coalescing-operator', 13 | '@babel/plugin-proposal-numeric-separator', 14 | '@babel/plugin-proposal-optional-catch-binding', 15 | ], 16 | }, 17 | }, 18 | 19 | included() { 20 | this._super.included.apply(this, arguments); 21 | 22 | this.checker = new VersionChecker(this.project); 23 | 24 | let importOptions; 25 | 26 | // If `ember-cli-fastboot` is being used in the root project, use the 27 | // fastboot transform when importing 28 | if (this.project.findAddonByName('ember-cli-fastboot')) { 29 | importOptions = { 30 | using: [ 31 | { 32 | transformation: 'fastbootShim', 33 | }, 34 | ], 35 | }; 36 | } 37 | 38 | this.import('node_modules/css-element-queries/src/ResizeSensor.js', importOptions); 39 | 40 | this.import('node_modules/hammerjs/hammer.js', importOptions); 41 | }, 42 | 43 | isDevelopingAddon() { 44 | // this prevents templates from being cached before we can strip them out 45 | // they get cached by jshintAddonTree before we can intervene 46 | if (this.checker.for('ember-source').gte('2.3.0')) { 47 | return this._super.isDevelopingAddon.apply(this, arguments); 48 | } 49 | 50 | return false; 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "experimentalDecorators": true 5 | }, 6 | "exclude": ["node_modules", "tmp", "dist"] 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-table", 3 | "version": "6.0.0-9", 4 | "description": "An addon to support large data set and a number of features around table.", 5 | "keywords": [ 6 | "ember-addon" 7 | ], 8 | "license": "BSD-3-Clause", 9 | "author": "", 10 | "directories": { 11 | "doc": "doc", 12 | "test": "tests" 13 | }, 14 | "repository": "https://github.com/Addepar/ember-table", 15 | "scripts": { 16 | "build": "ember build", 17 | "docs:deploy": "ember try:one ember-default-docs --- ember deploy production", 18 | "lint": "concurrently \"yarn:lint:*(!fix)\" --names \"lint:\"", 19 | "lint:files": "ls-lint", 20 | "lint:js": "eslint . --cache", 21 | "lint:js:fix": "eslint . --fix", 22 | "lint:sass": "concurrently \"yarn:lint:sass:*\"", 23 | "lint:sass:sass-lint": "sass-lint -c .sass-lint.yml --verbose --no-exit", 24 | "lint:sass:prettier": "prettier --list-different '{addon,app}/styles/**/*.scss' 'tests/dummy/app/styles/**/*.scss'", 25 | "release": "release-it", 26 | "start": "ember serve", 27 | "test": "ember test", 28 | "test:ember-compatibility": "ember try:one" 29 | }, 30 | "dependencies": { 31 | "@babel/core": "^7.0.0-0", 32 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.13", 33 | "@babel/plugin-proposal-numeric-separator": "^7.12.13", 34 | "@babel/plugin-proposal-object-rest-spread": "^7.12.13", 35 | "@babel/plugin-proposal-optional-catch-binding": "^7.12.13", 36 | "@babel/plugin-proposal-optional-chaining": "^7.12.13", 37 | "@html-next/vertical-collection": "^4.0.0", 38 | "css-element-queries": "^0.4.0", 39 | "ember-classy-page-object": "^0.8.0", 40 | "ember-cli-babel": "^7.12.0", 41 | "ember-cli-htmlbars": "^6.0.0", 42 | "ember-cli-node-assets": "^0.2.2", 43 | "ember-cli-version-checker": "^5.1.2", 44 | "ember-compatibility-helpers": "^1.2.6", 45 | "ember-raf-scheduler": "^0.3.0", 46 | "ember-test-selectors": "^7.1.0", 47 | "hammerjs": "^2.0.8" 48 | }, 49 | "devDependencies": { 50 | "@addepar/eslint-config": "^4.0.2", 51 | "@addepar/prettier-config": "^1.0.0", 52 | "@addepar/sass-lint-config": "^2.0.1", 53 | "@addepar/style-toolbox": "~0.8.1", 54 | "@babel/eslint-parser": "^7.27.1", 55 | "@babel/plugin-proposal-decorators": "^7.27.1", 56 | "@ember/optional-features": "^2.0.0", 57 | "@ember/string": "^3.0.0", 58 | "@ember/test-helpers": "^5.2.2", 59 | "@embroider/test-setup": "^4.0.0", 60 | "@glimmer/component": "^1.1.2", 61 | "@ls-lint/ls-lint": "^2.2.3", 62 | "@tsconfig/ember": "^1.0.1", 63 | "@types/ember__component": "^4.0.10", 64 | "broccoli-asset-rev": "^3.0.0", 65 | "concurrently": "^9.1.2", 66 | "ember-a11y-testing": "^7.1.2", 67 | "ember-auto-import": "^2.4.2", 68 | "ember-cli": "~3.28.0", 69 | "ember-cli-dependency-checker": "^3.2.0", 70 | "ember-cli-inject-live-reload": "^2.0.1", 71 | "ember-cli-sass": "^10.0.0", 72 | "ember-cli-sri": "^2.1.0", 73 | "ember-cli-terser": "^4.0.0", 74 | "ember-disable-prototype-extensions": "^1.1.2", 75 | "ember-faker": "^1.5.0", 76 | "ember-load-initializers": "^2.0.0", 77 | "ember-math-helpers": "~2.11.3", 78 | "ember-qunit": "^9.0.3", 79 | "ember-radio-button": "^2.0.0", 80 | "ember-resolver": "^8.0.2", 81 | "ember-source": "~3.28.0", 82 | "ember-truth-helpers": "^3.0.0", 83 | "ember-try": "^4.0.0", 84 | "eslint": "^8.57.1", 85 | "eslint-config-prettier": "^10.1.5", 86 | "eslint-plugin-ember": "^12.5.0", 87 | "eslint-plugin-n": "^17.18.0", 88 | "eslint-plugin-prettier": "^5.4.0", 89 | "eslint-plugin-qunit": "^8.1.2", 90 | "husky": "^1.3.1", 91 | "latest-version": "^9.0.0", 92 | "loader.js": "^4.2.3", 93 | "qunit": "^2.24.1", 94 | "qunit-dom": "^3.4.0", 95 | "release-it": "^15.5.0", 96 | "sass": "^1.26.10", 97 | "sass-lint": "^1.13.1", 98 | "typescript": "^4.8.4", 99 | "webpack": "^5.0.0" 100 | }, 101 | "engines": { 102 | "node": ">= 14.*" 103 | }, 104 | "ember": { 105 | "edition": "octane" 106 | }, 107 | "ember-addon": { 108 | "configPath": "tests/dummy/config" 109 | }, 110 | "homepage": "https://Addepar.github.io/ember-table", 111 | "resolutions": { 112 | "@ember/test-waiters": "^3.0.2", 113 | "prettier": "1.18.2" 114 | }, 115 | "typesVersions": { 116 | "*": { 117 | "*": [ 118 | "types/*" 119 | ] 120 | } 121 | }, 122 | "volta": { 123 | "node": "18.20.5", 124 | "yarn": "1.22.19" 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | test_page: 'tests/index.html?hidepassed', 5 | disable_watching: true, 6 | launch_in_ci: ['Chrome'], 7 | launch_in_dev: ['Chrome'], 8 | browser_start_timeout: 120, 9 | browser_args: { 10 | Chrome: { 11 | ci: [ 12 | // --no-sandbox is needed when running Chrome inside a container 13 | process.env.CI ? '--no-sandbox' : null, 14 | '--headless', 15 | '--disable-dev-shm-usage', 16 | '--disable-software-rasterizer', 17 | '--mute-audio', 18 | '--remote-debugging-port=0', 19 | '--window-size=1440,900', 20 | ].filter(Boolean), 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /tests/acceptance/docs-test.js: -------------------------------------------------------------------------------- 1 | import { module, test as qunitTest, skip as qunitSkip } from 'qunit'; 2 | import { visit, currentURL, click } from '@ember/test-helpers'; 3 | import { setupApplicationTest } from 'ember-qunit'; 4 | import config from 'dummy/config/environment'; 5 | import TablePage from 'ember-table/test-support/pages/ember-table'; 6 | 7 | let skip = (msg, ...args) => 8 | qunitSkip(`Skip because ember-cli-addon-docs is not installed. ${msg}`, ...args); 9 | let test = config.ADDON_DOCS_INSTALLED ? qunitTest : skip; 10 | 11 | // The nav that contains buttons to show each snippet in a 12 | // `{{docs.demo}}`. See https://github.com/ember-learn/ember-cli-addon-docs/blob/a00a28e33acea463d82c64fa0a712913d70de3f1/addon/components/docs-demo/template.hbs#L11 13 | const DOCS_DEMO_SNIPPET_NAV_SELECTOR = '.docs-demo__snippets-nav'; 14 | 15 | module('Acceptance | docs', function(hooks) { 16 | setupApplicationTest(hooks); 17 | 18 | test('visiting / redirects to /docs', async function(assert) { 19 | await visit('/'); 20 | 21 | assert.strictEqual(currentURL(), '/docs'); 22 | }); 23 | 24 | test('pages linked to by /docs nav all render', async function(assert) { 25 | await visit('/docs'); 26 | 27 | let nav = this.element.querySelector('nav'); 28 | assert.true(!!nav, 'nav exists'); 29 | 30 | let links = Array.from(nav.querySelectorAll('a')).filter(link => 31 | link.getAttribute('href').startsWith('/docs') 32 | ); 33 | assert.true(links.length > 0, `${links.length} nav links found`); 34 | for (let link of links) { 35 | let href = link.getAttribute('href'); 36 | await visit(href); 37 | assert.true(true, `Visited ${href} successfully`); 38 | 39 | let buttonCount = 0; 40 | let docsNavs = Array.from(this.element.querySelectorAll(DOCS_DEMO_SNIPPET_NAV_SELECTOR)); 41 | for (let nav of docsNavs) { 42 | let buttons = Array.from(nav.querySelectorAll('button')); 43 | for (let button of buttons) { 44 | await click(button); 45 | buttonCount++; 46 | } 47 | } 48 | assert.true(true, `Clicked ${buttonCount} snippet buttons on "${href}"`); 49 | 50 | await visit('/docs'); // start over 51 | } 52 | }); 53 | 54 | test('subcolumns docs renders cell content', async function(assert) { 55 | let DemoTable = TablePage.extend({ 56 | scope: '[data-test-demo="docs-example-subcolumns"] [data-test-ember-table]', 57 | }); 58 | 59 | await visit('/docs/guides/header/subcolumns'); 60 | let table = new DemoTable(); 61 | assert.strictEqual( 62 | table.header.headers.objectAt(0).text, 63 | 'A', 64 | 'first header cell renders correctly' 65 | ); 66 | assert.strictEqual( 67 | table.body.rows.objectAt(0).cells.objectAt(0).text, 68 | 'A A', 69 | 'first body cell renders correclty' 70 | ); 71 | }); 72 | 73 | test('autogenerated API docs are present', async function(assert) { 74 | await visit('/docs'); 75 | 76 | let nav = this.element.querySelector('nav'); 77 | assert.true(!!nav, 'nav exists'); 78 | 79 | let navItems = Array.from(nav.querySelectorAll('li')); 80 | 81 | let expectedNavItems = ['API REFERENCE', '']; 82 | 83 | expectedNavItems.forEach(expectedText => { 84 | assert.true( 85 | navItems.some(li => li.innerText.includes(expectedText)), 86 | `"${expectedText}" nav item is exists` 87 | ); 88 | }); 89 | }); 90 | 91 | test('sorting: 2-state sorting works as expected', async function(assert) { 92 | await visit('/docs/guides/header/sorting'); 93 | let DemoTable = TablePage.extend({ 94 | scope: '[data-test-demo="docs-example-2-state-sortings"] [data-test-ember-table]', 95 | }); 96 | 97 | let table = new DemoTable(); 98 | let header = table.headers.objectAt(0); 99 | 100 | assert.false(header.sortIndicator.isPresent, 'precond - sortIndicator is not present'); 101 | 102 | await header.click(); 103 | assert.true( 104 | // eslint-disable-next-line qunit/no-assert-logical-expression 105 | header.sortIndicator.isPresent && header.sortIndicator.isDescending, 106 | 'sort descending' 107 | ); 108 | 109 | await header.click(); 110 | assert.true( 111 | // eslint-disable-next-line qunit/no-assert-logical-expression 112 | header.sortIndicator.isPresent && header.sortIndicator.isAscending, 113 | 'sort ascending' 114 | ); 115 | 116 | await header.click(); 117 | assert.true( 118 | // eslint-disable-next-line qunit/no-assert-logical-expression 119 | header.sortIndicator.isPresent && header.sortIndicator.isDescending, 120 | 'sort cycles back to descending' 121 | ); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from 'ember-resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from 'dummy/config/environment'; 5 | 6 | export default class App extends Application { 7 | modulePrefix = config.modulePrefix; 8 | podModulePrefix = config.podModulePrefix; 9 | Resolver = Resolver; 10 | } 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | -------------------------------------------------------------------------------- /tests/dummy/app/components/custom-cell/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | 3 | export default Component.extend({ 4 | tagName: '', 5 | color: null, 6 | }); 7 | -------------------------------------------------------------------------------- /tests/dummy/app/components/custom-cell/template.hbs: -------------------------------------------------------------------------------- 1 | {{! BEGIN-SNIPPET custom-cell.hbs }} 2 |
3 | Cell {{yield}} 4 |
5 | {{! END-SNIPPET }} 6 | -------------------------------------------------------------------------------- /tests/dummy/app/components/custom-header/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | 3 | export default Component.extend({ 4 | tagName: '', 5 | color: null, 6 | }); 7 | -------------------------------------------------------------------------------- /tests/dummy/app/components/custom-header/template.hbs: -------------------------------------------------------------------------------- 1 | {{! BEGIN-SNIPPET custom-header.hbs }} 2 |
3 | Column {{yield}} 4 |
5 | {{! END-SNIPPET }} 6 | -------------------------------------------------------------------------------- /tests/dummy/app/components/custom-row/component.js: -------------------------------------------------------------------------------- 1 | import EmberTableRow from 'ember-table/components/ember-tr/component'; 2 | 3 | export default EmberTableRow.extend({ 4 | classNames: ['custom-row'], 5 | }); 6 | -------------------------------------------------------------------------------- /tests/dummy/app/components/examples/aborting-a-selection/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { action } from '@ember/object'; 3 | 4 | let shouldAbortSelection; 5 | 6 | export default class AbortingASelectionExampleComponent extends Component { 7 | // BEGIN-SNIPPET docs-example-aborting-a-selection.js 8 | @action 9 | selectRows(selection, { abort }) { 10 | if (shouldAbortSelection(selection)) { 11 | abort(); 12 | return; 13 | } 14 | 15 | this.selection = selection; 16 | } 17 | // END-SNIPPET 18 | } 19 | -------------------------------------------------------------------------------- /tests/dummy/app/components/examples/aborting-a-selection/template.hbs: -------------------------------------------------------------------------------- 1 | {{! BEGIN-SNIPPET docs-example-aborting-a-selection.hbs }} 2 | 3 | 4 | 5 | 10 | 11 | {{! END-SNIPPET }} 12 | -------------------------------------------------------------------------------- /tests/dummy/app/components/examples/fixed-columns-dynamic/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | 3 | export default Component.extend(); 4 | -------------------------------------------------------------------------------- /tests/dummy/app/components/examples/fixed-columns-dynamic/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{! BEGIN-SNIPPET docs-example-dynamic-fixed-columns.hbs }} 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{column.name}} 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{! END-SNIPPET }} 17 |
18 | -------------------------------------------------------------------------------- /tests/dummy/app/components/examples/infinite-scroll/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { computed } from '@ember/object'; 3 | import { task, timeout } from 'ember-concurrency'; 4 | import { A } from '@ember/array'; 5 | 6 | export default Component.extend({ 7 | // BEGIN-SNIPPET docs-example-infinite-scroll.js 8 | // count of records loaded so far 9 | offset: 0, 10 | 11 | // count of records per "page" 12 | limit: 20, 13 | 14 | // substitute total count of records from API response meta data 15 | maxRows: 100, 16 | 17 | // center spinner horizontally in scroll viewport 18 | centerSpinner: true, 19 | 20 | canLoadMore: computed('offset', 'maxRows', function() { 21 | return this.offset < this.maxRows; 22 | }), 23 | 24 | columns: computed(function() { 25 | return [ 26 | { name: 'ID', valuePath: 'id', width: 180 }, 27 | { name: 'A', valuePath: 'a', width: 180 }, 28 | { name: 'B', valuePath: 'b', width: 180 }, 29 | { name: 'C', valuePath: 'c', width: 180 }, 30 | ]; 31 | }), 32 | 33 | rows: computed(function() { 34 | return A(); 35 | }), 36 | 37 | didInsertElement() { 38 | this._super(...arguments); 39 | this.loadMore.perform(); 40 | }, 41 | 42 | // ember-concurrency task 43 | loadMore: task(function*() { 44 | let offset = this.offset; 45 | let limit = this.limit; 46 | 47 | if (!this.canLoadMore) { 48 | return; 49 | } 50 | 51 | // substitute paginated API request 52 | yield timeout(1000); 53 | 54 | let newRows = []; 55 | for (let i = 0; i < limit; i++) { 56 | newRows.push({ id: offset + i + 1, a: 'A', b: 'B', c: 'C' }); 57 | } 58 | 59 | this.rows.pushObjects(newRows); 60 | this.set('offset', offset + limit); 61 | }).drop(), 62 | 63 | // END-SNIPPET 64 | }); 65 | -------------------------------------------------------------------------------- /tests/dummy/app/components/examples/infinite-scroll/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | 6 |
7 | 8 |
9 | {{! BEGIN-SNIPPET docs-example-infinite-scroll.hbs }} 10 | 11 | 12 | 13 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | {{! END-SNIPPET }} 27 |
28 | -------------------------------------------------------------------------------- /tests/dummy/app/components/examples/row-selection/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | 3 | export default Component.extend(); 4 | -------------------------------------------------------------------------------- /tests/dummy/app/components/examples/row-selection/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{! BEGIN-SNIPPET docs-example-row-selection.hbs }} 3 | 4 | 5 | 6 | 11 | 12 | {{! END-SNIPPET }} 13 |
14 | -------------------------------------------------------------------------------- /tests/dummy/app/components/examples/selected-rows/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | 3 | export default Component.extend(); 4 | -------------------------------------------------------------------------------- /tests/dummy/app/components/examples/selected-rows/template.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{! BEGIN-SNIPPET docs-example-selected-rows.hbs }} 3 | 4 | 5 | 6 | 11 | 12 | {{! END-SNIPPET }} 13 |
14 | -------------------------------------------------------------------------------- /tests/dummy/app/components/examples/selection-modes/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | 3 | export default Component.extend(); 4 | -------------------------------------------------------------------------------- /tests/dummy/app/components/examples/selection-modes/template.hbs: -------------------------------------------------------------------------------- 1 | {{! BEGIN-SNIPPET docs-example-selection-modes.hbs }} 2 |
3 | 4 | 5 | 6 | 16 | 17 |
18 |
19 |

Current selection

20 |
{{currentSelection}}
21 |
22 |
23 |

rowSelectionMode

24 | 25 | 26 | 27 |
28 |
29 |

checkboxSelectionMode

30 | 31 | 32 | 33 |
34 |
35 |

selectingChildrenSelectsParent

36 | 37 |
38 | 39 | {{! END-SNIPPET }} -------------------------------------------------------------------------------- /tests/dummy/app/components/examples/sorting-empty-values/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { computed } from '@ember/object'; 3 | import faker from 'faker'; 4 | import { getRandomInt } from 'dummy/utils/generators'; 5 | 6 | export default Component.extend({ 7 | // BEGIN-SNIPPET docs-example-sorting-empty-values.js 8 | columns: computed(function() { 9 | return [ 10 | { name: 'Product', valuePath: 'name' }, 11 | { name: 'Material', valuePath: 'material' }, 12 | { name: 'Price', valuePath: 'price' }, 13 | { name: 'Sold', valuePath: 'sold' }, 14 | { name: 'Unsold', valuePath: 'unsold' }, 15 | { name: 'Total Revenue', valuePath: 'totalRevenue' }, 16 | ]; 17 | }), 18 | 19 | sortEmptyLast: true, 20 | // END-SNIPPET 21 | 22 | rows: computed(function() { 23 | let rows = []; 24 | 25 | for (let k = 0; k < 10; k++) { 26 | let sold = getRandomInt(100, 10); 27 | let unsold = getRandomInt(100, 10); 28 | let price = getRandomInt(50, 10); 29 | let totalRevenue = price * sold; 30 | 31 | let product = { 32 | name: faker.commerce.productName(), 33 | material: faker.commerce.productMaterial(), 34 | price: `$${price}`, 35 | sold, 36 | unsold, 37 | totalRevenue: `$${totalRevenue}`, 38 | }; 39 | 40 | rows.push(product); 41 | } 42 | 43 | for (let k = 0; k < 5; k++) { 44 | let sold = getRandomInt(100, 10); 45 | let unsold = getRandomInt(100, 10); 46 | let price = getRandomInt(50, 10); 47 | let totalRevenue = price * sold; 48 | 49 | let product = { 50 | name: faker.commerce.productName(), 51 | material: '', 52 | price: `$${price}`, 53 | sold, 54 | unsold, 55 | totalRevenue: `$${totalRevenue}`, 56 | }; 57 | 58 | rows.push(product); 59 | } 60 | 61 | return rows; 62 | }), 63 | }); 64 | -------------------------------------------------------------------------------- /tests/dummy/app/components/examples/sorting-empty-values/template.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{! BEGIN-SNIPPET docs-example-sorting-empty-values.hbs }} 4 |
5 | 9 |
10 |
11 | 12 | 22 | 23 | 24 | 25 |
26 | {{! END-SNIPPET }} 27 |
28 | 29 | 30 | 31 |
-------------------------------------------------------------------------------- /tests/dummy/app/components/index-content/template.md: -------------------------------------------------------------------------------- 1 | Ember Table is a power table made for users who need a full-fledged, 2 | fully-customizable table component for their apps. It is built to be flexible, 3 | scalable, and ergonomic for day-to-day use. 4 | 5 | Unlike other table components, Ember Table seeks to provide as many common table 6 | features as it can _without_ sacrificing flexibility or composeability. You can 7 | still use standard component idioms to customize, extend, and build on top of 8 | Ember Table at every level, ensuring that it never gets in your way or prevents 9 | you from doing what you want. 10 | 11 | ## Features 12 | 13 | - Fixed headers and footers 14 | - Fixed columns 15 | - Row selection 16 | - Row sorting 17 | - Tree tables (with group collapsing) 18 | - Column resizing and reordering 19 | - Nested subcolumns (e.g. to create pivot tables) 20 | - Scalability - Can render thousands of rows performantly 21 | -------------------------------------------------------------------------------- /tests/dummy/app/components/main/table-meta-data/cell-selection/component.js: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { computed, action } from '@ember/object'; 3 | import { generateRows } from 'dummy/utils/generators'; 4 | 5 | export default Component.extend({ 6 | rows: computed(function() { 7 | return generateRows(100); 8 | }), 9 | 10 | columns: computed(function() { 11 | return [ 12 | { name: 'A', valuePath: 'A' }, 13 | { name: 'B', valuePath: 'B' }, 14 | { name: 'C', valuePath: 'C' }, 15 | { name: 'D', valuePath: 'D' }, 16 | { name: 'E', valuePath: 'E' }, 17 | { name: 'F', valuePath: 'F' }, 18 | { name: 'G', valuePath: 'G' }, 19 | ]; 20 | }), 21 | 22 | // BEGIN-SNIPPET table-meta-data-cell-selection.js 23 | setSelected: action(function(cellMeta, columnMeta, rowMeta) { 24 | // If we have selected before, unselect the previous selection 25 | if (this._hasSelection) { 26 | this._lastSelectedCellMeta.set('selected', false); 27 | this._lastSelectedColumnMeta.set('selected', false); 28 | this._lastSelectedRowMeta.set('selected', false); 29 | } 30 | 31 | // Set selection on the meta objects 32 | cellMeta.set('selected', true); 33 | columnMeta.set('selected', true); 34 | rowMeta.set('selected', true); 35 | 36 | // Store the meta objects to unset in the future 37 | this._lastSelectedCellMeta = cellMeta; 38 | this._lastSelectedColumnMeta = columnMeta; 39 | this._lastSelectedRowMeta = rowMeta; 40 | this._hasSelection = true; 41 | }), 42 | // END-SNIPPET 43 | }); 44 | -------------------------------------------------------------------------------- /tests/dummy/app/components/main/table-meta-data/cell-selection/template.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | {{! BEGIN-SNIPPET table-meta-data-cell-selection.hbs }} 6 | 7 | 8 | 9 | 14 | {{column.name}} 15 | 16 | 17 | 18 | 19 | 20 | 25 | 30 |
31 | {{cell}} 32 |
33 |
34 |
35 |
36 |
37 | {{! END-SNIPPET }} 38 |
39 |
40 | 41 | 42 | 43 |
44 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/docs/guides/body/occlusion.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { computed } from '@ember/object'; 3 | 4 | export default Controller.extend({ 5 | // BEGIN-SNIPPET docs-example-occlusion.js 6 | columns: computed(function() { 7 | return [ 8 | { name: 'A', valuePath: 'A', width: 180 }, 9 | { name: 'B', valuePath: 'B', width: 180 }, 10 | { name: 'C', valuePath: 'C', width: 180 }, 11 | { name: 'D', valuePath: 'D', width: 180 }, 12 | ]; 13 | }), 14 | 15 | rows: computed(function() { 16 | return [ 17 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 18 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 19 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 20 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 21 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 22 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 23 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 24 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 25 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 26 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 27 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 28 | ]; 29 | }), 30 | // END-SNIPPET 31 | }); 32 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/docs/guides/body/row-selection.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { computed } from '@ember/object'; 3 | 4 | export default Controller.extend({ 5 | // BEGIN-SNIPPET docs-example-row-selection.js 6 | columns: computed(function() { 7 | return [ 8 | { name: 'A', valuePath: 'A', width: 180 }, 9 | { name: 'B', valuePath: 'B', width: 180 }, 10 | { name: 'C', valuePath: 'C', width: 180 }, 11 | { name: 'D', valuePath: 'D', width: 180 }, 12 | ]; 13 | }), 14 | 15 | rows: computed(function() { 16 | return [ 17 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 18 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 19 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 20 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 21 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 22 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 23 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 24 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 25 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 26 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 27 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 28 | ]; 29 | }), 30 | // END-SNIPPET 31 | 32 | // BEGIN-SNIPPET docs-example-selected-rows.js 33 | init() { 34 | this._super(...arguments); 35 | 36 | let [rowWithChildren] = this.rowWithChildren; 37 | 38 | this.preselection = [rowWithChildren]; 39 | }, 40 | 41 | rowWithChildren: computed(function() { 42 | return [ 43 | { 44 | A: 'A', 45 | B: 'B', 46 | C: 'C', 47 | D: 'D', 48 | 49 | children: [ 50 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 51 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 52 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 53 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 54 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 55 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 56 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 57 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 58 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 59 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 60 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 61 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 62 | ], 63 | }, 64 | ]; 65 | }), 66 | // END-SNIPPET 67 | 68 | // BEGIN-SNIPPET docs-example-selection-modes.js 69 | rowSelectionMode: 'multiple', 70 | checkboxSelectionMode: 'multiple', 71 | selectingChildrenSelectsParent: true, 72 | 73 | rowsWithChildren: computed(function() { 74 | let makeRow = (id, { children } = { children: [] }) => { 75 | return { 76 | A: `A${id}`, 77 | B: 'B', 78 | C: 'C', 79 | D: 'D', 80 | children, 81 | }; 82 | }; 83 | return [ 84 | makeRow(1, { 85 | children: [ 86 | makeRow(2, { 87 | children: [makeRow(3), makeRow(4), makeRow(5)], 88 | }), 89 | makeRow(6), 90 | makeRow(7), 91 | makeRow(8, { 92 | children: [makeRow(9), makeRow(10), makeRow(11)], 93 | }), 94 | ], 95 | }), 96 | ]; 97 | }), 98 | 99 | currentSelection: computed('demoSelection', function() { 100 | let selection = this.demoSelection; 101 | if (!selection || selection.length === 0) { 102 | return 'Nothing selected'; 103 | } else { 104 | if (Array.isArray(selection)) { 105 | return `Array: [${selection.map(row => row.A).join(',')}]`; 106 | } else { 107 | let row = selection; 108 | return `Single: ${row.A}`; 109 | } 110 | } 111 | }), 112 | // END-SNIPPET 113 | }); 114 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/docs/guides/body/rows-and-trees.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { computed } from '@ember/object'; 3 | 4 | export default Controller.extend({ 5 | // BEGIN-SNIPPET docs-example-rows.js 6 | columns: computed(function() { 7 | return [ 8 | { name: 'A', valuePath: 'A', width: 180 }, 9 | { name: 'B', valuePath: 'B', width: 180 }, 10 | { name: 'C', valuePath: 'C', width: 180 }, 11 | { name: 'D', valuePath: 'D', width: 180 }, 12 | ]; 13 | }), 14 | 15 | rows: computed(function() { 16 | return [ 17 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 18 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 19 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 20 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 21 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 22 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 23 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 24 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 25 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 26 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 27 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 28 | ]; 29 | }), 30 | // END-SNIPPET 31 | 32 | // BEGIN-SNIPPET docs-example-tree-rows.js 33 | treeEnabled: true, 34 | 35 | rowsWithChildren: computed(function() { 36 | return [ 37 | { 38 | A: 'A', 39 | B: 'B', 40 | C: 'C', 41 | D: 'D', 42 | 43 | children: [ 44 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 45 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 46 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 47 | ], 48 | }, 49 | { 50 | A: 'A', 51 | B: 'B', 52 | C: 'C', 53 | D: 'D', 54 | 55 | children: [ 56 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 57 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 58 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 59 | ], 60 | }, 61 | { 62 | A: 'A', 63 | B: 'B', 64 | C: 'C', 65 | D: 'D', 66 | 67 | children: [ 68 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 69 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 70 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 71 | ], 72 | }, 73 | ]; 74 | }), 75 | // END-SNIPPET 76 | 77 | // BEGIN-SNIPPET docs-example-rows-with-collapse.js 78 | collapseEnabled: true, 79 | 80 | rowsWithCollapse: computed(function() { 81 | return [ 82 | { 83 | A: 'A', 84 | B: 'B', 85 | C: 'C', 86 | D: 'D', 87 | 88 | isCollapsed: true, 89 | children: [ 90 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 91 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 92 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 93 | ], 94 | }, 95 | { 96 | A: 'A', 97 | B: 'B', 98 | C: 'C', 99 | D: 'D', 100 | 101 | isCollapsed: true, 102 | children: [ 103 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 104 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 105 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 106 | ], 107 | }, 108 | { 109 | A: 'A', 110 | B: 'B', 111 | C: 'C', 112 | D: 'D', 113 | 114 | isCollapsed: true, 115 | children: [ 116 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 117 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 118 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 119 | ], 120 | }, 121 | ]; 122 | }), 123 | // END-SNIPPET 124 | }); 125 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/docs/guides/header/columns.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { computed } from '@ember/object'; 3 | import { generateRows } from 'dummy/utils/generators'; 4 | 5 | export default Controller.extend({ 6 | rows: computed(function() { 7 | return generateRows(100); 8 | }), 9 | 10 | // BEGIN-SNIPPET docs-example-columns.js 11 | columns: computed(function() { 12 | return [ 13 | { name: 'A', valuePath: 'A' }, 14 | { name: 'B', valuePath: 'B' }, 15 | { name: 'C', valuePath: 'C' }, 16 | { name: 'D', valuePath: 'D' }, 17 | { name: 'E', valuePath: 'E' }, 18 | { name: 'F', valuePath: 'F' }, 19 | { name: 'G', valuePath: 'G' }, 20 | ]; 21 | }), 22 | // END-SNIPPET 23 | 24 | // BEGIN-SNIPPET docs-example-columns-with-components.js 25 | columnsWithComponents: computed(function() { 26 | return [ 27 | { heading: 'A', valuePath: 'A', component: 'custom-header', color: 'navy' }, 28 | { heading: 'B', valuePath: 'B', component: 'custom-header', color: 'blue' }, 29 | { heading: 'C', valuePath: 'C', component: 'custom-header', color: 'aqua' }, 30 | { heading: 'D', valuePath: 'D', component: 'custom-header', color: 'teal' }, 31 | { heading: 'E', valuePath: 'E', component: 'custom-header', color: 'orange' }, 32 | { heading: 'F', valuePath: 'F', component: 'custom-header', color: 'red' }, 33 | { heading: 'G', valuePath: 'G', component: 'custom-header', color: 'maroon' }, 34 | ]; 35 | }), 36 | // END-SNIPPET 37 | 38 | // BEGIN-SNIPPET docs-example-columns-with-widths.js 39 | columnsWithWidths: computed(function() { 40 | return [ 41 | { name: 'A', valuePath: 'A', width: 100 }, 42 | { name: 'B', valuePath: 'B', width: 100 }, 43 | { name: 'C', valuePath: 'C', width: 100 }, 44 | { name: 'D', valuePath: 'D', width: 100 }, 45 | { name: 'E', valuePath: 'E', width: 100 }, 46 | { name: 'F', valuePath: 'F', width: 100 }, 47 | { name: 'G', valuePath: 'G', width: 100 }, 48 | ]; 49 | }), 50 | // END-SNIPPET 51 | 52 | // BEGIN-SNIPPET docs-example-column-resize-reorder.js 53 | resizeEnabled: false, 54 | reorderEnabled: false, 55 | resizeModeFluid: false, 56 | // END-SNIPPET 57 | 58 | resizeCount: 0, 59 | reorderCount: 0, 60 | 61 | // BEGIN-SNIPPET docs-example-text-align.js 62 | columnsWithTextAlign: computed(function() { 63 | return [ 64 | { name: 'No alignment', valuePath: 'A' }, 65 | { name: 'Left alignment', valuePath: 'B', textAlign: 'left' }, 66 | { name: 'Center alignment', valuePath: 'C', textAlign: 'center' }, 67 | { name: 'Right alignment', valuePath: 'D', textAlign: 'right' }, 68 | ]; 69 | }), 70 | // END-SNIPPET 71 | }); 72 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/docs/guides/header/fixed-columns.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { A as emberA } from '@ember/array'; 3 | import { set, action } from '@ember/object'; 4 | 5 | import { generateRows } from 'dummy/utils/generators'; 6 | import { computed } from '@ember/object'; 7 | 8 | export default Controller.extend({ 9 | rows: computed(function() { 10 | return generateRows(100); 11 | }), 12 | 13 | // BEGIN-SNIPPET docs-example-fixed-columns.js 14 | columns: computed(function() { 15 | return [ 16 | { name: 'A', valuePath: 'A', isFixed: 'left' }, 17 | { name: 'B', valuePath: 'B' }, 18 | { name: 'C', valuePath: 'C' }, 19 | { name: 'D', valuePath: 'D' }, 20 | { name: 'E', valuePath: 'E' }, 21 | { name: 'F', valuePath: 'F' }, 22 | { name: 'G', valuePath: 'G' }, 23 | { name: 'H', valuePath: 'H' }, 24 | { name: 'I', valuePath: 'I' }, 25 | { name: 'J', valuePath: 'J' }, 26 | { name: 'K', valuePath: 'K', isFixed: 'right' }, 27 | ]; 28 | }), 29 | // END-SNIPPET 30 | 31 | // BEGIN-SNIPPET docs-example-out-of-order-fixed-columns.js 32 | outOfOrderFixedColumns: computed(function() { 33 | return [ 34 | { name: 'A', valuePath: 'A', isFixed: 'right' }, 35 | { name: 'B', valuePath: 'B' }, 36 | { name: 'C', valuePath: 'C' }, 37 | { name: 'D', valuePath: 'D', isFixed: 'left' }, 38 | { name: 'E', valuePath: 'E' }, 39 | { name: 'F', valuePath: 'F' }, 40 | { name: 'G', valuePath: 'G' }, 41 | { name: 'H', valuePath: 'H', isFixed: 'right' }, 42 | { name: 'I', valuePath: 'I' }, 43 | { name: 'J', valuePath: 'J' }, 44 | { name: 'K', valuePath: 'K', isFixed: 'left' }, 45 | ]; 46 | }), 47 | // END-SNIPPET 48 | 49 | // BEGIN-SNIPPET docs-example-dynamic-fixed-columns.js 50 | dynamicFixedColumns: computed(function() { 51 | return emberA([ 52 | { name: 'A', valuePath: 'A' }, 53 | { name: 'B', valuePath: 'B' }, 54 | { name: 'C', valuePath: 'C' }, 55 | { name: 'D', valuePath: 'D' }, 56 | { name: 'E', valuePath: 'E' }, 57 | { name: 'F', valuePath: 'F' }, 58 | { name: 'G', valuePath: 'G' }, 59 | { name: 'H', valuePath: 'H' }, 60 | { name: 'I', valuePath: 'I' }, 61 | { name: 'J', valuePath: 'J' }, 62 | { name: 'K', valuePath: 'K' }, 63 | ]); 64 | }), 65 | // END-SNIPPET 66 | 67 | toggleFixed: action(function(column) { 68 | if (column.isFixed) { 69 | set(column, 'isFixed', false); 70 | } else { 71 | set(column, 'isFixed', 'left'); 72 | } 73 | }), 74 | }); 75 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/docs/guides/header/scroll-indicators.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { computed } from '@ember/object'; 3 | 4 | import { generateRows } from 'dummy/utils/generators'; 5 | 6 | export default Controller.extend({ 7 | // BEGIN-SNIPPET docs-example-scroll-indicators.js 8 | columns: computed(function() { 9 | return [ 10 | { name: 'A', valuePath: 'A' }, 11 | { name: 'B', valuePath: 'B' }, 12 | { name: 'C', valuePath: 'C' }, 13 | { name: 'D', valuePath: 'D' }, 14 | { name: 'E', valuePath: 'E' }, 15 | { name: 'F', valuePath: 'F' }, 16 | { name: 'G', valuePath: 'G' }, 17 | { name: 'H', valuePath: 'H' }, 18 | { name: 'I', valuePath: 'I' }, 19 | { name: 'J', valuePath: 'J' }, 20 | { name: 'K', valuePath: 'K' }, 21 | ]; 22 | }), 23 | // END-SNIPPET 24 | 25 | // BEGIN-SNIPPET docs-example-scroll-indicators-with-fixed.js 26 | columnsWithFixed: computed(function() { 27 | return [ 28 | { name: 'A', valuePath: 'A', isFixed: 'left' }, 29 | { name: 'B', valuePath: 'B' }, 30 | { name: 'C', valuePath: 'C' }, 31 | { name: 'D', valuePath: 'D' }, 32 | { name: 'E', valuePath: 'E' }, 33 | { name: 'F', valuePath: 'F' }, 34 | { name: 'G', valuePath: 'G' }, 35 | { name: 'H', valuePath: 'H' }, 36 | { name: 'I', valuePath: 'I' }, 37 | { name: 'J', valuePath: 'J' }, 38 | { name: 'K', valuePath: 'K', isFixed: 'right' }, 39 | ]; 40 | }), 41 | // END-SNIPPET 42 | 43 | // BEGIN-SNIPPET docs-example-scroll-indicators-with-footer.js 44 | columnsWithFooter: computed(function() { 45 | return [ 46 | { name: 'A', valuePath: 'A' }, 47 | { name: 'B', valuePath: 'B' }, 48 | { name: 'C', valuePath: 'C' }, 49 | { name: 'D', valuePath: 'D' }, 50 | { name: 'E', valuePath: 'E' }, 51 | { name: 'F', valuePath: 'F' }, 52 | { name: 'G', valuePath: 'G' }, 53 | { name: 'H', valuePath: 'H' }, 54 | { name: 'I', valuePath: 'I' }, 55 | { name: 'J', valuePath: 'J' }, 56 | { name: 'K', valuePath: 'K' }, 57 | ]; 58 | }), 59 | // END-SNIPPET 60 | 61 | rows: computed(function() { 62 | return generateRows(100); 63 | }), 64 | 65 | footerRows: computed(function() { 66 | return generateRows(100, 1, (row, key) => { 67 | return String.fromCharCode(key.charCodeAt(0) + 7); 68 | }); 69 | }), 70 | }); 71 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/docs/guides/header/size-constraints.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { computed } from '@ember/object'; 3 | import { addObserver } from '@ember/object/observers'; // eslint-disable-line no-restricted-imports 4 | import { generateRows } from 'dummy/utils/generators'; 5 | 6 | const defaultResizeMode = { 7 | 'eq-container': 'fluid', 8 | 'eq-container-slack': 'standard', 9 | 'gte-container': 'standard', 10 | 'gte-container-slack': 'standard', 11 | 'lte-container': 'standard', 12 | }; 13 | 14 | export default Controller.extend({ 15 | rows: computed(function() { 16 | return generateRows(100); 17 | }), 18 | 19 | widthConstraint: 'eq-container', 20 | fillMode: 'equal-column', 21 | resizeMode: 'fluid', 22 | 23 | init() { 24 | this._super(...arguments); 25 | 26 | // eslint-disable-next-line ember/no-observers 27 | addObserver(this, 'widthConstraint', this.setDefaultResizeMode); 28 | }, 29 | 30 | setDefaultResizeMode() { 31 | let widthConstraint = this.widthConstraint; 32 | let resizeMode = defaultResizeMode[widthConstraint]; 33 | 34 | if (resizeMode) { 35 | this.set('resizeMode', resizeMode); 36 | } 37 | }, 38 | 39 | // BEGIN-SNIPPET docs-example-header-size-constraints.js 40 | columns: computed(function() { 41 | return [ 42 | { name: 'A', valuePath: 'A' }, 43 | { name: 'B', valuePath: 'B' }, 44 | { name: 'C', valuePath: 'C' }, 45 | { name: 'D', valuePath: 'D' }, 46 | ]; 47 | }), 48 | // END-SNIPPET 49 | }); 50 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/docs/guides/header/sorting.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { computed, action } from '@ember/object'; 3 | import faker from 'faker'; 4 | import { getRandomInt } from 'dummy/utils/generators'; 5 | 6 | export default Controller.extend({ 7 | // BEGIN-SNIPPET docs-example-sortings.js 8 | columns: computed(function() { 9 | return [ 10 | { name: 'Company ▸ Department ▸ Product', valuePath: 'name' }, 11 | { name: 'Price', valuePath: 'price' }, 12 | { name: 'Sold', valuePath: 'sold' }, 13 | { name: 'Unsold', valuePath: 'unsold' }, 14 | { name: 'Total Revenue', valuePath: 'totalRevenue' }, 15 | ]; 16 | }), 17 | // END-SNIPPET 18 | 19 | rows: computed(function() { 20 | let rows = []; 21 | 22 | for (let i = 0; i < getRandomInt(5, 2); i++) { 23 | let companyRow = { 24 | name: faker.company.companyName(), 25 | price: 'N/A', 26 | sold: 0, 27 | unsold: 0, 28 | totalRevenue: 0, 29 | children: [], 30 | }; 31 | 32 | for (let j = 0; j < getRandomInt(5, 2); j++) { 33 | let departmentRow = { 34 | name: faker.commerce.department(), 35 | price: 'N/A', 36 | sold: 0, 37 | unsold: 0, 38 | totalRevenue: 0, 39 | children: [], 40 | }; 41 | 42 | for (let k = 0; k < getRandomInt(100, 10); k++) { 43 | let sold = getRandomInt(100, 10); 44 | let unsold = getRandomInt(100, 10); 45 | let price = getRandomInt(50, 10); 46 | let totalRevenue = price * sold; 47 | 48 | let product = { 49 | name: faker.commerce.productName(), 50 | price: `$${price}`, 51 | sold, 52 | unsold, 53 | totalRevenue: `$${totalRevenue}`, 54 | }; 55 | 56 | departmentRow.sold += sold; 57 | departmentRow.unsold += unsold; 58 | departmentRow.totalRevenue += totalRevenue; 59 | departmentRow.children.push(product); 60 | } 61 | 62 | companyRow.sold += departmentRow.sold; 63 | companyRow.unsold += departmentRow.unsold; 64 | companyRow.totalRevenue += departmentRow.totalRevenue; 65 | 66 | departmentRow.totalRevenue = `$${departmentRow.totalRevenue}`; 67 | 68 | companyRow.children.push(departmentRow); 69 | } 70 | 71 | companyRow.totalRevenue = `$${companyRow.totalRevenue}`; 72 | 73 | rows.push(companyRow); 74 | } 75 | 76 | return rows; 77 | }), 78 | 79 | // BEGIN-SNIPPET docs-example-2-state-sortings.js 80 | twoStateSorting: action(function(sorts) { 81 | if (sorts.length > 1) { 82 | // multi-column sort, default behavior 83 | this.set('sorts', sorts); 84 | return; 85 | } 86 | 87 | let hasExistingSort = this.sorts && this.sorts.length; 88 | let isDefaultSort = !sorts.length; 89 | 90 | if (hasExistingSort && isDefaultSort) { 91 | // override empty sorts with reversed previous sort 92 | let newSorts = [ 93 | { 94 | valuePath: this.sorts[0].valuePath, 95 | isAscending: !this.sorts[0].isAscending, 96 | }, 97 | ]; 98 | this.set('sorts', newSorts); 99 | return; 100 | } 101 | 102 | this.set('sorts', sorts); 103 | }), 104 | // END-SNIPPET 105 | }); 106 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/docs/guides/header/subcolumns.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { A as emberA } from '@ember/array'; 3 | import { computed } from '@ember/object'; 4 | import { generateRows, generateColumn } from 'dummy/utils/generators'; 5 | 6 | const COLUMN_COUNT = 4; 7 | 8 | export default Controller.extend({ 9 | rows: computed(function() { 10 | return generateRows(100); 11 | }), 12 | 13 | // BEGIN-SNIPPET docs-example-subcolumns.js 14 | simpleColumns: computed(function() { 15 | return [ 16 | { 17 | name: 'A', 18 | subcolumns: [ 19 | { name: 'A A', valuePath: 'A A' }, 20 | { name: 'A B', valuePath: 'A B' }, 21 | { name: 'A C', valuePath: 'A C' }, 22 | ], 23 | }, 24 | { 25 | name: 'B', 26 | subcolumns: [ 27 | { name: 'B A', valuePath: 'B A' }, 28 | { name: 'B B', valuePath: 'B B' }, 29 | { name: 'B C', valuePath: 'B C' }, 30 | ], 31 | }, 32 | { 33 | name: 'C', 34 | subcolumns: [ 35 | { name: 'C A', valuePath: 'C A' }, 36 | { name: 'C B', valuePath: 'C B' }, 37 | { name: 'C C', valuePath: 'C C' }, 38 | ], 39 | }, 40 | ]; 41 | }), 42 | // END-SNIPPET 43 | 44 | complexColumns: computed(function() { 45 | let columns = emberA(); 46 | 47 | for (let i = 0; i < COLUMN_COUNT; i++) { 48 | let column = generateColumn(i, { subcolumns: [] }); 49 | 50 | if (Math.random() > 0.5) { 51 | for (let j = 0; j < COLUMN_COUNT - 1; j++) { 52 | let subcolumn = generateColumn([i, j], { subcolumns: [] }); 53 | 54 | if (Math.random() > 0.5) { 55 | for (let k = 0; k < COLUMN_COUNT - 2; k++) { 56 | subcolumn.subcolumns.push(generateColumn([i, j, k])); 57 | } 58 | } 59 | 60 | column.subcolumns.push(subcolumn); 61 | } 62 | } 63 | 64 | columns.pushObject(column); 65 | } 66 | 67 | return columns; 68 | }), 69 | }); 70 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/docs/guides/main/basic-table.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { computed } from '@ember/object'; 3 | 4 | export default Controller.extend({ 5 | // BEGIN-SNIPPET docs-example-basic-table.js 6 | columns: computed(function() { 7 | return [ 8 | { name: 'A', valuePath: 'A', width: 180 }, 9 | { name: 'B', valuePath: 'B', width: 180 }, 10 | { name: 'C', valuePath: 'C', width: 180 }, 11 | { name: 'D', valuePath: 'D', width: 180 }, 12 | ]; 13 | }), 14 | 15 | rows: computed(function() { 16 | return [ 17 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 18 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 19 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 20 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 21 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 22 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 23 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 24 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 25 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 26 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 27 | { A: 'A', B: 'B', C: 'C', D: 'D' }, 28 | ]; 29 | }), 30 | // END-SNIPPET 31 | }); 32 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/docs/guides/main/styling-the-table.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | 3 | export default class SimpleController extends Controller {} 4 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/docs/guides/main/table-meta-data.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { computed } from '@ember/object'; 3 | import { generateRows } from 'dummy/utils/generators'; 4 | 5 | export default Controller.extend({ 6 | rows: computed(function() { 7 | return generateRows(100); 8 | }), 9 | 10 | columns: computed(function() { 11 | return [ 12 | { name: 'A', valuePath: 'A' }, 13 | { name: 'B', valuePath: 'B' }, 14 | { name: 'C', valuePath: 'C' }, 15 | { name: 'D', valuePath: 'D' }, 16 | { name: 'E', valuePath: 'E' }, 17 | { name: 'F', valuePath: 'F' }, 18 | { name: 'G', valuePath: 'G' }, 19 | ]; 20 | }), 21 | }); 22 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/scenarios/performance.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { computed, action } from '@ember/object'; 3 | import { generateRows, generateColumns } from 'dummy/utils/generators'; 4 | 5 | export default Controller.extend({ 6 | rows: computed(function() { 7 | let rows = generateRows(10, 3, (row, key) => `${row.id}${key}`); 8 | 9 | rows[0].children[0].children[0].children = generateRows(10, 1, (row, key) => `${row.id}${key}`); 10 | 11 | return rows; 12 | }), 13 | 14 | columns: computed(function() { 15 | let columns = generateColumns(20); 16 | 17 | columns[0].width = 300; 18 | columns[0].isResizable = false; 19 | columns[0].isReorderable = false; 20 | 21 | columns[1].subcolumns = generateColumns(3); 22 | columns[1].subcolumns[0].isReorderable = false; 23 | columns[1].subcolumns[1].isResizable = false; 24 | columns[1].subcolumns[2].isSortable = false; 25 | 26 | return columns; 27 | }), 28 | 29 | onSelect: action(function(selection) { 30 | this.set('selection', selection); 31 | }), 32 | onUpdateSorts: action(function(sorts) { 33 | this.set('sorts', sorts); 34 | }), 35 | }); 36 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/scenarios/simple.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { computed } from '@ember/object'; 3 | import { generateRows, generateColumns } from 'dummy/utils/generators'; 4 | 5 | export default Controller.extend({ 6 | rows: computed(function() { 7 | return generateRows(100); 8 | }), 9 | 10 | columns: computed(function() { 11 | return generateColumns(7); 12 | }), 13 | }); 14 | -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Ember Table 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 39 | 40 | 41 | {{content-for "head-footer"}} 42 | 43 | 44 | {{content-for "body"}} 45 | 46 | 47 | 48 | 49 | {{content-for "body-footer"}} 50 | 51 | 52 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import AddonDocsRouter, { docsRoute } from 'ember-cli-addon-docs/router'; 2 | import config from './config/environment'; 3 | 4 | const Router = AddonDocsRouter.extend({ 5 | location: config.locationType, 6 | rootURL: config.rootURL, 7 | }); 8 | 9 | Router.map(function() { 10 | docsRoute(this, function() { 11 | this.route('quickstart'); 12 | this.route('why-ember-table'); 13 | 14 | this.route('guides', function() { 15 | this.route('main', function() { 16 | this.route('basic-table'); 17 | this.route('table-customization'); 18 | this.route('table-meta-data'); 19 | this.route('styling-the-table'); 20 | this.route('infinite-scroll'); 21 | }); 22 | 23 | this.route('header', function() { 24 | this.route('columns'); 25 | this.route('subcolumns'); 26 | this.route('fixed-columns'); 27 | this.route('size-constraints'); 28 | this.route('sorting'); 29 | this.route('scroll-indicators'); 30 | this.route('column-keys'); 31 | }); 32 | 33 | this.route('body', function() { 34 | this.route('rows-and-trees'); 35 | this.route('row-selection'); 36 | this.route('occlusion'); 37 | }); 38 | }); 39 | 40 | this.route('testing', function() { 41 | this.route('test-page'); 42 | }); 43 | 44 | this.route('api', function() { 45 | this.route('class', { path: '/:class_id' }); 46 | }); 47 | }); 48 | 49 | this.route('scenarios', function() { 50 | this.route('simple'); 51 | this.route('performance'); 52 | this.route('blank'); 53 | }); 54 | 55 | this.route('not-found', { path: '/*path' }); 56 | }); 57 | 58 | export default Router; 59 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/index.js: -------------------------------------------------------------------------------- 1 | import { inject as service } from '@ember/service'; 2 | import Route from '@ember/routing/route'; 3 | 4 | export default class IndexRoute extends Route { 5 | @service router; 6 | 7 | redirect() { 8 | this.router.transitionTo('docs'); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.scss: -------------------------------------------------------------------------------- 1 | @import './tables'; 2 | @import './fixes'; 3 | @import '@addepar/style-toolbox/onyx/index'; 4 | 5 | // Override the url for the checkmark that indicates row selection. 6 | // This overrides a style in style-toolbox that uses the absolute url 7 | // '/@addepar/style-toolbox/assets/images/checkmark.svg'. When addon docs 8 | // are deployed, the root URL is '/ember-table/', and thus the checkmark asset 9 | // is not able to be loaded. 10 | // So instead we override the style and use a checkmark svg that has been added to this project: 11 | // See https://github.com/Addepar/ember-table/issues/593 12 | // The style-toolbox style is here: https://github.com/Addepar/addepar-style-toolbox/blob/640825d7722571c223f4b93e78a5c528018521e5/styles/onyx/elements/inputs/checkbox/_checkbox-core.scss#L68 13 | .ember-table .et-toggle-select input[type='checkbox'] + *:after { 14 | background-image: url('images/checkmark.svg'); 15 | } 16 | 17 | $colors: ( 18 | navy: #001f3f, 19 | blue: #0074d9, 20 | aqua: #7fdbff, 21 | teal: #39cccc, 22 | olive: #3d9970, 23 | green: #2ecc40, 24 | lime: #01ff70, 25 | yellow: #ffdc00, 26 | orange: #ff851b, 27 | red: #ff4136, 28 | maroon: #85144b, 29 | fuchsia: #f012be, 30 | purple: #b10dc9, 31 | black: #111111, 32 | grey: #aaaaaa, 33 | silver: #dddddd, 34 | ); 35 | 36 | @each $color-name, $color-value in $colors { 37 | .bg-#{'' + $color-name} { 38 | background-color: #{$color-value}; 39 | color: white; 40 | } 41 | .text-#{'' + $color-name} { 42 | color: #{$color-value}; 43 | } 44 | } 45 | 46 | .resize-container { 47 | resize: both; 48 | overflow: scroll; 49 | border: solid 1px #dae1e7; 50 | width: 400px; 51 | height: 200px; 52 | padding: 10px; 53 | 54 | &.w-100 { 55 | width: 100%; 56 | } 57 | } 58 | 59 | .docs-hero { 60 | > div { 61 | padding-top: 2rem; 62 | padding-bottom: 2rem; 63 | } 64 | 65 | h1 { 66 | display: flex; 67 | justify-content: center; 68 | line-height: 1.5; 69 | 70 | > div { 71 | margin: 0; 72 | 73 | > span { 74 | height: 4rem; 75 | } 76 | } 77 | } 78 | 79 | p { 80 | margin-top: 0; 81 | } 82 | } 83 | 84 | tr { 85 | &.has-children { 86 | td { 87 | background-color: #f7f7f7; 88 | } 89 | } 90 | 91 | &.is-row-selected { 92 | td { 93 | background-color: #e5f5ff; 94 | } 95 | } 96 | 97 | td.is-column-selected, 98 | th.is-column-selected { 99 | background-color: #e5f5ff; 100 | border-bottom-color: #d2e2ec; 101 | } 102 | 103 | td.is-cell-selected { 104 | background-color: #b8ddf5; 105 | } 106 | } 107 | 108 | .cell-content { 109 | margin: -4px; 110 | padding: 4px; 111 | height: 30px; 112 | } 113 | 114 | .small { 115 | font-size: small; 116 | } 117 | 118 | .custom-header { 119 | padding: 0.25em; 120 | } 121 | 122 | .spinner { 123 | margin-top: 7px; 124 | width: 50%; 125 | height: 50%; 126 | } 127 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/fixes.scss: -------------------------------------------------------------------------------- 1 | /* CSS fixes for Chrome table rendering issues */ 2 | .table-fixes { 3 | table { 4 | /* fix for vanishing borders */ 5 | border-spacing: 0; 6 | border-collapse: separate; 7 | } 8 | 9 | thead { 10 | /* fix for buggy Ember Table headers, especially with `isFixed` */ 11 | position: sticky !important; 12 | z-index: 5; 13 | 14 | th { 15 | /* paint over Ember Table header bleed */ 16 | box-shadow: 0 -1px 0 0 white; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/tables.scss: -------------------------------------------------------------------------------- 1 | .demo-options { 2 | & > * + * { 3 | padding-left: 2em; 4 | } 5 | padding-top: 0.5em; 6 | padding-bottom: 0.5em; 7 | 8 | &:not(:last-child) { 9 | padding-bottom: 1em; 10 | } 11 | 12 | label { 13 | white-space: nowrap; 14 | } 15 | } 16 | 17 | .demo-options-heading { 18 | text-transform: uppercase; 19 | } 20 | 21 | .demo-current-selection { 22 | font-weight: bold; 23 | } 24 | 25 | .demo-options-group { 26 | h4 { 27 | font-weight: normal; 28 | font-size: larger; 29 | } 30 | 31 | display: flex; 32 | 33 | & *:first-child { 34 | flex-basis: 40%; 35 | } 36 | 37 | & > * + * { 38 | padding-left: 2em; 39 | } 40 | } 41 | 42 | .demo-container { 43 | position: relative; 44 | width: 100%; 45 | height: 400px; 46 | 47 | &.small { 48 | height: 200px; 49 | } 50 | 51 | &.fixed-width { 52 | width: 800px; 53 | } 54 | } 55 | 56 | .vertical-borders { 57 | td, 58 | th { 59 | &:not(:first-child) { 60 | box-sizing: border-box; 61 | border-left: 1px solid #ddd; 62 | } 63 | } 64 | } 65 | 66 | .et-table { 67 | td, 68 | th { 69 | background: #ffffff; 70 | } 71 | } 72 | 73 | /* Custom component classes */ 74 | .custom-component-container { 75 | position: relative; 76 | width: 360px; 77 | height: 300px; 78 | } 79 | 80 | .cell-image { 81 | height: 40px; 82 | width: 40px; 83 | } 84 | 85 | .custom-row { 86 | background: #00ff00; 87 | } 88 | 89 | .custom-footer { 90 | background-color: #00ff00; 91 | } 92 | 93 | .info-table { 94 | margin: 2em auto; 95 | 96 | th { 97 | background-color: #f1f5f8; 98 | } 99 | 100 | th, 101 | td { 102 | padding: 0.5em; 103 | border: 1px solid #555; 104 | 105 | &.center { 106 | text-align: center; 107 | } 108 | 109 | &.highlight { 110 | background-color: #96c178; 111 | } 112 | } 113 | 114 | td:not(:first-child) { 115 | font-weight: bold; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Addepar/ember-table/5d70fc82dfe50c238aabb4995a0ea62dadb3a944/tests/dummy/app/templates/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{outlet}} 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | {{outlet}} 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/guides/body/occlusion.md: -------------------------------------------------------------------------------- 1 | # Occlusion 2 | 3 | Rendering lots and lots of HTML is really expensive for the browser, much more 4 | expensive than tracking those things in Javascript. Occlusion is a technique 5 | where we only render the HTML that is visible to the user at a given time. This 6 | allows us to load and present much more data than would otherwise be possible. 7 | 8 | Ember Table uses [vertical-collection](https://github.com/html-next/vertical-collection) 9 | by default to occlude rows of the table. This allows the table to render tens of 10 | thousands of rows without any performance hiccups. 11 | 12 | The occlusion also accounts for variable row heights automatically - no need to 13 | have static row heights, or to know the row heights in advance. 14 | 15 | ## Configuring Occlusion 16 | 17 | You can pass some parameters to the table body to fine tune the occlusion 18 | settings. The current options are: 19 | 20 | * `estimateRowHeight`: Vertical-collection figures out what your row heights 21 | are by measuring them after they have rendered. The first time each row is 22 | rendered, it assumes the row's height will be whatever value is provided by 23 | the `estimateRowHeight` in pixels (defaults to `30`). A more accurate estimate 24 | is always better, as it means vertical-collection will have less work to do 25 | if the "guess" was incorrect. 26 | 27 | * `staticHeight`: This field is a boolean flag that defaults to `false`. If you 28 | enable this field, vertical-collection will assume that all of the rows' 29 | heights are _exactly_ the value of `estimateRowHeight`. This will mean less 30 | work for vertical-collection and will be slightly more performant. 31 | 32 | Vertical-collection will **not** apply style changes to your rows if you 33 | pass `staticHeight=true`. You are responsible for ensuring that your rows are 34 | styled to always be the same as `estimateRowHeight`. 35 | 36 | * `key`: This key is the property used by the vertical-collection to determine 37 | whether an array mutation is an append, prepend, or complete replacement. It 38 | defaults to the object identity `"@identity"`. 39 | 40 | * `containerSelector`: A selector string used by the vertical-collection to select 41 | the element from which to calculate the viewable height. It defaults to the 42 | table id `"#{tableId}"`. 43 | 44 | {{#docs-demo as |demo|}} 45 | {{#demo.example}} 46 | {{! BEGIN-SNIPPET docs-example-occlusion.hbs }} 47 |
48 | 49 | 50 | 51 | 57 | 58 |
59 | {{! END-SNIPPET }} 60 | {{/demo.example}} 61 | 62 | {{demo.snippet name='docs-example-occlusion.hbs'}} 63 | {{demo.snippet label='component.js' name='docs-example-occlusion.js'}} 64 | {{/docs-demo}} 65 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/guides/body/rows-and-trees.md: -------------------------------------------------------------------------------- 1 | # Rows and Trees 2 | 3 | Table body components must receive an `rows` array. The items in this array must 4 | all be objects, but beyond that there are no specific requirements for the 5 | objects themselves - they can be anything. 6 | 7 | {{#docs-demo as |demo|}} 8 | {{#demo.example}} 9 |
10 | {{! BEGIN-SNIPPET docs-example-rows.hbs }} 11 | 12 | 13 | 14 | 15 | {{! END-SNIPPET }} 16 |
17 | {{/demo.example}} 18 | 19 | {{demo.snippet name='docs-example-rows.hbs'}} 20 | {{demo.snippet label='component.js' name='docs-example-rows.js'}} 21 | {{/docs-demo}} 22 | 23 | The value passed to each cell in the table is determined by the `valuePath` of 24 | the `column` object. A simplified version of this in handlebars would look like 25 | this: 26 | 27 | ```hbs 28 | {{#each this.rows as |row|}} 29 | 30 | {{#each columns as |column|}} 31 | 32 | {{yield (get row column.valuePath)}} 33 | 34 | {{/each}} 35 | 36 | {{/each}} 37 | ``` 38 | 39 | ## Trees and Children 40 | 41 | By default, Ember Table handles trees of rows. Each row can have a `children` 42 | property which is another array of rows. Children are treated the same way as 43 | parents - cells will attempt to find a value by getting the value at the value 44 | path on the child. 45 | 46 | If you want to disable the tree behavior, you can pass `enableTree=false` to 47 | the table body. 48 | 49 | {{#docs-demo as |demo|}} 50 | {{#demo.example name="trees"}} 51 | {{! BEGIN-SNIPPET docs-example-tree-rows.hbs }} 52 |
53 | 57 |
58 |
59 | 60 | 61 | 62 | 66 | 67 |
68 | {{! END-SNIPPET }} 69 | {{/demo.example}} 70 | 71 | {{demo.snippet label='component.js' name='docs-example-tree-rows.js'}} 72 | {{demo.snippet name='docs-example-tree-rows.hbs'}} 73 | {{/docs-demo}} 74 | 75 | ## Collapsing Rows 76 | 77 | Trees with children are collapsible by default. You can set the `isCollapsed` 78 | property directly on rows to control the collapse state of rows externally. If 79 | you set `isCollapsed`, the table will update it when the user collapses or 80 | uncollapses a row. Otherwise, it will keep the state internally only. 81 | 82 | If you want to disable collapsing, you can pass `enableCollapse=false` to the 83 | table body. 84 | 85 | If you want to disable collapsing at a row level, you can pass 86 | `disableCollapse=true` to the row. 87 | 88 | {{#docs-demo as |demo|}} 89 | {{#demo.example name="collapse"}} 90 | {{! BEGIN-SNIPPET docs-example-rows-with-collapse.hbs }} 91 |
92 | 96 |
97 |
98 | 99 | 100 | 101 | 105 | 106 |
107 | {{! END-SNIPPET }} 108 | {{/demo.example}} 109 | 110 | {{demo.snippet label='component.js' name='docs-example-rows-with-collapse.js'}} 111 | {{demo.snippet name='docs-example-rows-with-collapse.hbs'}} 112 | {{/docs-demo}} 113 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/guides/header/column-keys.md: -------------------------------------------------------------------------------- 1 | ## Column Keys 2 | 3 | Ember Table will update the table layout as the `columns` array changes. When 4 | objects are removed and re-added to the array, meta data about the state of 5 | the column is preserved and recovered. By default, Ember Table uses the objects 6 | themselves as keys to save and retrieve this data. 7 | 8 | If your application is written in a functional style, this can pose a problem. 9 | For example, if your columns are generated by a getter that depends on tracked 10 | properties, you may be frequently passing new columns to Ember Table that aren't 11 | really new at all. 12 | 13 | ```js 14 | get columns() { 15 | // `data` could be a tracked property that updates frequently 16 | return this.data.headers.map((header, index) => { 17 | heading: header.name, 18 | valuePath: `${index}`, 19 | // ... 20 | }); 21 | } 22 | ``` 23 | 24 | This can have unexpected consequences, as meta data about your columns 25 | is being lost each time `columns` is re-computed. 26 | 27 | To handle this scenario, you can specify a `columnKeyPath` property that 28 | identifies a "key" property on each column object. This acts like a primary key 29 | in a database, identifying when two objects represent the same underlying 30 | entity. When `columns` is updated, these keys are used to preserve the state 31 | of your table, even if the replacement column objects are not identical to the 32 | originals. 33 | 34 | ```js 35 | get columns() { 36 | return this.data.headers.map((header, index) => { 37 | heading: header.name, 38 | valuePath: `${index}`, 39 | 40 | // assumes headers have distinct names 41 | key: header.name, 42 | 43 | // ... 44 | }); 45 | } 46 | ``` 47 | 48 | ```hbs 49 | 50 | 54 | 55 | 56 | 57 | ``` 58 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/guides/header/fixed-columns.md: -------------------------------------------------------------------------------- 1 | # Fixed Columns 2 | 3 | Columns can be fixed to the left or the right side of a table by setting the 4 | `isFixed` property on the column to `'left'` or `'right'`. Only root columns 5 | may be fixed, subcolumns will ignore their own `isFixed` property and use their 6 | parent's value instead. 7 | 8 | {{#docs-demo as |demo|}} 9 | {{#demo.example name='fixed-columns'}} 10 |
11 | {{! BEGIN-SNIPPET docs-example-fixed-columns.hbs }} 12 | 13 | 14 | 15 | 16 | 17 | {{! END-SNIPPET }} 18 |
19 | {{/demo.example}} 20 | 21 | {{demo.snippet name='docs-example-fixed-columns.hbs'}} 22 | {{demo.snippet name='docs-example-fixed-columns.js' label='component.js'}} 23 | {{/docs-demo}} 24 | 25 | ## Multiple Fixed Columns and Ordering 26 | 27 | Multiple columns may be fixed to either side of the table. Fixed columns _must_ 28 | be placed contiguously at the start or end of the `columns` array. If columns 29 | are marked as fixed and are out of order, Ember Table will sort the columns 30 | array directly to fix the ordering. 31 | 32 | {{#docs-demo as |demo|}} 33 | {{#demo.example name='out-of-order-fixed-columns'}} 34 |
35 | {{! BEGIN-SNIPPET docs-example-out-of-order-fixed-columns.hbs }} 36 | 37 | 38 | 39 | 40 | 41 | {{! END-SNIPPET }} 42 |
43 | {{/demo.example}} 44 | 45 | {{demo.snippet name='docs-example-out-of-order-fixed-columns.hbs'}} 46 | {{demo.snippet name='docs-example-out-of-order-fixed-columns.js' label='component.js'}} 47 | {{/docs-demo}} 48 | 49 | ## Dynamic Fixed Columns 50 | 51 | Fixed positioning can be changed at any time, the below example demonstrates how 52 | you can make fixed columns toggleable for your users. 53 | 54 | {{#docs-demo as |demo|}} 55 | {{#demo.example name='dynamic-fixed-columns'}} 56 | {{examples/fixed-columns-dynamic 57 | rows=this.rows 58 | columns=this.dynamicFixedColumns 59 | toggleFixed=this.toggleFixed 60 | }} 61 | {{/demo.example}} 62 | 63 | {{demo.snippet name='docs-example-dynamic-fixed-columns.hbs'}} 64 | {{demo.snippet name='docs-example-dynamic-fixed-columns.js' label='component.js'}} 65 | {{/docs-demo}} 66 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/guides/header/scroll-indicators.md: -------------------------------------------------------------------------------- 1 | # Scroll Indicators 2 | 3 | Set the `scrollIndicators` argument to `all`, `horizontal`, `vertical`, or `none` to 4 | shade the edges of the table where the user can scroll to see more data. 5 | These indicators will show/hide when there is content overflowing in their 6 | respective direction. 7 | 8 | {{#docs-demo as |demo|}} 9 | {{#demo.example name='scroll-indicators'}} 10 |
11 | {{! BEGIN-SNIPPET docs-example-scroll-indicators.hbs }} 12 | 13 | 14 | 18 | 19 | 20 | {{! END-SNIPPET }} 21 |
22 | {{/demo.example}} 23 | 24 | {{demo.snippet name='docs-example-scroll-indicators.hbs'}} 25 | {{demo.snippet name='docs-example-scroll-indicators.js' label='component.js'}} 26 | {{/docs-demo}} 27 | 28 | ## Horizontal Scroll Indicators with Fixed Columns 29 | 30 | Horizontal indicators will respect fixed columns, appearing inside of 31 | them when they are present, or at the edges of the table when they are not. 32 | 33 | {{#docs-demo as |demo|}} 34 | {{#demo.example name='scroll-indicators-with-fixed'}} 35 |
36 | {{! BEGIN-SNIPPET docs-example-scroll-indicators-with-fixed.hbs }} 37 | 38 | 39 | 43 | 44 | 45 | {{! END-SNIPPET }} 46 |
47 | {{/demo.example}} 48 | 49 | {{demo.snippet name='docs-example-scroll-indicators-with-fixed.hbs'}} 50 | {{demo.snippet name='docs-example-scroll-indicators-with-fixed.js' label='component.js'}} 51 | {{/docs-demo}} 52 | 53 | ## Vertical Scroll Indicators with a Header & Footer 54 | 55 | Vertical scroll indicators respect both headers and footers, appearing just 56 | inside any sticky rows at the top or bottom of the table. 57 | 58 | {{#docs-demo as |demo|}} 59 | {{#demo.example name='scroll-indicators-with-footer'}} 60 |
61 | {{! BEGIN-SNIPPET docs-example-scroll-indicators-with-footer.hbs }} 62 | 63 | 64 | 68 | 69 | 70 | 71 | 72 | 73 | {{! END-SNIPPET }} 74 |
75 | {{/demo.example}} 76 | 77 | {{demo.snippet name='docs-example-scroll-indicators-with-footer.hbs'}} 78 | {{demo.snippet name='docs-example-scroll-indicators-with-footer.js' label='component.js'}} 79 | {{/docs-demo}} 80 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/guides/header/subcolumns.md: -------------------------------------------------------------------------------- 1 | # Subcolumns 2 | 3 | Columns can have an array of `subcolumns`, which should be objects that are 4 | exactly the same as any other column. Subcolumns can have their own subcolumns, 5 | and when rendering the rows of the table, cells will be matched up with the 6 | lowest level of subcolumns (the leaves of the column tree). This means that 7 | `valuePath` is optional for columns that have subcolumns. 8 | 9 | {{#docs-demo as |demo|}} 10 | {{#demo.example}} 11 |
12 | {{! BEGIN-SNIPPET docs-example-subcolumns.hbs }} 13 | 14 | 15 | 16 | 17 | 18 | {{! END-SNIPPET }} 19 |
20 | {{/demo.example}} 21 | 22 | {{demo.snippet name='docs-example-subcolumns.hbs'}} 23 | {{demo.snippet name='docs-example-subcolumns.js' label='component.js'}} 24 | {{/docs-demo}} 25 | 26 | ## Resizing and Reordering 27 | 28 | Subcolumns can be resized like any other column. When resizing a column with 29 | subcolumns, changes in width will be spread throughout the subcolumns. 30 | Subcolumns can only be reordered within their group - it is not currently 31 | possible to reorder move columns around arbitrarily. 32 | 33 | Columns do not need to have the same numbers of subcolumns, they can mix and 34 | match as much as you would like. This table's columns have generated completely 35 | randomly, demonstrating the flexibility of subcolumns. 36 | 37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
46 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/guides/main/basic-table.md: -------------------------------------------------------------------------------- 1 | # A Basic Table 2 | 3 | Tables can get very complicated, and it's not easy to have a table API that is 4 | powerful, flexible, and succinct. Ember Table makes this tradeoff by providing a 5 | very flexible API, but at the cost of being fairly verbose. Because this table 6 | is meant for power users who need a lot of functionality and flexibility, this 7 | tradeoff generally makes sense. 8 | 9 | The table has a set of sane defaults. If you don't need much customization, 10 | setting up a minimal instance of Ember Table will only require you to define a 11 | header and a body, with columns and rows passed to it. 12 | 13 | {{#docs-demo as |demo|}} 14 | {{#demo.example}} 15 |
16 | {{! BEGIN-SNIPPET docs-example-basic-table.hbs }} 17 | 18 | 19 | 20 | 21 | 22 | {{! END-SNIPPET }} 23 |
24 | {{/demo.example}} 25 | 26 | {{demo.snippet name='docs-example-basic-table.hbs'}} 27 | {{demo.snippet label='component.js' name='docs-example-basic-table.js'}} 28 | {{/docs-demo}} 29 | 30 | ## High Level Structure 31 | 32 | At a high level, the structure of Ember Table is meant to mimic the structure of 33 | HTML tables directly. This allows you to customize each element in the table; 34 | you can add class names, setup actions, and handle events anywhere. 35 | 36 | This example demonstrates the same table as above, but with each level yielded. 37 | 38 | {{#docs-demo as |demo|}} 39 | {{#demo.example name='expanded'}} 40 |
41 | {{! BEGIN-SNIPPET docs-example-basic-expanded-table.hbs }} 42 | 43 | 44 | 45 | 46 | {{column.name}} 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | {{value}} 55 | 56 | 57 | 58 | 59 | {{! END-SNIPPET }} 60 |
61 | {{/demo.example}} 62 | 63 | {{demo.snippet name='docs-example-basic-expanded-table.hbs'}} 64 | {{/docs-demo}} 65 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/guides/main/infinite-scroll.md: -------------------------------------------------------------------------------- 1 | # Infinite Scroll 2 | 3 | Infinite scroll is a common UI pattern where additional table rows are loaded as the user scrolls toward the bottom of the table. Ember Table does not provide infinite scrolling out of the box, but it has all the parts required to build an infinite scrolling table. 4 | 5 | {{#docs-demo as |demo|}} 6 | {{#demo.example}} 7 | {{examples/infinite-scroll}} 8 | {{/demo.example}} 9 | 10 | {{demo.snippet name='docs-example-infinite-scroll.hbs'}} 11 | {{demo.snippet label='component.js' name='docs-example-infinite-scroll.js'}} 12 | {{/docs-demo}} 13 | 14 | Ember Table does not provide a built-in spinner. You must specify your own by passing a block to `` like in the example above. See the {{#link-to route='docs.api.item' model='components/ember-table-loading-more'}}`EmberTableLoadingMore`{{/link-to}} component docs for more information. 15 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/guides/main/styling-the-table.md: -------------------------------------------------------------------------------- 1 | # Styling the Table [WIP] 2 | 3 | Ember Table by default only ships with the minimum CSS necessary for it to 4 | function. This includes the `sticky` positioning of the header, footer, and 5 | fixed columns, along with a few other things. 6 | 7 | You can style the table by directly styling the `.ember-table` class, and the 8 | HTML structure beneath it. The examples in this documentation app use Addepar's 9 | own [CSS framework](https://github.com/Addepar/addepar-style-toolbox), which is 10 | tailored to our specific needs. You can check out the [table styles](https://github.com/Addepar/addepar-style-toolbox/blob/master/styles/onyx/components/table/_core.scss) 11 | applied there for inspiration. 12 | 13 | In the future, this page will contain a class list and outline of the base HTML 14 | for Ember Table. 15 | 16 | ## Reorder indicators 17 | 18 | When reordering the columns, two elements are created to be customized and allow the user to understand what he is doing. 19 | 20 | A first `.et-reorder-main-indicator` element is created, which basically is a ghost copy of the header cell currently grabbed. By default, it has no CSS property, giving a `position: absolute;` so this element is positioned on the `` is recommended. 21 | 22 | A second `.et-reorder-drop-indicator` element is created, which is the target header cell. It has two CSS classes `.et-reorder-direction-left` and `.et-reorder-direction-right` depending on the current drop position to show whether the drop will be directed on the left or on the right. Styling these will give the user a more intuitive experience of how they is reordering their table. 23 | 24 | ## Styling Examples 25 | 26 | Here is an example of using CSS Flex properties to create a layout that has a fixed page header and footer and a content area that is split for a search input area on the left and a data table on the right: [CSS Flex](https://ember-twiddle.com/4cb616452e3316ddcec242192fc4a96c?openFiles=templates.application.hbs%2C). The same layout using Bootstrap classes: [Bootstrap Flex](https://ember-twiddle.com/d27c9f154050688518a7ca9a0b055a26?openFiles=templates.application.hbs%2C). This version is also somewhat responsive, so change your window sizes to see it respond. 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/guides/main/table-meta-data.md: -------------------------------------------------------------------------------- 1 | # Table Meta Data 2 | 3 | So far you've seen how Ember Table revolves around three central concepts: 4 | 5 | 1. The `rowValue`, which is one of the rows that are provided to the body 6 | 2. The `columnValue`, which is one of the columns that are provided to the 7 | header 8 | 3. The `cellValue`, which is produced by using the `columnValue` to lookup a 9 | value on the `rowValue` 10 | 11 | These are the fundamental building blocks of any table, so it makes sense that 12 | they would be what is given to you when using the table API. 13 | 14 | You'll also find that Ember Table provides a meta object that is associated 15 | with each of these. These meta objects are yielded after the main 16 | cell/column/row values at the cell level, and are generally accessible wherever 17 | their corresponding values are: 18 | 19 | ```hbs 20 | 21 | ``` 22 | 23 | ## What are meta objects? 24 | 25 | The meta objects are unique POJOs that are associated with a corresponding 26 | value. That is to say, for every `cell`, `column`, and `row` in the table, there 27 | are corresponding `cellMeta`, `columnMeta`, and `rowMeta` objects. 28 | 29 | `columnMeta` and `rowMeta` objects are used by the table to accomplish some 30 | internal bookkeeping such as collapse and selection state, but you are free to 31 | use these objects to store whatever meta information you would like in the 32 | table. 33 | 34 | `rowsCount` is also yielded by the cell component. This count is a reflection 35 | of how many rows the user can currently see by scrolling through the table. It 36 | is typically smaller than the total number of rows passed into, say, the 37 | `ember-tbody` component, because it excludes rows that have been hidden by 38 | collapsing a parent. 39 | 40 | ## What are they used for? 41 | 42 | Complex data tables have lots of functionality that requires some amount of 43 | state to be tracked. This state is generally unique to the table, and oftentimes 44 | related to a particular cell, column, or row. A good example of this is cell 45 | selection, like in Excel. 46 | 47 | When you click a cell in Excel, the row, column, and cell are all marked as 48 | active to show the user where they are in the table. Ember Table does _not_ have 49 | this functionality out of the box - let's see how we would implement it with 50 | meta objects: 51 | 52 | 53 | {{main/table-meta-data/cell-selection}} 54 | 55 | ## Accessing row indices in templates 56 | 57 | Meta objects can be used in templates to render conditional markdown based on 58 | the index of the current row. 59 | 60 | {{#docs-demo as |demo|}} 61 | {{#demo.example name="row-indices"}} 62 |
63 | 74 | 75 | {{! BEGIN-SNIPPET table-meta-data-row-indices.hbs }} 76 | 77 | 78 | 79 | 80 | 81 | {{#if (eq rowMeta.index 0)}} 82 | {{cell}} 83 | {{!-- `dec` helper is part of `ember-composable-helpers` --}} 84 | {{else if (lt rowMeta.index (dec rowsCount))}} 85 | {{cell}} 86 | {{else}} 87 | {{cell}} 88 | {{/if}} 89 | 90 | 91 | 92 | 93 | {{! END-SNIPPET }} 94 |
95 | {{/demo.example}} 96 | 97 | {{demo.snippet name='table-meta-data-row-indices.hbs'}} 98 | {{demo.snippet name='table-meta-data-row-indices.css'}} 99 | {{/docs-demo}} 100 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/quickstart.md: -------------------------------------------------------------------------------- 1 | # Quickstart. 2 | 3 | ## Installation 4 | 5 | ```sh 6 | ember install ember-table 7 | ``` 8 | 9 | ## Basic Usage 10 | 11 | To use `Ember Table`, you'll need to create column definitions, and you'll need 12 | to pass in an array of rows. Here is a component definition with some basic 13 | column definitions and a rows array: 14 | 15 | ```javascript 16 | import Component from '@ember/component'; 17 | 18 | class DemoComponent extends Component { 19 | columns = [ 20 | { 21 | name: `First Name`, 22 | valuePath: `firstName` 23 | }, 24 | { 25 | name: `Last Name`, 26 | valuePath: `lastName` 27 | } 28 | ]; 29 | 30 | rows = [ 31 | { 32 | firstName: 'Tony', 33 | lastName: 'Stark', 34 | }, 35 | { 36 | firstName: 'Tom', 37 | lastName: 'Dale', 38 | } 39 | ]; 40 | } 41 | ``` 42 | 43 | Note how each item in the rows array has `firstName` and `lastName` properties, 44 | which matches up with the `valuePath` properties from the column definitions. 45 | This is how the table will get the values for each column. 46 | 47 | Now let's setup the template for this component: 48 | 49 | ```hbs 50 | 51 | 52 | 53 | 54 | ``` 55 | 56 | And voila! You should have a basic table up and running! 57 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/testing/test-page.md: -------------------------------------------------------------------------------- 1 | # Testing Ember Table 2 | 3 | ## Table Page Object 4 | 5 | Ember Table includes a test helper that you can import in your app's acceptance tests and use to interact with a table on the page. The Page helper is based on [`ember-classy-page-object`](https://github.com/pzuraq/ember-classy-page-object). 6 | 7 | To import it: 8 | 9 | ```js 10 | import { TablePage } from 'ember-table/test-support'; 11 | ``` 12 | 13 | Usage: 14 | 15 | ```js 16 | // ... in an acceptance test: 17 | const table = new TablePage(); 18 | 19 | assert.strictEqual(table.body.rowCount, 5, 'the table has 5 body rows'); 20 | assert.strictEqual(table.header.rows.length, 1, 'the table has 1 row of headers'); 21 | assert.strictEqual(table.footer.rows.length, 1, 'the table has 1 row of footers'); 22 | 23 | await table.selectRow(0); // The first body row is selected 24 | assert.true(table.body.rows.objectAt(0).isSelected, 'first row is selected'); 25 | assert.false(table.body.rows.objectAt(1).isSelected, 'second row is not selected'); 26 | ``` 27 | 28 | To learn more about the properties that are present on the table page object, refer to [its source](https://github.com/Addepar/ember-table/blob/master/addon-test-support/pages/ember-table.js) or 29 | to [its usage in the ember-table tests](https://github.com/Addepar/ember-table/blob/master/tests/integration/components/basic-test.js). 30 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/docs/why-ember-table.md: -------------------------------------------------------------------------------- 1 | # Why Ember Table? 2 | 3 | ## Tables are Hard 4 | 5 | They've been with us, for better or for worse, since the early 6 | days of computing, helping us sort through mounds of data and generate TPS 7 | reports. Excel macros are practically Turing complete at this point, and just 8 | about every UX pattern imaginable has been explored and implemented in a table 9 | in some application somewhere. 10 | 11 | Building that functionality into your Single Page App is a long, tiresome 12 | process, and as the datasets scale it just gets harder. There are a lot of 13 | different off the shelf tables out there, but customizing them is difficult and 14 | time consuming, and if they don't implement the feature you need there's no easy 15 | way to add it. 16 | 17 | ## What makes Ember Table different? 18 | 19 | Most table components try to abstract the complexity of the table away from 20 | users. They provide a few top components that are meant to make declaring a 21 | table simple, avoiding the nested HTML structure of traditional tables. After 22 | all, who wants to deal with figuring out the right ordering of `tbody`, `th`, 23 | and `td` tags to get everything working? 24 | 25 | The truth, however, is that tables _are_ complicated, and most attempts to 26 | reduce that complexity through abstraction actually make the problem worse. You 27 | end up having to pass tens or hundreds of configuration options to your table 28 | component to get it to work the way you want. 29 | 30 | Ember Table has decided to do the opposite. The structure of a table component 31 | mirrors the structure of an actual HTML table, giving you the ability to 32 | customize every level of your table using standard Ember templates, components, 33 | and actions: 34 | 35 | ```hbs 36 | 37 | 38 | 39 | 40 | 41 | 44 | 45 | 46 | 47 | 48 | 49 | 52 | 53 | 54 | 55 | 56 | 57 | 60 | 61 | 62 |
42 | Header 43 |
50 | Cell 51 |
58 | Footer 59 |
63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | Header 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | Cell 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | Footer 87 | 88 | 89 | 90 | 91 | ``` 92 | 93 | Our stance is that rather than attempting to hide this complexity from you, we 94 | would rather give you full flexibility. It's very likely that you will want to 95 | customize the behavior of your table, and conversely, it's very _unlikely_ that 96 | you will have hundreds or thousands of data tables sprinkled throughout your 97 | app. Data tables tend to be used in a few central locations, so having a 98 | flexible API that allows you to easily flesh out those (relatively) few use 99 | cases is very valuable. 100 | 101 | ## Feature Complete OOTB 102 | 103 | Ember Table doesn't stop at giving you a solid API to build on, it also provides 104 | many features that you would expect a modern data table to have, including: 105 | 106 | - Fixed headers and footers 107 | - Fixed columns 108 | - Row selection 109 | - Row sorting 110 | - Tree tables (with group collapsing) 111 | - Column resizing and reordering 112 | - Nested subcolumns (e.g. to create pivot tables) 113 | - Scalability - Can render thousands of rows performantly 114 | 115 | By default, Ember Table is a feature complete data table solution that you can 116 | drop in to your app. We aim to support most standard data table features, and 117 | provide escape hatches where possible. 118 | 119 | ## Lightweight 120 | 121 | Ember Table is also a relatively lightweight table solution, currently weighing 122 | in at `22kb` (minified and gzipped) with plenty of extra weight to shed. Compare 123 | that to standalone table solutions like ag-grid (`152kb`) or HandsOnTable 124 | (`196kb`) and you can see how a table solution that integrates with the 125 | framework directly can save quite a few bytes. 126 | 127 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/not-found.hbs: -------------------------------------------------------------------------------- 1 |
2 |

Not found

3 |

This page doesn't exist. Head home?

4 |
5 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/scenarios.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
  • Simple
  • 3 |
  • scenarios.performance
  • 4 |
  • scenarios.blank
  • 5 |
6 | 7 | {{outlet}} 8 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/scenarios/blank.hbs: -------------------------------------------------------------------------------- 1 | blank 2 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/scenarios/performance.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | {{value}} 13 | 14 | {{#if (eq columnMeta.index 0)}}lorem ipsum dolor{{/if}} 15 | 16 | 17 | 18 | 19 |
20 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/scenarios/simple.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 |
8 | -------------------------------------------------------------------------------- /tests/dummy/app/utils/base-26.js: -------------------------------------------------------------------------------- 1 | import { assert } from '@ember/debug'; 2 | 3 | const ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 4 | const BASE = 26; 5 | 6 | /** 7 | * Convert an integer to bijective hexavigesimal notation (alphabetic base-26). 8 | * 9 | * @param {number} int - A positive integer above zero 10 | */ 11 | export function toBase26(int) { 12 | assert('base 26 conversion must receive an integer', Number.isInteger(int)); 13 | assert('base 26 conversion must receive a positive value', int >= 0); 14 | 15 | if (int < BASE) { 16 | return ALPHA[int]; 17 | } 18 | 19 | let result = ''; 20 | int += 1; 21 | 22 | while (int > 0) { 23 | let index = int % BASE || BASE; 24 | result = ALPHA[index - 1] + result; 25 | int = Math.floor((int - 1) / BASE); 26 | } 27 | 28 | return result; 29 | } 30 | -------------------------------------------------------------------------------- /tests/dummy/app/utils/generators.js: -------------------------------------------------------------------------------- 1 | import { A as emberA } from '@ember/array'; 2 | import { toBase26 } from './base-26'; 3 | import faker from 'faker'; 4 | 5 | const DEFAULT_USE_EMBER_ARRAY = true; 6 | let useEmberArray = DEFAULT_USE_EMBER_ARRAY; 7 | 8 | export function configureTableGeneration({ useEmberArray: _useEmberArray }) { 9 | useEmberArray = _useEmberArray; 10 | } 11 | 12 | export function resetTableGenerationConfig() { 13 | useEmberArray = DEFAULT_USE_EMBER_ARRAY; 14 | } 15 | 16 | export function getRandomInt(max, min) { 17 | return faker.random.number({ min, max }); 18 | } 19 | 20 | function identity(row, key) { 21 | return key; 22 | } 23 | 24 | export class DummyRow { 25 | constructor(id, format = identity) { 26 | this.id = id; 27 | this.format = format; 28 | 29 | // Set these so that they are not picked up by `unknownProperty` below 30 | this.disableCollapse = null; 31 | this.children = null; 32 | } 33 | 34 | unknownProperty(key) { 35 | return this.format(this, key); 36 | } 37 | } 38 | 39 | export function generateRow(id, format) { 40 | return new DummyRow(id, format); 41 | } 42 | 43 | export function generateRows(rowCount, depth, format, idPrefix = '') { 44 | let arr = []; 45 | 46 | for (let i = 0; i < rowCount; i++) { 47 | let id = idPrefix + i; 48 | let row = generateRow(id, format); 49 | 50 | if (depth > 1) { 51 | row.children = generateRows(rowCount, depth - 1, format, id); 52 | } 53 | 54 | arr.push(row); 55 | } 56 | 57 | return useEmberArray ? emberA(arr) : arr; 58 | } 59 | 60 | export function generateColumn(id, options) { 61 | let formattedId = Array.isArray(id) ? id.map(toBase26).join(' ') : toBase26(id); 62 | 63 | return { 64 | name: formattedId, 65 | valuePath: formattedId, 66 | 67 | ...options, 68 | }; 69 | } 70 | 71 | export function generateColumns( 72 | columnCount, 73 | { 74 | id = [], 75 | subcolumnCount = 0, 76 | fixedLeftCount = 0, 77 | fixedRightCount = 0, 78 | 79 | ...columnOptions 80 | } = {} 81 | ) { 82 | let columns = []; 83 | 84 | for (let i = 0; i < columnCount; i++) { 85 | let columnId = id.slice(); 86 | 87 | columnId.push(i); 88 | 89 | let columnDefinition = generateColumn(columnId, columnOptions); 90 | 91 | if (subcolumnCount) { 92 | columnDefinition.subcolumns = generateColumns(subcolumnCount, { 93 | id: columnId, 94 | ...columnOptions, 95 | }); 96 | } 97 | 98 | columns.push(columnDefinition); 99 | } 100 | 101 | for (let i = 0; i < fixedLeftCount; i++) { 102 | columns[i].isFixed = 'left'; 103 | } 104 | 105 | for (let i = 0; i < fixedRightCount; i++) { 106 | columns[columnCount - i - 1].isFixed = 'right'; 107 | } 108 | 109 | return useEmberArray ? emberA(columns) : columns; 110 | } 111 | -------------------------------------------------------------------------------- /tests/dummy/config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup'); 4 | 5 | module.exports = async function() { 6 | let { default: latestVersion } = await import('latest-version'); 7 | 8 | return { 9 | packageManager: 'yarn', 10 | scenarios: [ 11 | embroiderSafe(), 12 | embroiderOptimized(), 13 | { 14 | name: 'ember-lts-3.28', 15 | npm: { 16 | devDependencies: { 17 | '@ember/test-helpers': '2.9.4', 18 | 'ember-a11y-testing': '5.2.1', 19 | 'ember-qunit': '6.0.0', 20 | 'ember-source': '~3.28.0', 21 | }, 22 | }, 23 | }, 24 | { 25 | name: 'ember-lts-4.4', 26 | npm: { 27 | devDependencies: { 28 | '@ember/test-helpers': '2.9.4', 29 | 'ember-a11y-testing': '5.2.1', 30 | 'ember-qunit': '6.0.0', 31 | 'ember-source': '~4.4.0', 32 | }, 33 | }, 34 | }, 35 | { 36 | name: 'ember-lts-4.12', 37 | npm: { 38 | devDependencies: { 39 | 'ember-source': '~4.12.0', 40 | }, 41 | }, 42 | }, 43 | { 44 | name: 'ember-lts-5.4', 45 | npm: { 46 | devDependencies: { 47 | 'ember-source': '~5.4.0', 48 | }, 49 | }, 50 | }, 51 | { 52 | name: 'ember-lts-5.12', 53 | npm: { 54 | devDependencies: { 55 | 'ember-source': '~5.12.0', 56 | }, 57 | }, 58 | }, 59 | { 60 | name: 'ember-release', 61 | npm: { 62 | devDependencies: { 63 | 'ember-source': await latestVersion('ember-source'), 64 | }, 65 | }, 66 | }, 67 | { 68 | name: 'ember-beta', 69 | npm: { 70 | devDependencies: { 71 | 'ember-source': await latestVersion('ember-source', { 72 | version: 'beta', 73 | }), 74 | }, 75 | }, 76 | }, 77 | { 78 | name: 'ember-canary', 79 | npm: { 80 | devDependencies: { 81 | 'ember-source': await latestVersion('ember-source', { 82 | version: 'alpha', 83 | }), 84 | }, 85 | }, 86 | }, 87 | { 88 | name: 'ember-default', 89 | npm: { 90 | devDependencies: {}, 91 | }, 92 | }, 93 | { 94 | name: 'ember-production', 95 | command: 'ember test -e production', 96 | npm: { 97 | devDependencies: {}, 98 | }, 99 | }, 100 | { 101 | name: 'ember-default-docs', 102 | command: 'ember test --filter="Acceptance | docs"', 103 | npm: { 104 | devDependencies: { 105 | 'ember-data': '~3.24.0', 106 | 'ember-cli-addon-docs': '^5.2.0', 107 | 'ember-cli-addon-docs-yuidoc': '^1.0.0', 108 | 'ember-cli-deploy': '^1.0.2', 109 | 'ember-cli-deploy-build': '^1.1.1', 110 | 'ember-cli-deploy-git': '^1.3.3', 111 | 'ember-cli-deploy-git-ci': '^1.0.1', 112 | }, 113 | resolutions: { 114 | '@handlebars/parser': '~2.1.0', 115 | }, 116 | }, 117 | }, 118 | ], 119 | }; 120 | }; 121 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | const ADDON_DOCS_INSTALLED = Object.keys(require('../../../package.json').devDependencies).includes( 2 | 'ember-cli-addon-docs' 3 | ); 4 | 5 | module.exports = function(environment) { 6 | let ENV = { 7 | modulePrefix: 'dummy', 8 | environment, 9 | rootURL: '/', 10 | locationType: 'history', 11 | ADDON_DOCS_INSTALLED, 12 | EmberENV: { 13 | EXTEND_PROTOTYPES: false, 14 | FEATURES: { 15 | // Here you can enable experimental features on an ember canary build 16 | // e.g. 'with-controller': true 17 | }, 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | }, 24 | }; 25 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | ENV.APP.autoboot = false; 44 | } 45 | 46 | if (environment === 'production') { 47 | // Allow ember-cli-addon-docs to update the rootURL in compiled assets 48 | ENV.rootURL = 'ADDON_DOCS_ROOT_URL'; 49 | } 50 | 51 | return ENV; 52 | }; 53 | -------------------------------------------------------------------------------- /tests/dummy/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "jquery-integration": false, 4 | "template-only-glimmer-components": true 5 | } 6 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | const browsers = ['last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions']; 2 | 3 | module.exports = { 4 | browsers, 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Single Page Apps for GitHub Pages 6 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /tests/dummy/public/assets/images/checkmark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/dummy/public/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Addepar/ember-table/5d70fc82dfe50c238aabb4995a0ea62dadb3a944/tests/dummy/public/assets/images/favicon.ico -------------------------------------------------------------------------------- /tests/dummy/public/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Addepar/ember-table/5d70fc82dfe50c238aabb4995a0ea62dadb3a944/tests/dummy/public/assets/images/logo.png -------------------------------------------------------------------------------- /tests/dummy/public/assets/images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Addepar/ember-table/5d70fc82dfe50c238aabb4995a0ea62dadb3a944/tests/dummy/public/assets/images/spinner.gif -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Addepar/ember-table/5d70fc82dfe50c238aabb4995a0ea62dadb3a944/tests/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/helpers/base-26.js: -------------------------------------------------------------------------------- 1 | import { assert } from '@ember/debug'; 2 | 3 | const ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 4 | const BASE = 26; 5 | 6 | /** 7 | * Convert an integer to bijective hexavigesimal notation (alphabetic base-26). 8 | * 9 | * @param {number} int - A positive integer above zero 10 | */ 11 | export function toBase26(int) { 12 | assert('base 26 conversion must receive an integer', Number.isInteger(int)); 13 | assert('base 26 conversion must receive a positive value', int >= 0); 14 | 15 | if (int < BASE) { 16 | return ALPHA[int]; 17 | } 18 | 19 | let result = ''; 20 | int += 1; 21 | 22 | while (int > 0) { 23 | let index = int % BASE || BASE; 24 | result = ALPHA[index - 1] + result; 25 | int = Math.floor((int - 1) / BASE); 26 | } 27 | 28 | return result; 29 | } 30 | -------------------------------------------------------------------------------- /tests/helpers/module.js: -------------------------------------------------------------------------------- 1 | import { module } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | 4 | export function scenarioModule(scenarios, callback) { 5 | for (let scenario in scenarios) { 6 | module(scenario, function(...moduleArgs) { 7 | callback(scenarios[scenario], ...moduleArgs); 8 | }); 9 | } 10 | } 11 | 12 | export function componentModule(moduleName, callback) { 13 | module(moduleName, function(hooks) { 14 | setupRenderingTest(hooks); 15 | 16 | callback(); 17 | }); 18 | } 19 | 20 | export function parameterizedComponentModule(moduleName, parameters, callback) { 21 | Object.keys(parameters).forEach(key => { 22 | let { values, hooks } = parameters[key]; 23 | 24 | for (let value of values) { 25 | module(`${moduleName} > params {${key}: ${value}}`, function(qunitHooks) { 26 | setupRenderingTest(qunitHooks); 27 | qunitHooks.beforeEach(function() { 28 | hooks.beforeEach && hooks.beforeEach(value); 29 | }); 30 | qunitHooks.afterEach(function() { 31 | hooks.afterEach && hooks.afterEach(value); 32 | }); 33 | callback(qunitHooks); 34 | }); 35 | } 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /tests/helpers/warn-handlers.js: -------------------------------------------------------------------------------- 1 | import { registerWarnHandler } from '@ember/debug'; 2 | 3 | let _registeredHandler = null; 4 | let _didRegisterWarnHandler = false; 5 | 6 | export function setup() { 7 | if (_didRegisterWarnHandler) { 8 | // We cannot unregister a warn handler, so make sure to only register the 9 | // handler one time during all tests. 10 | return; 11 | } 12 | _didRegisterWarnHandler = true; 13 | registerWarnHandler((message, options, next) => { 14 | if (_registeredHandler) { 15 | _registeredHandler(message, options); 16 | } else { 17 | next(message, options); 18 | } 19 | }); 20 | } 21 | 22 | export function registerTestWarnHandler(callback) { 23 | _registeredHandler = callback; 24 | } 25 | 26 | export function teardown() { 27 | _registeredHandler = null; 28 | } 29 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} {{content-for "test-head"}} 11 | 12 | 13 | 14 | 15 | 16 | {{content-for "head-footer"}} {{content-for "test-head-footer"}} 17 | 18 | 19 | {{content-for "body"}} {{content-for "test-body"}} 20 | 21 |
22 |
23 |
24 |
25 |
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | {{content-for "body-footer"}} {{content-for "test-body-footer"}} 35 | 36 | 37 | -------------------------------------------------------------------------------- /tests/integration/components/footer-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | 3 | import { generateTable } from '../../helpers/generate-table'; 4 | import { componentModule } from '../../helpers/module'; 5 | 6 | import TablePage from 'ember-table/test-support/pages/ember-table'; 7 | 8 | const table = new TablePage(); 9 | 10 | module('Integration | footer', function() { 11 | componentModule('basic', function() { 12 | test('renders if footerRows are set', async function(assert) { 13 | await generateTable(this, { footerRowCount: 3 }); 14 | 15 | assert.true(table.footer.isPresent, 'Footer is present in the table'); 16 | assert.strictEqual(table.footer.rows.length, 3, 'correct number of footer rows rendered'); 17 | assert.strictEqual( 18 | table.footer.rowCount, 19 | 3, 20 | 'The total number of rows in the footer is available through the page object' 21 | ); 22 | 23 | this.set('footerRows', []); 24 | 25 | assert.true(table.footer.isPresent, 'Footer is present in the table'); 26 | assert.strictEqual(table.footer.rows.length, 0, 'Footer rows are removed'); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/integration/components/headers/cell-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { componentModule } from '../../../helpers/module'; 3 | 4 | import { generateTable } from '../../../helpers/generate-table'; 5 | import TablePage from 'ember-table/test-support/pages/ember-table'; 6 | 7 | let table = new TablePage(); 8 | 9 | module('Integration | header | th', function() { 10 | componentModule('basic', function() { 11 | test('sends onContextMenu action', async function(assert) { 12 | this.set('onHeaderCellContextMenu', event => { 13 | assert.ok(event, 'event sent'); 14 | }); 15 | 16 | await generateTable(this); 17 | await table.headers.objectAt(0).contextMenu(); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/integration/components/headers/resize-handle-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { render } from '@ember/test-helpers'; 3 | import hbs from 'htmlbars-inline-precompile'; 4 | 5 | import { ResizePage } from 'ember-table/test-support/pages/-private/ember-table-header'; 6 | 7 | import { componentModule } from '../../../helpers/module'; 8 | 9 | let resize = new ResizePage(); 10 | 11 | module('Integration | Component | ember-th/resize-handle', function() { 12 | componentModule('basic', function() { 13 | test('it renders', async function(assert) { 14 | this.set('columnMeta', { 15 | isResizable: true, 16 | }); 17 | 18 | await render(hbs``); 19 | 20 | assert.true(resize.isPresent); 21 | 22 | this.set('columnMeta.isResizable', false); 23 | assert.false(resize.isPresent); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /tests/integration/components/headers/sort-indicator-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { render } from '@ember/test-helpers'; 3 | import hbs from 'htmlbars-inline-precompile'; 4 | 5 | import { SortPage } from 'ember-table/test-support/pages/-private/ember-table-header'; 6 | 7 | import { componentModule } from '../../../helpers/module'; 8 | 9 | let sort = new SortPage(); 10 | 11 | module('Integration | Component | ember-th/sort-indicator', function() { 12 | componentModule('basic', function() { 13 | test('it renders', async function(assert) { 14 | this.set('columnMeta', { 15 | isSorted: true, 16 | }); 17 | 18 | await render(hbs``); 19 | 20 | assert.true(sort.indicator.isPresent); 21 | 22 | // Template block usage: 23 | await render(hbs` 24 | 25 | template block text 26 | 27 | `); 28 | 29 | assert.strictEqual(sort.indicator.text, 'template block text'); 30 | }); 31 | 32 | test('it is hidden when not sorted', async function(assert) { 33 | this.set('columnMeta', { 34 | isSorted: false, 35 | }); 36 | 37 | await render(hbs``); 38 | 39 | assert.false(sort.indicator.isPresent); 40 | 41 | // Template block usage: 42 | await render(hbs` 43 | 44 | template block text 45 | 46 | `); 47 | 48 | assert.false(sort.indicator.isPresent); 49 | }); 50 | 51 | test('it displays the sort order', async function(assert) { 52 | this.set('columnMeta', { 53 | isSorted: true, 54 | isSortedAsc: true, 55 | }); 56 | 57 | await render(hbs``); 58 | // asc sort 59 | assert.true(sort.indicator.isAscending); 60 | assert.false(sort.indicator.isDescending); 61 | 62 | // desc sort 63 | this.set('columnMeta.isSortedAsc', false); 64 | assert.false(sort.indicator.isAscending); 65 | assert.true(sort.indicator.isDescending); 66 | }); 67 | 68 | test('it displays the sort index when using multiple sorts', async function(assert) { 69 | this.set('columnMeta', { 70 | isSorted: true, 71 | isMultiSorted: true, 72 | sortIndex: 2, 73 | }); 74 | 75 | await render(hbs``); 76 | 77 | assert.strictEqual(sort.indicator.text, '2'); 78 | 79 | // desc sort 80 | this.set('columnMeta.isMultiSorted', false); 81 | assert.strictEqual(sort.indicator.text, ''); 82 | }); 83 | 84 | test('the sort option supports accessibility', async function(assert) { 85 | this.set('columnMeta', { 86 | isSortable: true, 87 | }); 88 | 89 | await render(hbs``); 90 | 91 | assert.true(sort.toggle.isPresent); 92 | assert.strictEqual(sort.toggle.text, 'Toggle Sort'); 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /tests/integration/components/row-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | 3 | import { 4 | generateTable, 5 | configureTableGeneration, 6 | resetTableGenerationConfig, 7 | } from '../../helpers/generate-table'; 8 | import { parameterizedComponentModule } from '../../helpers/module'; 9 | 10 | import TablePage from 'ember-table/test-support/pages/ember-table'; 11 | import { collection, hasClass } from 'ember-classy-page-object'; 12 | 13 | let table = new TablePage({ 14 | body: { 15 | rows: collection({ 16 | isCustomRow: hasClass('custom-row'), 17 | }), 18 | }, 19 | }); 20 | 21 | const USE_EMBER_ARRAY_PARAMETERS = { 22 | useEmberArray: { 23 | values: [true, false], 24 | hooks: { 25 | beforeEach(value) { 26 | configureTableGeneration({ useEmberArray: value }); 27 | }, 28 | afterEach() { 29 | resetTableGenerationConfig(); 30 | }, 31 | }, 32 | }, 33 | }; 34 | 35 | module('Integration | row', function() { 36 | parameterizedComponentModule('basic', USE_EMBER_ARRAY_PARAMETERS, function() { 37 | test('can use a custom row component', async function(assert) { 38 | await generateTable(this, { 39 | rowComponent: 'custom-row', 40 | }); 41 | 42 | assert.true(table.rows.objectAt(0).isCustomRow, 'Table has custom row'); 43 | }); 44 | 45 | test('sends onClick action', async function(assert) { 46 | this.set('onRowClick', ({ event, rowValue, rowMeta }) => { 47 | assert.ok(event, 'event sent'); 48 | assert.ok(rowValue, 'rowValue sent'); 49 | assert.ok(rowMeta, 'rowMeta sent'); 50 | }); 51 | 52 | await generateTable(this); 53 | await table.rows.objectAt(0).click(); 54 | }); 55 | 56 | test('sends onDoubleClick action', async function(assert) { 57 | this.set('onRowDoubleClick', ({ event, rowValue, rowMeta }) => { 58 | assert.ok(event, 'event sent'); 59 | assert.ok(rowValue, 'rowValue sent'); 60 | assert.ok(rowMeta, 'rowMeta sent'); 61 | }); 62 | 63 | await generateTable(this); 64 | await table.rows.objectAt(0).doubleClick(); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from '../app'; 2 | import config from '../config/environment'; 3 | import registerRAFWaiter from 'ember-raf-scheduler/test-support/register-waiter'; 4 | import { setApplication } from '@ember/test-helpers'; 5 | import { setupEmberOnerrorValidation, start } from 'ember-qunit'; 6 | import { loadTests } from 'ember-qunit/test-loader'; 7 | import QUnit from 'qunit'; 8 | import { setup } from 'qunit-dom'; 9 | import { 10 | setup as setupWarnHandlers, 11 | teardown as teardownWarnHandlers, 12 | } from './helpers/warn-handlers'; 13 | import { setupForTest as setupEmberTableForTest } from 'ember-table/test-support'; 14 | 15 | registerRAFWaiter(); 16 | setApplication(Application.create(config.APP)); 17 | 18 | setup(QUnit.assert); 19 | setupEmberOnerrorValidation(); 20 | loadTests(); 21 | 22 | QUnit.testStart(() => { 23 | setupEmberTableForTest(); 24 | setupWarnHandlers(); 25 | }); 26 | 27 | QUnit.testDone(() => { 28 | teardownWarnHandlers(); 29 | }); 30 | 31 | start(); 32 | -------------------------------------------------------------------------------- /tests/unit/-private/column-tree-test.js: -------------------------------------------------------------------------------- 1 | import { set } from '@ember/object'; 2 | import { run } from '@ember/runloop'; 3 | import ColumnTree from 'ember-table/-private/column-tree'; 4 | import MetaCache from 'ember-table/-private/meta-cache'; 5 | import { module, test } from 'qunit'; 6 | 7 | let columnMetaCache, tree; 8 | 9 | module('Unit | Private | ColumnTree', function(hooks) { 10 | hooks.beforeEach(function() { 11 | columnMetaCache = new MetaCache(); 12 | }); 13 | 14 | hooks.afterEach(function() { 15 | // Clean up so we can look for memory leaks more easily 16 | run(() => { 17 | for (let [key, value] of columnMetaCache.entries()) { 18 | value.destroy(); 19 | columnMetaCache.delete(key); 20 | } 21 | 22 | tree.destroy(); 23 | }); 24 | }); 25 | 26 | test('Setting the column width to its current value works', function(assert) { 27 | let columns = [ 28 | { name: 'A', valuePath: 'A', width: 180 }, 29 | { name: 'B', valuePath: 'B', width: 200 }, 30 | { name: 'C', valuePath: 'C', width: 100 }, 31 | { name: 'D', valuePath: 'D', width: 150 }, 32 | ]; 33 | tree = ColumnTree.create({ 34 | columns, 35 | columnMetaCache, 36 | enableTree: true, 37 | }); 38 | 39 | let root = tree.root; 40 | let subcolumns = root.subcolumnNodes; 41 | let firstSubcolumns = subcolumns[0]; 42 | let initialWidth = firstSubcolumns.width; 43 | set(firstSubcolumns, 'width', initialWidth); 44 | let width = firstSubcolumns.width; 45 | 46 | assert.strictEqual(width, 180, 'The width is unchanged'); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tests/unit/-private/meta-cache-test.js: -------------------------------------------------------------------------------- 1 | import MetaCache from 'ember-table/-private/meta-cache'; 2 | import { module, test } from 'qunit'; 3 | 4 | let metaCache; 5 | 6 | module('Unit | Private | MetaCache', function(hooks) { 7 | hooks.beforeEach(function() { 8 | metaCache = new MetaCache(); 9 | }); 10 | 11 | test('it behaves like Map with no keyPath set', function(assert) { 12 | let a = { id: 1 }; 13 | let b = { id: 1 }; 14 | 15 | metaCache.set(a, 1); 16 | assert.strictEqual(metaCache.get(a), 1, 'gets value by added object key'); 17 | assert.true(metaCache.has(a), 'contains added object key'); 18 | assert.strictEqual(metaCache.get(b), undefined, 'gets undefined value when object key unknown'); 19 | assert.false(metaCache.has(b), 'does not contain unknown object key'); 20 | 21 | metaCache.delete(a); 22 | assert.strictEqual(metaCache.get(a), undefined, 'gets undefined value when object key removed'); 23 | assert.false(metaCache.has(a), 'does not contain removed object key'); 24 | }); 25 | 26 | test('it uses keyPath for cache lookup', function(assert) { 27 | let a = { id: 1 }; 28 | let b = { id: 1 }; 29 | 30 | metaCache.keyPath = 'id'; 31 | 32 | metaCache.set(a, 1); 33 | assert.strictEqual(metaCache.get(a), 1, 'gets value by added object key'); 34 | assert.true(metaCache.has(a), 'contains added object key'); 35 | assert.strictEqual(metaCache.get(b), 1, 'gets same value with equivalent object key'); 36 | assert.true(metaCache.has(b), 'contains equivalent object key'); 37 | 38 | metaCache.delete(a); 39 | assert.strictEqual(metaCache.get(a), undefined, 'gets undefined value when object key removed'); 40 | assert.false(metaCache.has(a), 'does not contain removed object key'); 41 | assert.strictEqual( 42 | metaCache.get(b), 43 | undefined, 44 | 'gets undefined value when equivalent key removed' 45 | ); 46 | assert.false(metaCache.has(b), 'does not contain equivalent object key'); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Addepar/ember-table/5d70fc82dfe50c238aabb4995a0ea62dadb3a944/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/ember/tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "ember-table": [ 7 | "types" 8 | ], 9 | "ember-table/*": [ 10 | "types/*" 11 | ], 12 | } 13 | }, 14 | "include": [ 15 | "types/**/*" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /types/components/ember-table-loading-more/component.d.ts: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | 3 | export interface EmberTableLoadingMoreSignature { 4 | Element: HTMLDivElement; 5 | Args: { 6 | canLoadMore: boolean; 7 | center?: boolean; 8 | isLoading: boolean; 9 | }; 10 | } 11 | 12 | export default class EmberTableLoadingMoreComponent extends Component {} 13 | -------------------------------------------------------------------------------- /types/components/ember-table/component.d.ts: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { EmberTableColumn, EmberTableRow } from 'ember-table'; 3 | import EmberTbodyComponent from 'ember-table/components/ember-tbody/component'; 4 | import EmberTfootComponent from 'ember-table/components/ember-tfoot/component'; 5 | import EmberTheadComponent from 'ember-table/components/ember-thead/component'; 6 | import EmberTableLoadingMoreComponent from 'ember-table/components/ember-table-loading-more/component'; 7 | 8 | export interface EmberTableSignature< 9 | RowType extends EmberTableRow, 10 | ColumnType extends EmberTableColumn 11 | > { 12 | Element: HTMLDivElement; 13 | Blocks: { 14 | default: [ 15 | { 16 | body: typeof EmberTbodyComponent; 17 | foot: typeof EmberTfootComponent; 18 | head: typeof EmberTheadComponent; 19 | loadingMore: typeof EmberTableLoadingMoreComponent; 20 | } 21 | ]; 22 | }; 23 | } 24 | 25 | export default class EmberTableComponent< 26 | RowType extends EmberTableRow = EmberTableRow, 27 | ColumnType extends EmberTableColumn = EmberTableColumn 28 | > extends Component> {} 29 | -------------------------------------------------------------------------------- /types/components/ember-td/component.d.ts: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { EmberTableColumn, EmberTableRow, TableColumnMeta, TableRowMeta } from 'ember-table'; 3 | 4 | export interface EmberTdSignature { 5 | Element: HTMLTableCellElement; 6 | Args: { 7 | /** 8 | * Action sent when the user clicks this element. 9 | */ 10 | onClick?: ((row: RowType, event: Event) => void) | undefined; 11 | 12 | /** 13 | * Action sent when the user double clicks this element. 14 | */ 15 | onDoubleClick?: ((row: RowType, event: Event) => void) | undefined; 16 | }; 17 | Blocks: { 18 | default: [ 19 | cellValue: RowType[keyof RowType], 20 | columnValue: ColumnType, 21 | rowValue: RowType, 22 | cellMeta: unknown, 23 | columnMeta: TableColumnMeta, 24 | rowMeta: TableRowMeta, 25 | ]; 26 | }; 27 | } 28 | 29 | export default class EmberTdComponent< 30 | RowType extends EmberTableRow = EmberTableRow, 31 | ColumnType extends EmberTableColumn = EmberTableColumn 32 | > extends Component> {} 33 | -------------------------------------------------------------------------------- /types/components/ember-tfoot/component.d.ts: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { EmberTableColumn, EmberTableRow, TableRowMeta } from 'ember-table'; 3 | import EmberTrComponent from 'ember-table/components/ember-tr/component'; 4 | 5 | export interface EmberTfootSignature< 6 | RowType extends EmberTableRow, 7 | ColumnType extends EmberTableColumn 8 | > { 9 | Element: HTMLDivElement; 10 | Args: { 11 | rows: RowType[]; 12 | }; 13 | Blocks: { 14 | default: [ 15 | { 16 | row: typeof EmberTrComponent; 17 | rowMeta: TableRowMeta; 18 | rowValue: RowType; 19 | } 20 | ]; 21 | }; 22 | } 23 | 24 | export default class EmberTfootComponent< 25 | RowType extends EmberTableRow = EmberTableRow, 26 | ColumnType extends EmberTableColumn = EmberTableColumn 27 | > extends Component> {} 28 | -------------------------------------------------------------------------------- /types/components/ember-th/component.d.ts: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { TableColumnMeta } from 'ember-table'; 3 | 4 | export interface EmberThSignature { 5 | Element: HTMLTableCellElement; 6 | Args: { 7 | /** 8 | * Action sent when the user clicks right this element. 9 | */ 10 | onContextMenu?: ((event: Event) => void); 11 | }; 12 | Blocks: { 13 | default: [ 14 | columnValue: ColumnType, 15 | columnMeta: TableColumnMeta, 16 | ]; 17 | }; 18 | } 19 | 20 | export default class EmberThComponent extends Component> {} 21 | -------------------------------------------------------------------------------- /types/components/ember-th/resize-handle/component.d.ts: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { TableColumnMeta } from 'ember-table'; 3 | 4 | export interface ResizeHandleSignature { 5 | Element: HTMLTableCellElement; 6 | Args: { 7 | columnMeta: TableColumnMeta; 8 | }; 9 | } 10 | 11 | export default class ResizeHandleComponent extends Component {} 12 | -------------------------------------------------------------------------------- /types/components/ember-th/sort-indicator/component.d.ts: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { TableColumnMeta } from 'ember-table'; 3 | 4 | export interface SortIndicatorSignature { 5 | Element: HTMLTableCellElement; 6 | Args: { 7 | columnMeta: TableColumnMeta; 8 | }; 9 | } 10 | 11 | export default class SortIndicatorComponent extends Component {} 12 | -------------------------------------------------------------------------------- /types/components/ember-tr/component.d.ts: -------------------------------------------------------------------------------- 1 | import Component from '@ember/component'; 2 | import { EmberTableColumn, EmberTableRow, TableColumnMeta, TableRowMeta } from 'ember-table'; 3 | import EmberTdComponent from 'ember-table/components/ember-td/component'; 4 | 5 | export interface EmberTrSignature< 6 | RowType extends EmberTableRow, 7 | ColumnType extends EmberTableColumn, 8 | CellComponentType 9 | > { 10 | Element: HTMLTableRowElement; 11 | Args: { 12 | /** 13 | * An action that is called when a row is clicked. Will be called with the row and the event. 14 | */ 15 | onClick?: (event: { event: Event, rowValue: RowType, rowMeta: TableRowMeta }) => void; 16 | 17 | /** 18 | * An action that is called when a row is double clicked. Will be called with the row and the event. 19 | */ 20 | onDoubleClick?: (event: { event: Event, rowValue: RowType, rowMeta: TableRowMeta }) => void; 21 | }; 22 | Blocks: { 23 | default: [ 24 | { 25 | cell: CellComponentType; 26 | cellMeta: unknown; 27 | cellValue: RowType[keyof RowType]; 28 | columnMeta: TableColumnMeta; 29 | columnValue: ColumnType; 30 | rowMeta: TableRowMeta; 31 | rowValue: RowType; 32 | } 33 | ]; 34 | }; 35 | } 36 | 37 | export default class EmberTrComponent< 38 | RowType extends EmberTableRow = EmberTableRow, 39 | ColumnType extends EmberTableColumn = EmberTableColumn, 40 | CellComponentType = typeof EmberTdComponent 41 | > extends Component> {} 42 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface EmberTableColumn { 2 | name?: string; 3 | valuePath?: string; 4 | 5 | isFixed?: 'left' | 'right'; 6 | isResizable?: boolean; 7 | isSortable?: boolean; 8 | maxWidth?: number; 9 | minWidth?: number; 10 | subcolumns?: EmberTableColumn[]; 11 | textAlign?: 'left' | 'center' | 'right'; 12 | width?: number; 13 | } 14 | 15 | export interface EmberTableRow {} 16 | 17 | export interface EmberTableSort { 18 | isAscending: boolean; 19 | valuePath: string; 20 | } 21 | 22 | export interface TableRowMeta { 23 | isCollapsed: boolean; 24 | isSelected: boolean; 25 | isGroupSelected: boolean; 26 | canCollapse: boolean; 27 | depth: number; 28 | first: unknown | null; 29 | last: unknown | null; 30 | next: unknown | null; 31 | prev: unknown | null; 32 | } 33 | 34 | export interface TableColumnMeta { 35 | isLeaf: boolean; 36 | isFixed: 'left' | 'right' | undefined; 37 | isSortable: boolean; 38 | isResizable: boolean; 39 | isReorderable: boolean; 40 | isSlack: boolean; 41 | width: number; 42 | offsetLeft: number; 43 | offsetRight: number; 44 | rowSpan: number; 45 | columnSpan: number; 46 | index: number | undefined; 47 | isLastRendered: boolean; 48 | sortIndex: number; 49 | isSorted: boolean; 50 | isMultiSorted: boolean; 51 | isSortedAsc: boolean; 52 | } 53 | -------------------------------------------------------------------------------- /types/template-registry.d.ts: -------------------------------------------------------------------------------- 1 | import EmberTableComponent from 'ember-table/components/ember-table/component'; 2 | import ResizeHandleComponent from 'ember-table/components/ember-th/resize-handle/component'; 3 | import SortIndicatorComponent from 'ember-table/components/ember-th/sort-indicator/component'; 4 | 5 | export default interface Registry { 6 | EmberTable: typeof EmberTableComponent; 7 | 'ember-table': typeof EmberTableComponent; 8 | 'EmberTh::ResizeHandle': typeof ResizeHandleComponent; 9 | 'ember-th/resize-handle': typeof ResizeHandleComponent; 10 | 'EmberTh::SortIndicator': typeof SortIndicatorComponent; 11 | 'ember-th/sort-indicator': typeof SortIndicatorComponent; 12 | } 13 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Addepar/ember-table/5d70fc82dfe50c238aabb4995a0ea62dadb3a944/vendor/.gitkeep --------------------------------------------------------------------------------