Repository: emberjs/ember-collection Branch: master Commit: 069a3742ca92 Files: 95 Total size: 182.2 KB Directory structure: gitextract_ds3n4771/ ├── .editorconfig ├── .ember-cli ├── .eslintignore ├── .eslintrc.js ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── .template-lintrc.js ├── .watchmanconfig ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── RELEASE.md ├── addon/ │ ├── components/ │ │ ├── ember-collection/ │ │ │ └── template.hbs │ │ ├── ember-collection.js │ │ └── ember-native-scrollable.js │ ├── layouts/ │ │ ├── grid.js │ │ ├── mixed-grid.js │ │ └── percentage-columns.js │ └── utils/ │ ├── identity.js │ ├── needs-revalidate.js │ ├── style-generators.js │ ├── style-properties.js │ └── translate.js ├── app/ │ ├── components/ │ │ ├── ember-collection.js │ │ └── ember-native-scrollable.js │ └── helpers/ │ ├── fixed-grid-layout.js │ ├── mixed-grid-layout.js │ └── percentage-columns-layout.js ├── config/ │ ├── ember-try.js │ └── environment.js ├── ember-cli-build.js ├── index.js ├── package.json ├── testem.js └── tests/ ├── acceptance/ │ └── list-view-test.js ├── dummy/ │ ├── app/ │ │ ├── app.js │ │ ├── components/ │ │ │ └── .gitkeep │ │ ├── controllers/ │ │ │ ├── .gitkeep │ │ │ ├── mixed.js │ │ │ ├── percentages.js │ │ │ ├── scroll-position.js │ │ │ └── simple.js │ │ ├── helpers/ │ │ │ ├── .gitkeep │ │ │ └── size-to-style.js │ │ ├── index.html │ │ ├── models/ │ │ │ └── .gitkeep │ │ ├── resolver.js │ │ ├── router.js │ │ ├── routes/ │ │ │ ├── .gitkeep │ │ │ ├── mixed.js │ │ │ ├── percentages.js │ │ │ ├── scroll-position.js │ │ │ └── simple.js │ │ ├── styles/ │ │ │ └── app.css │ │ ├── templates/ │ │ │ ├── application.hbs │ │ │ ├── components/ │ │ │ │ └── .gitkeep │ │ │ ├── index.hbs │ │ │ ├── mixed.hbs │ │ │ ├── percentages.hbs │ │ │ ├── scroll-position.hbs │ │ │ └── simple.hbs │ │ └── utils/ │ │ ├── fixtures.js │ │ ├── images.js │ │ └── make-model.js │ ├── config/ │ │ ├── ember-cli-update.json │ │ ├── environment.js │ │ ├── optional-features.json │ │ └── targets.js │ └── public/ │ ├── crossdomain.xml │ └── robots.txt ├── helpers/ │ ├── destroy-app.js │ ├── helpers.js │ ├── module-for-acceptance.js │ ├── module-for-view.js │ └── start-app.js ├── index.html ├── templates/ │ ├── fixed-grid.js │ ├── indexed.js │ └── percentage.js ├── test-helper.js └── unit/ ├── .gitkeep ├── content-test.js ├── fixed-grid-test.js ├── layout-test.js ├── multi-height-list-view-test.js ├── percentage-layout-test.js ├── raf-test.js ├── recycling-tests.js ├── scroll-top-test.js ├── starting-index-test.js └── total-height-test.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # EditorConfig helps developers define and maintain consistent # coding styles between different editors and IDEs # editorconfig.org root = true [*] end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true indent_style = space indent_size = 2 [*.hbs] insert_final_newline = false [*.{diff,md}] trim_trailing_whitespace = false ================================================ FILE: .ember-cli ================================================ { /** Ember CLI sends analytics information by default. The data is completely anonymous, but there are times when you might want to disable this behavior. Setting `disableAnalytics` to true will prevent any data from being sent. */ "disableAnalytics": false } ================================================ FILE: .eslintignore ================================================ # unconventional js /blueprints/*/files/ /vendor/ # compiled output /dist/ /tmp/ # dependencies /bower_components/ /node_modules/ # misc /coverage/ !.* # ember-try /.node_modules.ember-try/ /bower.json.ember-try /package.json.ember-try ================================================ FILE: .eslintrc.js ================================================ module.exports = { root: true, parser: 'babel-eslint', parserOptions: { ecmaVersion: 2018, sourceType: 'module', ecmaFeatures: { legacyDecorators: true, }, }, plugins: [ 'ember' ], extends: [ 'eslint:recommended', 'plugin:ember/recommended' ], env: { browser: true }, rules: { }, overrides: [ // node files { files: [ '.eslintrc.js', '.template-lintrc.js', 'ember-cli-build.js', 'index.js', 'testem.js', 'blueprints/*/index.js', 'config/**/*.js', 'tests/dummy/config/**/*.js' ], excludedFiles: [ 'addon/**', 'addon-test-support/**', 'app/**', 'tests/dummy/app/**' ], parserOptions: { sourceType: 'script' }, env: { browser: false, node: true }, plugins: ['node'], rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, { // add your custom rules and overrides for node files here }) } ] }; ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [master] pull_request: branches: [master] concurrency: group: ci-${{ github.head_ref || github.ref }} cancel-in-progress: true env: NODE_VERSION: 16 jobs: lint: name: Lint Addon runs-on: ubuntu-latest steps: - name: Check out a copy of the repo uses: actions/checkout@v3 - name: Use Node.js ${{ env.NODE_VERSION }} uses: actions/setup-node@v3 with: node-version: ${{ env.NODE_VERSION }} cache: yarn - name: Install dependencies run: yarn install --frozen-lockfile - name: Lint JS run: yarn lint:js continue-on-error: true - name: Lint HBS run: yarn lint:hbs continue-on-error: true test-addon: name: Test Addon runs-on: ubuntu-latest strategy: fail-fast: true matrix: try-scenario: - 'ember-lts-3.28' - 'ember-lts-4.8' - 'ember-lts-4.12' - 'ember-release' steps: - name: Checkout code uses: actions/checkout@v3 - name: Use Node.js ${{ env.NODE_VERSION }} uses: actions/setup-node@v3 with: node-version: ${{ env.NODE_VERSION }} cache: yarn - name: Install dependencies run: yarn install --no-lockfile - name: Test run: ./node_modules/.bin/ember try:one ${{ matrix.try-scenario }} ================================================ FILE: .gitignore ================================================ # See https://help.github.com/ignore-files/ for more about ignoring files. # compiled output /dist/ /tmp/ # dependencies /bower_components/ /node_modules/ # misc /.env* /.pnp* /.sass-cache /connect.lock /coverage/ /libpeerconnection.log npm-debug.log* yarn-error.log testem.log .DS_Store test-results.xml # ember-try /.node_modules.ember-try/ /bower.json.ember-try /package.json.ember-try ================================================ FILE: .npmignore ================================================ # compiled output /dist/ /tmp/ # dependencies /bower_components/ # misc /.bowerrc /.editorconfig /.ember-cli /.env* /.eslintignore /.eslintrc.js /.gitignore /.template-lintrc.js /.watchmanconfig /bower.json /config/ember-try.js /CONTRIBUTING.md /ember-cli-build.js /testem.js /tests/ /yarn.lock .gitkeep /.github/ # ember-try /.node_modules.ember-try/ /bower.json.ember-try /package.json.ember-try ================================================ FILE: .nvmrc ================================================ 14 ================================================ FILE: .template-lintrc.js ================================================ 'use strict'; module.exports = { extends: 'recommended', rules: { 'no-inline-styles': false, 'no-quoteless-attributes': false, 'no-triple-curlies': false, 'require-button-type': false, 'style-concatenation': false } }; ================================================ FILE: .watchmanconfig ================================================ { "ignore_dirs": ["tmp", "dist"] } ================================================ FILE: CHANGELOG.md ================================================ ## v3.0.0 (2023-11-01) [BREAKING] Require Node 16+ #### :boom: Breaking Change * [#222](https://github.com/adopted-ember-addons/ember-collection/pull/222) Update to latest layout-bin-packer ([@lukemelia](https://github.com/lukemelia)) #### Committers: 1 - Luke Melia ([@lukemelia](https://github.com/lukemelia)) ## v2.0.0 (2023-05-02) #### :boom: Breaking Change * [#220](https://github.com/adopted-ember-addons/ember-collection/pull/220) Upgrade to Ember v4.12 ([@mukilane](https://github.com/mukilane)) #### :house: Internal * [#220](https://github.com/adopted-ember-addons/ember-collection/pull/220) Upgrade to Ember v4.12 ([@mukilane](https://github.com/mukilane)) #### Committers: 1 - Mukil Elango ([@mukilane](https://github.com/mukilane)) ## v1.0.0 (2020-06-24) ## v1.0.0-rc.0 (2020-05-25) #### :boom: Breaking Change * [#184](https://github.com/adopted-ember-addons/ember-collection/pull/184) Update to 3.12 ([@vasind](https://github.com/vasind)) #### :rocket: Enhancement * [#182](https://github.com/adopted-ember-addons/ember-collection/pull/182) [CHORE] Update to ember 3.4 and fix tests ([@Gaurav0](https://github.com/Gaurav0)) #### :bug: Bug Fix * [#180](https://github.com/adopted-ember-addons/ember-collection/pull/180) Check if destroyed before validating, update travis/lint ([@rwwagner90](https://github.com/rwwagner90)) #### :house: Internal * [#189](https://github.com/adopted-ember-addons/ember-collection/pull/189) [CHORE] Update Travis CI badge link ([@vasind](https://github.com/vasind)) * [#188](https://github.com/adopted-ember-addons/ember-collection/pull/188) chore: Remove ember-cli-deploy and its plugins ([@Alonski](https://github.com/Alonski)) * [#187](https://github.com/adopted-ember-addons/ember-collection/pull/187) [CHORE] release-it setup ([@vasind](https://github.com/vasind)) * [#185](https://github.com/adopted-ember-addons/ember-collection/pull/185) Update addon URL in index.hbs ([@Alonski](https://github.com/Alonski)) #### Committers: 4 - Alon Bukai ([@Alonski](https://github.com/Alonski)) - Gaurav Munjal ([@Gaurav0](https://github.com/Gaurav0)) - Robert Wagner ([@rwwagner90](https://github.com/rwwagner90)) - Vasanth ([@vasind](https://github.com/vasind)) # Change Log ## [1.0.0-alpha.9](https://github.com/emberjs/ember-collection/tree/1.0.0-alpha.9) (2018-08-02) [Full Changelog](https://github.com/emberjs/ember-collection/compare/v1.0.0-alpha.8...1.0.0-alpha.9) **Closed issues:** - Publish alpha 8 [\#171](https://github.com/emberjs/ember-collection/issues/171) - Publish v1.0.0-alpha.8 to NPM Repository [\#168](https://github.com/emberjs/ember-collection/issues/168) - didInitAttrs and Ember.k DEPRECATION warnings for ember 2.13.1 [\#150](https://github.com/emberjs/ember-collection/issues/150) **Merged pull requests:** - Fix test that fails in recent Firefox & Safari versions under some circumstances [\#174](https://github.com/emberjs/ember-collection/pull/174) ([lukemelia](https://github.com/lukemelia)) - Remove arrayObserver when the component is destroyed [\#173](https://github.com/emberjs/ember-collection/pull/173) ([pieter-v](https://github.com/pieter-v)) - Upgrade `layout-bin-packer` [\#172](https://github.com/emberjs/ember-collection/pull/172) ([alexlafroscia](https://github.com/alexlafroscia)) ## [v1.0.0-alpha.8](https://github.com/emberjs/ember-collection/tree/v1.0.0-alpha.8) (2018-01-16) [Full Changelog](https://github.com/emberjs/ember-collection/compare/v1.0.0-alpha.7...v1.0.0-alpha.8) **Closed issues:** - Single js file version [\#165](https://github.com/emberjs/ember-collection/issues/165) - URL leads to 404 Error [\#147](https://github.com/emberjs/ember-collection/issues/147) - scroll to item [\#144](https://github.com/emberjs/ember-collection/issues/144) - Fill gaps in Mixed Grid Layout [\#134](https://github.com/emberjs/ember-collection/issues/134) - Glimmer 2 Compatibility [\#126](https://github.com/emberjs/ember-collection/issues/126) **Merged pull requests:** - Cleanup... [\#166](https://github.com/emberjs/ember-collection/pull/166) ([rwjblue](https://github.com/rwjblue)) - Fix tests broken in browsers that have visible scrollbars [\#164](https://github.com/emberjs/ember-collection/pull/164) ([raytiley](https://github.com/raytiley)) - Clean up Bower dependencies [\#162](https://github.com/emberjs/ember-collection/pull/162) ([Turbo87](https://github.com/Turbo87)) - \[BREAKING\] Bump minimum Node version to 4.5 [\#161](https://github.com/emberjs/ember-collection/pull/161) ([Turbo87](https://github.com/Turbo87)) - Update "ember-cli" to v2.16.2 [\#160](https://github.com/emberjs/ember-collection/pull/160) ([Turbo87](https://github.com/Turbo87)) - Use yarn instead of npm [\#159](https://github.com/emberjs/ember-collection/pull/159) ([Turbo87](https://github.com/Turbo87)) - README: Use TravisCI badge instead of CircleCI [\#158](https://github.com/emberjs/ember-collection/pull/158) ([Turbo87](https://github.com/Turbo87)) - Use TravisCI instead of CircleCI [\#157](https://github.com/emberjs/ember-collection/pull/157) ([Turbo87](https://github.com/Turbo87)) - testem: Run Chrome and Firefox in headless mode [\#156](https://github.com/emberjs/ember-collection/pull/156) ([Turbo87](https://github.com/Turbo87)) - testem: Remove "PhantomJS" target [\#155](https://github.com/emberjs/ember-collection/pull/155) ([Turbo87](https://github.com/Turbo87)) - Bump node to version 6 [\#153](https://github.com/emberjs/ember-collection/pull/153) ([raytiley](https://github.com/raytiley)) - Automated demo deploy via ember-cli-deploy-ghpages [\#151](https://github.com/emberjs/ember-collection/pull/151) ([lolmaus](https://github.com/lolmaus)) - Fix spelling and naming mistake [\#149](https://github.com/emberjs/ember-collection/pull/149) ([Alonski](https://github.com/Alonski)) - Removed ember-try from dependencies [\#148](https://github.com/emberjs/ember-collection/pull/148) ([Alonski](https://github.com/Alonski)) - style prefix fix [\#146](https://github.com/emberjs/ember-collection/pull/146) ([mival](https://github.com/mival)) - Fix readme scroll-right reference [\#139](https://github.com/emberjs/ember-collection/pull/139) ([jubar](https://github.com/jubar)) - Remove deprecated Ember.K [\#136](https://github.com/emberjs/ember-collection/pull/136) ([cibernox](https://github.com/cibernox)) - Prevent `didInitAttrs` deprecation. [\#133](https://github.com/emberjs/ember-collection/pull/133) ([rwjblue](https://github.com/rwjblue)) - Clear cells and cellMap when items changes [\#110](https://github.com/emberjs/ember-collection/pull/110) ([paddyobrien](https://github.com/paddyobrien)) ## [v1.0.0-alpha.7](https://github.com/emberjs/ember-collection/tree/v1.0.0-alpha.7) (2016-11-17) [Full Changelog](https://github.com/emberjs/ember-collection/compare/v1.0.0-alpha.6...v1.0.0-alpha.7) **Closed issues:** - Ncaught Error: Assertion Failed: A helper named ‘percentage-columns-layout’ could not be found [\#118](https://github.com/emberjs/ember-collection/issues/118) **Merged pull requests:** - Pr/130 [\#131](https://github.com/emberjs/ember-collection/pull/131) ([stefanpenner](https://github.com/stefanpenner)) - Glimmer 2 Compatibility [\#130](https://github.com/emberjs/ember-collection/pull/130) ([paddyobrien](https://github.com/paddyobrien)) ## [v1.0.0-alpha.6](https://github.com/emberjs/ember-collection/tree/v1.0.0-alpha.6) (2016-04-25) [Full Changelog](https://github.com/emberjs/ember-collection/compare/v1.0.0-alpha.5...v1.0.0-alpha.6) **Closed issues:** - how to test sort ordered collection when dom elements are not same order as visual order [\#107](https://github.com/emberjs/ember-collection/issues/107) - A helper named 'percentage-columns-layout' could not be found [\#106](https://github.com/emberjs/ember-collection/issues/106) - Items not appended using momentum/inertia scroll on iOS [\#105](https://github.com/emberjs/ember-collection/issues/105) - Could not find addon with name: ember-collection [\#93](https://github.com/emberjs/ember-collection/issues/93) - Collection cells overlap pop-overs [\#89](https://github.com/emberjs/ember-collection/issues/89) - fixed-grid.js:74 Uncaught TypeError: height depends on the first argument of visibleWidth\(number\) [\#71](https://github.com/emberjs/ember-collection/issues/71) - Weird rendering glitch only in safari [\#68](https://github.com/emberjs/ember-collection/issues/68) - test tooling \(phantomjs support?\) [\#43](https://github.com/emberjs/ember-collection/issues/43) **Merged pull requests:** - Update ember-try to 0.2.0. [\#102](https://github.com/emberjs/ember-collection/pull/102) ([rwjblue](https://github.com/rwjblue)) - Update ember-cli from 2.3.0-beta.2 to 2.3.0 [\#101](https://github.com/emberjs/ember-collection/pull/101) ([fpauser](https://github.com/fpauser)) - Update percentage-columns-layout example. [\#95](https://github.com/emberjs/ember-collection/pull/95) ([dustinspecker](https://github.com/dustinspecker)) - Remove template compiler from built assets. [\#91](https://github.com/emberjs/ember-collection/pull/91) ([rwjblue](https://github.com/rwjblue)) - Use Xunit reporter in CI. [\#90](https://github.com/emberjs/ember-collection/pull/90) ([rwjblue](https://github.com/rwjblue)) - Update releases tested in CI. [\#88](https://github.com/emberjs/ember-collection/pull/88) ([rwjblue](https://github.com/rwjblue)) - Fix percentage-columns-layout [\#87](https://github.com/emberjs/ember-collection/pull/87) ([raytiley](https://github.com/raytiley)) - Update to ember-cli@2.3.0-beta.1. [\#86](https://github.com/emberjs/ember-collection/pull/86) ([rwjblue](https://github.com/rwjblue)) - Update README [\#85](https://github.com/emberjs/ember-collection/pull/85) ([raytiley](https://github.com/raytiley)) - Fallback to setTimeout if requestAnimationFrame is not present [\#84](https://github.com/emberjs/ember-collection/pull/84) ([raytiley](https://github.com/raytiley)) - Fix comparison link in CHANGELOG.md [\#82](https://github.com/emberjs/ember-collection/pull/82) ([tricknotes](https://github.com/tricknotes)) - Delegate to layout for calculating style [\#81](https://github.com/emberjs/ember-collection/pull/81) ([raytiley](https://github.com/raytiley)) ## [v1.0.0-alpha.5](https://github.com/emberjs/ember-collection/tree/v1.0.0-alpha.5) (2016-01-21) [Full Changelog](https://github.com/emberjs/ember-collection/compare/v1.0.0-alpha.4...v1.0.0-alpha.5) **Closed issues:** - Deploy dummy app to provide an interactive demo. [\#73](https://github.com/emberjs/ember-collection/issues/73) - Is this dead\> [\#70](https://github.com/emberjs/ember-collection/issues/70) - listening to scroll change [\#69](https://github.com/emberjs/ember-collection/issues/69) - Nested data structure support? [\#67](https://github.com/emberjs/ember-collection/issues/67) - Tests Failing - starting with ember-beta [\#61](https://github.com/emberjs/ember-collection/issues/61) - Remember scroll position [\#60](https://github.com/emberjs/ember-collection/issues/60) - use main view as "scrolling container" instead of adding another child view [\#56](https://github.com/emberjs/ember-collection/issues/56) - ember-collection doesn't support Ember arrays [\#55](https://github.com/emberjs/ember-collection/issues/55) - Vendoring [\#54](https://github.com/emberjs/ember-collection/issues/54) - Can't install this component [\#53](https://github.com/emberjs/ember-collection/issues/53) - Inline style violates Content Security Policy [\#50](https://github.com/emberjs/ember-collection/issues/50) - Does not work with DS.RecordArray\(\) [\#44](https://github.com/emberjs/ember-collection/issues/44) - ember dev community slack channel [\#40](https://github.com/emberjs/ember-collection/issues/40) **Merged pull requests:** - Prepare for alpha.5 release [\#80](https://github.com/emberjs/ember-collection/pull/80) ([lukemelia](https://github.com/lukemelia)) - Add a note to the scroll-position example in the dummy app about remembering the scroll position across re-renders [\#79](https://github.com/emberjs/ember-collection/pull/79) ([lukemelia](https://github.com/lukemelia)) - Document behavior of scroll-change action in README [\#78](https://github.com/emberjs/ember-collection/pull/78) ([lukemelia](https://github.com/lukemelia)) - Specify the demoURL in the package.json for emberaddons.com and emberobserver.com [\#77](https://github.com/emberjs/ember-collection/pull/77) ([lukemelia](https://github.com/lukemelia)) - Bootstrap styling, removed unused dummy app items [\#76](https://github.com/emberjs/ember-collection/pull/76) ([raytiley](https://github.com/raytiley)) - Adding some context to the online demo [\#75](https://github.com/emberjs/ember-collection/pull/75) ([ef4](https://github.com/ef4)) - automated deploys to github pages [\#74](https://github.com/emberjs/ember-collection/pull/74) ([ef4](https://github.com/ef4)) - Update to use Ember.Array API... [\#66](https://github.com/emberjs/ember-collection/pull/66) ([lukemelia](https://github.com/lukemelia)) - Set Dummy app title more accurately [\#65](https://github.com/emberjs/ember-collection/pull/65) ([lukemelia](https://github.com/lukemelia)) - ember-template-compiler.js appears to be unnecessary for tests, and hardcoding it to released version is problematic for ember-try [\#64](https://github.com/emberjs/ember-collection/pull/64) ([lukemelia](https://github.com/lukemelia)) - It's not necessary to set the box-sizing css property on the ember-native-scrollable [\#63](https://github.com/emberjs/ember-collection/pull/63) ([lukemelia](https://github.com/lukemelia)) - Explain relative positioning, estimated width/height [\#59](https://github.com/emberjs/ember-collection/pull/59) ([samselikoff](https://github.com/samselikoff)) - Update README.md [\#57](https://github.com/emberjs/ember-collection/pull/57) ([DanielOchoa](https://github.com/DanielOchoa)) - Fix typo in build instructions [\#52](https://github.com/emberjs/ember-collection/pull/52) ([aldhsu](https://github.com/aldhsu)) - Use separate variables instead of two-element hashes when it makes sense [\#51](https://github.com/emberjs/ember-collection/pull/51) ([srgpqt](https://github.com/srgpqt)) ## [v1.0.0-alpha.4](https://github.com/emberjs/ember-collection/tree/v1.0.0-alpha.4) (2015-09-15) [Full Changelog](https://github.com/emberjs/ember-collection/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) ## [v1.0.0-alpha.3](https://github.com/emberjs/ember-collection/tree/v1.0.0-alpha.3) (2015-09-15) [Full Changelog](https://github.com/emberjs/ember-collection/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) **Merged pull requests:** - If scrollChange action is provided, emit scroll changes to it and bind the scroll position [\#48](https://github.com/emberjs/ember-collection/pull/48) ([lukemelia](https://github.com/lukemelia)) ## [v1.0.0-alpha.2](https://github.com/emberjs/ember-collection/tree/v1.0.0-alpha.2) (2015-09-14) [Full Changelog](https://github.com/emberjs/ember-collection/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) ## [v1.0.0-alpha.1](https://github.com/emberjs/ember-collection/tree/v1.0.0-alpha.1) (2015-09-14) **Fixed bugs:** - Some tests fail in Safari [\#19](https://github.com/emberjs/ember-collection/issues/19) - Memory Leak [\#14](https://github.com/emberjs/ember-collection/issues/14) - requestAnimationFrame cycle runs forever [\#12](https://github.com/emberjs/ember-collection/issues/12) **Closed issues:** - build [\#45](https://github.com/emberjs/ember-collection/issues/45) - Make a virtual scrolling implementation [\#36](https://github.com/emberjs/ember-collection/issues/36) - build failure [\#33](https://github.com/emberjs/ember-collection/issues/33) **Merged pull requests:** - Cleanup translate style [\#42](https://github.com/emberjs/ember-collection/pull/42) ([krisselden](https://github.com/krisselden)) - Use Ember.set to set \_clientSize, so it is observable. [\#41](https://github.com/emberjs/ember-collection/pull/41) ([lukemelia](https://github.com/lukemelia)) - Abstract scrolling [\#35](https://github.com/emberjs/ember-collection/pull/35) ([krisselden](https://github.com/krisselden)) - Abstract scrolling rebased [\#34](https://github.com/emberjs/ember-collection/pull/34) ([lukemelia](https://github.com/lukemelia)) - disabling prototype extentions [\#31](https://github.com/emberjs/ember-collection/pull/31) ([shaunc](https://github.com/shaunc)) - Assorted clean-up [\#29](https://github.com/emberjs/ember-collection/pull/29) ([jonnii](https://github.com/jonnii)) - Remove list item class [\#28](https://github.com/emberjs/ember-collection/pull/28) ([jonnii](https://github.com/jonnii)) - Buffer fix [\#25](https://github.com/emberjs/ember-collection/pull/25) ([shaunc](https://github.com/shaunc)) - fixes scroll top bugs \(including in safari\) [\#24](https://github.com/emberjs/ember-collection/pull/24) ([shaunc](https://github.com/shaunc)) - Remove class from collection container [\#23](https://github.com/emberjs/ember-collection/pull/23) ([jonnii](https://github.com/jonnii)) - Move container class to tests [\#22](https://github.com/emberjs/ember-collection/pull/22) ([jonnii](https://github.com/jonnii)) - Remove some uneeded tests [\#20](https://github.com/emberjs/ember-collection/pull/20) ([mmun](https://github.com/mmun)) - activates more of the tests; adds "display in fixed grid" test to dem… [\#18](https://github.com/emberjs/ember-collection/pull/18) ([mmun](https://github.com/mmun)) - Cancel animation frame when destroying component [\#16](https://github.com/emberjs/ember-collection/pull/16) ([raytiley](https://github.com/raytiley)) - Pass along height to layout-bin-packer in mixed grid [\#15](https://github.com/emberjs/ember-collection/pull/15) ([raytiley](https://github.com/raytiley)) - Fix dummy app [\#13](https://github.com/emberjs/ember-collection/pull/13) ([raytiley](https://github.com/raytiley)) - First pass at updating readme [\#11](https://github.com/emberjs/ember-collection/pull/11) ([raytiley](https://github.com/raytiley)) - Cleanup [\#9](https://github.com/emberjs/ember-collection/pull/9) ([mmun](https://github.com/mmun)) - Fix dummy app [\#8](https://github.com/emberjs/ember-collection/pull/8) ([mmun](https://github.com/mmun)) - Use ember@1.13.8 as default ember [\#7](https://github.com/emberjs/ember-collection/pull/7) ([mmun](https://github.com/mmun)) - rename to ember-collection [\#4](https://github.com/emberjs/ember-collection/pull/4) ([jonnii](https://github.com/jonnii)) - Hook up CircleCI [\#2](https://github.com/emberjs/ember-collection/pull/2) ([mmun](https://github.com/mmun)) - Tests 2.0 [\#1](https://github.com/emberjs/ember-collection/pull/1) ([shaunc](https://github.com/shaunc)) \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* ================================================ FILE: CODE_OF_CONDUCT.md ================================================ The Ember team and community are committed to everyone having a safe and inclusive experience. **Our Community Guidelines / Code of Conduct can be found here**: http://emberjs.com/guidelines/ For a history of updates, see the page history here: https://github.com/emberjs/website/commits/master/source/guidelines.html.erb ================================================ FILE: CONTRIBUTING.md ================================================ # How To Contribute ## Installation * `git clone ` * `cd my-addon` * `npm install` ## Linting * `npm run lint:hbs` * `npm run lint:js` * `npm run lint:js -- --fix` ## Running tests * `ember test` – Runs the test suite on the current Ember version * `ember test --server` – Runs the test suite in "watch mode" * `ember try:each` – Runs the test suite against multiple Ember versions ## Running the dummy application * `ember serve` * Visit the dummy application at [http://localhost:4200](http://localhost:4200). For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). ================================================ FILE: LICENSE.md ================================================ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Ember Collection [![Github Actions](https://github.com/adopted-ember-addons/ember-collection/workflows/CI/badge.svg)](https://github.com/adopted-ember-addons/ember-collection/actions?query=workflow%3ACI) [![Ember Observer Score](http://emberobserver.com/badges/ember-collection.svg)](http://emberobserver.com/addons/ember-collection) An efficient incremental rendering component with support for custom layouts and large lists. ### Table of Contents 1. [Installation](#installation) 1. [Usage](#usage) 1. [Layouts](#layouts) 1. [Build it](#build-it) 1. [How it works](#how-it-works) 1. [Run unit tests](#running-unit-tests) ## Installation * `ember install ember-collection` ## Submitting bugs Create a reproduction of the bug in https://ember-twiddle.com/ It would help us greatly to help you and to improve ember collection. ## Usage The height of the collection is inferred from its nearest relative parent. This is so you can just use CSS to style the container. So, first make sure the collection has a parent with `position: relative`, and then render a template: ```handlebars {{#ember-collection items=model cell-layout=(fixed-grid-layout 800 50) as |item index| }} {{item.name}} {{/ember-collection}} ``` Next, let's feed our template with some data: ``` javascript // define index route and return some data from model export default Ember.Route.extend({ model: function() { var items = []; for (var i = 0; i < 10000; i++) { items.push({name: "Item " + i}); } return items; } }); ``` Shazam! You should be able to see a scrollable area with 10,000 items in it. ### Required parameters You must specify `cell-layout` parameter so that *EmberCollection* can layout out your items. The provided layouts are described in the [Layouts](#layouts) section. ### Estimating width/height You can pass `estimated-width` and `estimated-height` to the collection, for situations where the collection cannot infer its height from its parent (e.g., when there's no DOM in FastBoot). Once the collection has been rendered, `estimated-width` and `estimated-height` have no effect. ### Actions #### scroll-change If you do not provide a `scroll-change` action name or closure action, scrolling will work normally. If you *do* specify `scroll-change`, ember-collection assumes that you want to handle the scroll-change action in a true data down, actions up manner. For this reason, ember-collection will not set `scroll-left` and `scroll-top` itself, but rather rely on you to update those properties based on action handling as you see fit. An example of specifying an action and keeping scrolling working normally looks like this: ```hbs {{#ember-collection items=model cell-layout=(fixed-grid-layout itemWidth itemHeight) scroll-left=scrollLeft scroll-top=scrollTop scroll-change=(action "scrollChange") as |item index| }}
{{item.name}}
{{/ember-collection}} ``` ```js export default Ember.Controller.extend({ scrollLeft: 0, scrollTop: 0, actions: { scrollChange(scrollLeft, scrollTop) { this.set('scrollLeft', scrollLeft); this.set('scrollTop', scrollTop); } } }); ``` ## Layouts ### Fixed Grid Layout The `fixed-grid-layout` will arrange the items in a grid to to fill the content area. The arguments for the layout are: | Argument | Description | | ------------ | --------------------------- | | `itemWidth` | The width of each item | | `itemHeight` | The height of each item | ```hbs {{#ember-collection items=model cell-layout=(fixed-grid-layout itemWidth itemHeight) scroll-left=scrollLeft scroll-top=scrollTop scroll-change=(action "scrollChange") as |item index| }}
{{item.name}}
{{/ember-collection}} ``` ### Mixed Grid Layout The `mixed-grid-layout` is used when each item has a known `width` and `height` and will arrange the items in rows from left to right fitting as many items in each row as possible. The arguments for the layout are: | Argument | Description | | ----------- | --------------------------- | | `itemSizes` | A collection of objects having `width` and `height` properties. Used to lookup with size of the corresponding index in the collection. | For example if you want the first element in `items` to have a size of `20x50` then the first element in `itemSizes` must be `{width: 20, height: 50}`. If the items have `width` and `height` properties you can use pass collection to `items` and `itemSizes`. ```hbs {{#ember-collection items=model cell-layout=(mixed-grid-layout itemSizes) scroll-left=scrollLeft scroll-top=scrollTop scroll-change=(action "scrollChange") as |item index| }}
{{item.name}}
{{/ember-collection}} ``` ### Percentage Columns Layout The `percentage-columns-layout` allows items to be laid out in a fixed number of columns sized using percentage widths with a fixed height in pixels. The arguments for the layout are: | Argument | Description | | ------------ | -------------------------------------------------------------------------------------------------------------- | | `itemCount` | The number of items passed to the collection. This is usually the number of items in the model (`model.length`). | | `columns` | An array of numbers not totaling more than 100. e.g. `[33.333, 66.666]`, `[25, 50, 10, 15]` | | `itemHeight` | The height in pixels of each item. | ```hbs {{#ember-collection items=model cell-layout=(percentage-columns-layout itemCount columns itemHeight) scroll-left=scrollLeft scroll-top=scrollTop scroll-change=(action "scrollChange") as |item index| }}
{{item.name}}
{{/ember-collection}} ``` ### Creating your own layout If none of the built in layouts included with *EmberCollection* fit your needs you can create your own. A layout is simply an object returned from a helper that conforms to the following interface. ```js import Ember from 'ember' export default Ember.Helper.helper(function(params, hash) { return { /** * Return an object that describes the size of the content area */ contentSize(clientWidth, clientHeight) { return { width, height }; } /** * Return the index of the first item shown. */ indexAt(offsetX, offsetY, clientWidth, clientHeight) { return Number; } /** * Return the number of items to display */ count(offsetX, offsetY, width, height) { return Number; } /** * Return the css that should be used to set the size and position of the item. */ formatItemStyle(itemIndex, clientWidth, clientHeight) { return String; } } }); ``` ## Build It 1. `git clone https://github.com/adopted-ember-addons/ember-collection.git` 2. `cd ember-collection` 3. `npm install` 5. `ember build` ## How it works *EmberCollection* will create enough rows to fill the visible area. It reacts to scroll events and reuses/repositions the rows as scrolled. ## Running unit tests ```sh npm install npm test ``` ## Thanks A lot of the work was sponsored by [Yapp Labs](https://www.yapp.us/), and some work was sponsored by [Tightrope Media Systems](http://trms.com). ================================================ FILE: RELEASE.md ================================================ # Release Releases are mostly automated using [release-it](https://github.com/release-it/release-it/) and [lerna-changelog](https://github.com/lerna/lerna-changelog/). ## Preparation Since the majority of the actual release process is automated, the primary remaining task prior to releasing is confirming that all pull requests that have been merged since the last release have been labeled with the appropriate `lerna-changelog` labels and the titles have been updated to ensure they represent something that would make sense to our users. Some great information on why this is important can be found at [keepachangelog.com](https://keepachangelog.com/en/1.0.0/), but the overall guiding principle here is that changelogs are for humans, not machines. When reviewing merged PR's the labels to be used are: * breaking - Used when the PR is considered a breaking change. * enhancement - Used when the PR adds a new feature or enhancement. * bug - Used when the PR fixes a bug included in a previous release. * documentation - Used when the PR adds or updates documentation. * internal - Used for internal changes that still require a mention in the changelog/release notes. ## Release Once the prep work is completed, the actual release is straight forward: * First ensure that you have `release-it` installed globally, generally done by using one of the following commands: ``` # using https://volta.sh volta install release-it # using Yarn yarn global add release-it # using npm npm install --global release-it ``` * Second, ensure that you have installed your projects dependencies: ``` yarn install ``` * And last (but not least 😁) do your release. It requires a [GitHub personal access token](https://github.com/settings/tokens) as `$GITHUB_AUTH` environment variable. Only "repo" access is needed; no "admin" or other scopes are required. ``` export GITHUB_AUTH="f941e0..." release-it ``` [release-it](https://github.com/release-it/release-it/) manages the actual release process. It will prompt you to to choose the version number after which you will have the chance to hand tweak the changelog to be used (for the `CHANGELOG.md` and GitHub release), then `release-it` continues on to tagging, pushing the tag and commits, etc. ================================================ FILE: addon/components/ember-collection/template.hbs ================================================
{{~#each this._cells as |cell|~}}
{{yield cell.item cell.index }}
{{~/each~}}
================================================ FILE: addon/components/ember-collection.js ================================================ import { A } from '@ember/array'; import Component from '@ember/component'; import { action, set, get } from '@ember/object'; import layout from './ember-collection/template'; import identity from '../utils/identity'; import needsRevalidate from '../utils/needs-revalidate'; class Cell { constructor(key, item, index, style) { this.key = key; this.hidden = false; this.item = item; this.index = index; this.style = style; } } function noop() {} export default Component.extend({ layout: layout, init() { // State pulled from attrs is prefixed with an underscore // so that there's no chance of shadowing the attrs proxy. this._buffer = undefined; this._cellLayout = undefined; this._rawItems = undefined; this._items = undefined; this._scrollLeft = undefined; this._scrollTop = undefined; this._clientWidth = undefined; this._clientHeight = undefined; this._contentSize = undefined; // this.firstCell = undefined; // this.lastCell = undefined; // this.cellCount = undefined; this.contentElement = undefined; this._cells = A(); this._cellMap = Object.create(null); // TODO: Super calls should always be at the top of the constructor. // I had to move the super call after the properties were defined to // work around what I believe is a bug in the attrs proxy. The problem // seems to arise when you: // // 1. Call this._super() immediately. // 2. Set a property on `this` that is both not in the // initial attrs hash and not on the prototype. this._super(); // initialize from passed in attrs let buffer = this.getAttr('buffer'); // getIntAttr('buffer', 5) this._buffer = (typeof buffer === 'number') ? buffer : 5; this._scrollLeft = this.getAttr('scroll-left') | 0; this._scrollTop = this.getAttr('scroll-top') | 0; this._clientWidth = this.getAttr('estimated-width') | 0; this._clientHeight = this.getAttr('estimated-height') | 0; this._scrollChange = this.getAttr('scroll-change'); }, _needsRevalidate(){ if (this.isDestroyed || this.isDestroying) {return;} if (this._isGlimmer2()) { this.rerender(); } else { needsRevalidate(this); } }, didReceiveAttrs() { // Work around emberjs/ember.js#11992. Affects <=1.13.8 and <=2.0.0. // This will likely be patched in 1.13.9 and 2.0.1. this._super(); this.updateItems(); this.updateScrollPosition(); }, willDestroyElement() { if (this._items && this._items.removeArrayObserver) { this._items.removeArrayObserver(this, { willChange: noop, didChange: '_needsRevalidate' }); } }, updateItems(){ this._cellLayout = this.getAttr('cell-layout'); var rawItems = this.getAttr('items'); if (this._rawItems !== rawItems) { if (this._items && this._items.removeArrayObserver) { this._items.removeArrayObserver(this, { willChange: noop, didChange: '_needsRevalidate' }); } this._rawItems = rawItems; var items = A(rawItems); this.set('_items', items); if (items && items.addArrayObserver) { items.addArrayObserver(this, { willChange: noop, didChange: '_needsRevalidate' }); } } }, updateScrollPosition(){ if (!this._scrollChange) { return; } // don't process bound scroll coords unless our action is being handled let scrollLeftAttr = this.getAttr('scroll-left'); if (scrollLeftAttr !== undefined) { scrollLeftAttr = parseInt(scrollLeftAttr, 10); if (this._scrollLeft !== scrollLeftAttr) { this.set('_scrollLeft', scrollLeftAttr); } } let scrollTopAttr = this.getAttr('scroll-top'); if (scrollTopAttr !== undefined) { scrollTopAttr = parseInt(scrollTopAttr, 10); if (this._scrollTop !== scrollTopAttr) { // console.log('updateScrollPosition', this._scrollTop, scrollTopAttr); this.set('_scrollTop', scrollTopAttr); } } }, updateContentSize() { var cellLayout = this._cellLayout; var contentSize = cellLayout.contentSize(this._clientWidth, this._clientHeight); if (this._contentSize === undefined || contentSize.width !== this._contentSize.width || contentSize.height !== this._contentSize.height) { this.set('_contentSize', contentSize); } }, willRender: function() { this.updateCells(); this.updateContentSize(); }, updateCells() { if (!this._items) { return; } const numItems = get(this._items, 'length'); if (this._cellLayout.length !== numItems) { this._cellLayout.length = numItems; } var priorMap = this._cellMap; var cellMap = Object.create(null); var index = this._cellLayout.indexAt(this._scrollLeft, this._scrollTop, this._clientWidth, this._clientHeight); var count = this._cellLayout.count(this._scrollLeft, this._scrollTop, this._clientWidth, this._clientHeight); var items = this._items; var bufferBefore = Math.min(index, this._buffer); index -= bufferBefore; count += bufferBefore; count = Math.min(count + this._buffer, get(items, 'length') - index); var i, style, itemIndex, itemKey, cell; var newItems = []; for (i=0; i= 0) { this.element.scrollTop = this._scrollTop; } } if (this._appliedScrollLeft !== this._scrollLeft) { this._appliedScrollLeft = this._scrollLeft; if (this._scrollLeft >= 0) { this.element.scrollLeft = this._scrollLeft; } } }, startScrollCheck() { const component = this; function step() { component.scrollCheck(); nextStep(); } function nextStep() { if (window.requestAnimationFrame) { component._animationFrame = requestAnimationFrame(step); } else { component._animationFrame = setTimeout(step, 16); } } nextStep(); }, cancelScrollCheck() { if (this._animationFrame) { if (window.requestAnimationFrame) { cancelAnimationFrame(this._animationFrame); } else { clearTimeout(this._animationFrame); } this._animationFrame = undefined; } }, scrollCheck() { let element = this.element; let scrollLeft = element.scrollLeft; let scrollTop = element.scrollTop; let scrollChanged = false; if (scrollLeft !== this._appliedScrollLeft || scrollTop !== this._appliedScrollTop) { scrollChanged = true; this._appliedScrollLeft = scrollLeft; this._appliedScrollTop = scrollTop; } let clientWidth = element.clientWidth; let clientHeight = element.clientHeight; let clientSizeChanged = false; if (clientWidth !== this._clientWidth || clientHeight !== this._clientHeight) { clientSizeChanged = true; this._clientWidth = clientWidth; this._clientHeight = clientHeight; } if (scrollChanged || clientSizeChanged) { join(this, function sendActionsFromScrollCheck(){ if (scrollChanged) { this.scrollChange(scrollLeft, scrollTop); } if (clientSizeChanged) { this.clientSizeChange(clientWidth, clientHeight); } }); } } }); ================================================ FILE: addon/layouts/grid.js ================================================ import FixedGrid from 'layout-bin-packer/fixed-grid'; import { formatPixelStyle } from '../utils/style-generators'; export default class Grid { constructor(cellWidth, cellHeight) { this.length = 0; this.bin = new FixedGrid(this, cellWidth, cellHeight); } contentSize(clientWidth/*, clientHeight*/) { return { width: clientWidth, height: this.bin.height(clientWidth) }; } indexAt(offsetX, offsetY, width, height) { return this.bin.visibleStartingIndex(offsetY, width, height); } positionAt(index, width /*,height*/) { return this.bin.position(index, width); } widthAt(index) { return this.bin.widthAtIndex(index); } heightAt(index) { return this.bin.heightAtIndex(index); } count(offsetX, offsetY, width, height) { return this.bin.numberVisibleWithin(offsetY, width, height, true); } formatItemStyle(itemIndex, clientWidth, clientHeight) { let pos = this.positionAt(itemIndex, clientWidth, clientHeight); let width = this.widthAt(itemIndex, clientWidth, clientHeight); let height = this.heightAt(itemIndex, clientWidth, clientHeight); return formatPixelStyle(pos, width, height); } } ================================================ FILE: addon/layouts/mixed-grid.js ================================================ import ShelfFirst from 'layout-bin-packer/shelf-first'; import { formatPixelStyle } from '../utils/style-generators'; export default class MixedGrid { constructor(content, width) { this.content = content; this.bin = new ShelfFirst(content, width); } contentSize(clientWidth/*, clientHeight*/) { return { width: clientWidth, height: this.bin.height(clientWidth) }; } indexAt(offsetX, offsetY, width, height) { return this.bin.visibleStartingIndex(offsetY, width, height); } positionAt(index, width, height) { return this.bin.position(index, width, height); } widthAt(index) { return this.bin.widthAtIndex(index); } heightAt(index) { return this.bin.heightAtIndex(index); } count(offsetX, offsetY, width, height) { return this.bin.numberVisibleWithin(offsetY, width, height, true); } formatItemStyle(itemIndex, clientWidth, clientHeight) { let pos = this.positionAt(itemIndex, clientWidth, clientHeight); let width = this.widthAt(itemIndex, clientWidth, clientHeight); let height = this.heightAt(itemIndex, clientWidth, clientHeight); return formatPixelStyle(pos, width, height); } } ================================================ FILE: addon/layouts/percentage-columns.js ================================================ import { assert } from '@ember/debug'; import ShelfFirst from 'layout-bin-packer/shelf-first'; import { formatPercentageStyle } from '../utils/style-generators'; export default class PercentageColumns { // How this layout works is by creating a fake grid that is 100px wide. // Each item's width is set to be the size of the column. The ShelfFirst lays out everything according to this fake grid. // When ember-collection asks for the style in formatItemStyle we pull the percent property to use as the width. constructor(itemCount, columns, height) { let total = columns.reduce(function(a, b) { return a+b; }); // Assert that the columns add up to 100. We don't want to enforce that they are EXACTLY 100 in case the user wants to use percentages. // for example [33.333, 66.666] assert('All columns must total 100 ' + total, total > 99 && total <= 100 ); let positions = []; var ci = 0; for (var i = 0; i < itemCount; i++) { positions.push({ width: columns[ci], height: height, percent: columns[ci] }); ci++; if (ci >= columns.length) { ci = 0; } } this.positions = positions; this.bin = new ShelfFirst(positions, 100); } contentSize(clientWidth/*, clientHeight*/) { let size = { width: clientWidth, height: this.bin.height(100) }; return size; } indexAt(offsetX, offsetY, width, height) { return this.bin.visibleStartingIndex(offsetY, 100, height); } positionAt(index, width, height) { return this.bin.position(index, 100, height); } widthAt(index) { return this.bin.widthAtIndex(index); } heightAt(index) { return this.bin.heightAtIndex(index); } count(offsetX, offsetY, width, height) { return this.bin.numberVisibleWithin(offsetY, 100, height, true); } formatItemStyle(itemIndex, clientWidth, clientHeight) { let pos = this.positionAt(itemIndex, 100, clientHeight); let width = this.positions[itemIndex].percent; let height = this.heightAt(itemIndex, 100, clientHeight); let x = Math.floor((pos.x / 100) * clientWidth); return formatPercentageStyle({x:x, y:pos.y}, width, height); } } ================================================ FILE: addon/utils/identity.js ================================================ import { guidFor } from '@ember/object/internals'; export default function identity(item) { let key; let type = typeof item; if (type === 'string' || type === 'number') { key = item; } else { key = guidFor(item); } return key; } ================================================ FILE: addon/utils/needs-revalidate.js ================================================ export default function needsRevalidate(view){ view._renderNode.isDirty = true; view._renderNode.ownerNode.emberView.scheduleRevalidate(view._renderNode, view.toString(), 'rerendering via needsRevalidate'); } ================================================ FILE: addon/utils/style-generators.js ================================================ import { translateCSS } from './translate'; export function formatPixelStyle(pos, width, height) { let css = 'position:absolute;top:0;left:0;'; css += translateCSS(pos.x, pos.y); css += 'width:' + width + 'px;height:' + height + 'px;'; return css; } export function formatPercentageStyle(pos, width, height) { let css = 'position:absolute;top:0;left:0;'; css += translateCSS(pos.x, pos.y); css += 'width:' + width + '%;height:' + height + 'px;'; return css; } ================================================ FILE: addon/utils/style-properties.js ================================================ import { capitalize, camelize } from '@ember/string'; const stylePrefixes = ['webkit', 'Webkit', 'ms', 'Moz', 'O']; const cssPrefixes = ['-webkit-','-ms-','-moz-','-o-']; const style = typeof document !== 'undefined' && document.documentElement && document.documentElement.style; function findProperty(property, css) { let prop = css ? camelize(property) : property; if (prop in style) { return property; } let capitalized = capitalize(prop); for (let i=0; i= 16.*" }, "publishConfig": { "registry": "https://registry.npmjs.org" }, "ember": { "edition": "octane" }, "ember-addon": { "configPath": "tests/dummy/config" }, "release-it": { "plugins": { "release-it-lerna-changelog": { "infile": "CHANGELOG.md", "launchEditor": true } }, "git": { "tagName": "v${version}" }, "github": { "release": true, "tokenRef": "GITHUB_AUTH" } } } ================================================ FILE: testem.js ================================================ module.exports = { test_page: 'tests/index.html?hidepassed', disable_watching: true, launch_in_ci: [ 'Chrome', 'Firefox' ], launch_in_dev: [ 'Chrome', 'Firefox', 'Safari' ], browser_args: { Chrome: { ci: [ // --no-sandbox is needed when running Chrome inside a container process.env.CI ? '--no-sandbox' : null, '--headless', '--disable-dev-shm-usage', '--disable-software-rasterizer', '--mute-audio', '--remote-debugging-port=0', '--window-size=1440,900' ].filter(Boolean) }, Firefox: { mode: 'ci', args: [ '--headless', '--window-size=1440,900' ] } } }; ================================================ FILE: tests/acceptance/list-view-test.js ================================================ import { currentURL, visit } from '@ember/test-helpers'; import { run } from '@ember/runloop'; import { module, skip } from 'qunit'; import startApp from '../../tests/helpers/start-app'; module('Acceptance | ember collection', function(hooks) { hooks.beforeEach(function() { this.application = startApp(); }); hooks.afterEach(function() { run(this.application, 'destroy'); }); skip('visiting /list-view', async function(assert) { await visit('/list-view'); assert.equal(currentURL(), '/list-view'); }); /* FOLLOWING IS OLD ACCEPTANCE TEST CODE THAT NEEDS TO BE REWRITTEN */ /* import Ember from 'ember'; import { test } from 'ember-qunit'; import moduleForView from '../helpers/module-for-view'; import { compile, generateContent, sortElementsByPosition, itemPositions } from '../helpers/helpers'; moduleForView("list-view", "acceptance", {}); test("should render an empty view when there is no content", function(assert) { var content = generateContent(0), height = 500, rowHeight = 50, emptyViewHeight = 170, itemViewClass = ListItemView.extend({ template: compile("{{name}}") }), emptyView = Ember.View.extend({ attributeBindings: ['style'], classNames: ['empty-view'], style: 'height:' + emptyViewHeight + 'px;' }); var view; Ember.run(this, function(){ view = this.subject({ content: content, height: height, rowHeight: rowHeight, itemViewClass: itemViewClass, emptyView: emptyView }); }); this.render(); assert.equal(view.get('element').style.height, "500px", "The list view height is correct"); assert.equal(this.$('.ember-list-container').height(), emptyViewHeight, "The scrollable view has the correct height"); assert.equal(this.$('.ember-list-item-view').length, 0, "The correct number of rows were rendered"); assert.equal(this.$('.empty-view').length, 1, "The empty view rendered"); Ember.run(function () { view.set('content', generateContent(10)); }); assert.equal(view.get('element').style.height, "500px", "The list view height is correct"); assert.equal(this.$('.ember-list-container').height(), 500, "The scrollable view has the correct height"); assert.equal(this.$('.ember-list-item-view').length, 10, "The correct number of rows were rendered"); assert.equal(this.$('.empty-view').length, 0, "The empty view is removed"); Ember.run(function () { view.set('content', content); }); assert.equal(view.get('element').style.height, "500px", "The list view height is correct"); assert.equal(this.$('.ember-list-container').height(), emptyViewHeight, "The scrollable view has the correct height"); assert.equal(this.$('.ember-list-item-view').length, 0, "The correct number of rows were rendered"); assert.equal(this.$('.empty-view').length, 1, "The empty view rendered"); Ember.run(function () { view.set('content', generateContent(10)); }); assert.equal(view.get('element').style.height, "500px", "The list view height is correct"); assert.equal(this.$('.ember-list-container').height(), 500, "The scrollable view has the correct height"); assert.equal(this.$('.ember-list-item-view').length, 10, "The correct number of rows were rendered"); assert.equal(this.$('.empty-view').length, 0, "The empty view has been removed"); }); test("should render a subset of the full content, based on the height, in the correct positions", function(assert) { var content = generateContent(100), height = 500, rowHeight = 50, itemViewClass = ListItemView.extend({ template: compile("{{name}}") }); var view; Ember.run(this, function(){ view = this.subject({ content: content, height: height, rowHeight: rowHeight, itemViewClass: itemViewClass }); }); this.render(); assert.equal(view.get('element').style.height, "500px", "The list view height is correct"); assert.equal(this.$('.ember-list-container').height(), 5000, "The scrollable view has the correct height"); var positionSorted = sortElementsByPosition(this.$('.ember-list-item-view')); assert.equal(this.$('.ember-list-item-view').length, 11, "The correct number of rows were rendered"); assert.equal(Ember.$(positionSorted[0]).text(), "Item 1"); assert.equal(Ember.$(positionSorted[10]).text(), "Item 11"); assert.deepEqual(itemPositions(view).map(yPosition), [0, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500]); }); test("should render correctly with an initial scrollTop", function(assert) { var content = generateContent(100), height = 500, rowHeight = 50, itemViewClass = ListItemView.extend({ template: compile("{{name}}") }); var view; Ember.run(this, function(){ view = this.subject({ content: content, height: height, rowHeight: rowHeight, itemViewClass: itemViewClass, scrollTop: 475 }); }); this.render(); assert.equal(this.$('.ember-list-item-view').length, 11, "The correct number of rows were rendered"); var positionSorted = sortElementsByPosition(this.$('.ember-list-item-view')); assert.equal(Ember.$(positionSorted[0]).text(), "Item 10"); assert.equal(Ember.$(positionSorted[10]).text(), "Item 20"); assert.deepEqual(itemPositions(view).map(yPosition), [450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950], "The rows are in the correct positions"); }); test("should perform correct number of renders and repositions on short list init", function (assert) { var content = generateContent(8), height = 60, width = 50, rowHeight = 10, positions = 0, renders = 0, itemViewClass = ListItemView.extend({ template: compile("{{name}}") }); Ember.subscribe("view.updateContext.render", { before: function(){}, after: function(name, timestamp, payload) { renders++; } }); Ember.subscribe("view.updateContext.positionElement", { before: function(){}, after: function(name, timestamp, payload) { positions++; } }); var view; Ember.run(this, function(){ view = this.subject({ content: content, height: height, width: width, rowHeight: rowHeight, itemViewClass: itemViewClass, scrollTop: 0 }); }); this.render(); assert.equal(renders, 7, "The correct number of renders occured"); assert.equal(positions, 14, "The correct number of positions occured"); }); test("should perform correct number of renders and repositions while short list scrolling", function (assert) { var content = generateContent(8), height = 60, width = 50, scrollTop = 50, rowHeight = 10, positions = 0, renders = 0, itemViewClass = ListItemView.extend({ template: compile("{{name}}") }); if (window.console) { Ember.ENABLE_PROFILING = true; } Ember.subscribe("view.updateContext.render", { before: function(){}, after: function(name, timestamp, payload) { renders++; } }); Ember.subscribe("view.updateContext.positionElement", { before: function(){}, after: function(name, timestamp, payload) { positions++; } }); var view; Ember.run(this, function(){ view = this.subject({ content: content, height: height, width: width, rowHeight: rowHeight, itemViewClass: itemViewClass, scrollTop: 0 }); }); this.render(); Ember.run(function () { view.scrollTo(scrollTop); }); assert.equal(renders, 14, "The correct number of renders occured"); assert.equal(positions, 21, "The correct number of positions occured"); }); test("should perform correct number of renders and repositions on long list init", function (assert) { var content = generateContent(200), height = 50, width = 50, rowHeight = 10, positions = 0, renders = 0, itemViewClass = ListItemView.extend({ template: compile("{{name}}") }); Ember.subscribe("view.updateContext.render", { before: function(){}, after: function(name, timestamp, payload) { renders++; } }); Ember.subscribe("view.updateContext.positionElement", { before: function(){}, after: function(name, timestamp, payload) { positions++; } }); var view; Ember.run(this, function(){ view = this.subject({ content: content, height: height, width: width, rowHeight: rowHeight, itemViewClass: itemViewClass, scrollTop: 0 }); }); this.render(); assert.equal(renders, ((height / 10) + 1), "The correct number of renders occurred"); assert.equal(positions, 12, "The correct number of positions occurred"); }); test("should be programatically scrollable", function(assert) { var content = generateContent(100), height = 500, rowHeight = 50, itemViewClass = ListItemView.extend({ template: compile("{{name}}") }); var view; Ember.run(this, function(){ view = this.subject({ content: content, height: height, rowHeight: rowHeight, itemViewClass: itemViewClass }); }); this.render(); Ember.run(function() { view.scrollTo(475); }); assert.equal(this.$('.ember-list-item-view').length, 11, "The correct number of rows were rendered"); assert.deepEqual(itemPositions(view).map(yPosition), [450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950], "The rows are in the correct positions"); }); test("height change", function(assert){ var content = generateContent(100), height = 500, rowHeight = 50, itemViewClass = ListItemView.extend({ template: compile("{{name}}") }); var view; Ember.run(this, function(){ view = this.subject({ content: content, height: height, rowHeight: rowHeight, itemViewClass: itemViewClass }); }); this.render(); assert.equal(this.$('.ember-list-item-view').length, 11, "The correct number of rows were rendered"); assert.deepEqual(itemPositions(view).map(yPosition), [0, 50, 100, 150, 200, 250, 300, 350, 400, 450, 500], "The rows are in the correct positions"); Ember.run(function() { view.set('height', 100); }); assert.equal(this.$('.ember-list-item-view').length, 3, "The correct number of rows were rendered"); assert.deepEqual(itemPositions(view).map(yPosition), [0, 50, 100], "The rows are in the correct positions"); Ember.run(function() { view.set('height', 50); }); assert.equal(this.$('.ember-list-item-view').length, 2, "The correct number of rows were rendered"); assert.deepEqual(itemPositions(view).map(yPosition), [0, 50], "The rows are in the correct positions"); Ember.run(function() { view.set('height', 100); }); assert.equal(this.$('.ember-list-item-view').length, 3, "The correct number of rows were rendered"); assert.deepEqual(itemPositions(view).map(yPosition), [0, 50, 100], "The rows are in the correct positions" ); }); test("adding a column, when everything is already within viewport", function(assert){ // start off with 2x3 grid visible and 4 elements // x: visible, +: padding w/ element, 0: element not-drawn, o: padding w/o element, ?: no element // // x x --| // x x |- viewport // ? ? --| var content = generateContent(4), width = 100, height = 150, rowHeight = 50, elementWidth = 50, itemViewClass = ListItemView.extend({ template: compile("A:{{name}}{{view view.NestedViewClass}}"), NestedViewClass: Ember.View.extend({ tagName: 'span', template: compile("B:{{name}}") }) }); var view; Ember.run(this, function(){ view = this.subject({ content: content, width: width, height: height, rowHeight: rowHeight, elementWidth: elementWidth, itemViewClass: itemViewClass, scrollTop: 0 }); }); this.render(); assert.deepEqual(itemPositions(view), [ { x: 0, y: 0 }, { x: 50, y: 0 }, { x: 0, y: 50 }, { x: 50, y: 50 } ], "initial render: The rows are rendered in the correct positions"); assert.equal(this.$('.ember-list-item-view').length, 4, "initial render: The correct number of rows were rendered"); // rotate to a with 3x2 grid visible and 8 elements // rapid dimension changes Ember.run(function() { view.set('width', 140); }); Ember.run(function() { view.set('width', 150); }); // x: visible, +: padding w/ element, 0: element not-drawn, o: padding w/o element // // x x x --| // x ? ? |- viewport // ? ? ? --| assert.equal(this.$('.ember-list-item-view').length, 4, "after width + height change: the correct number of rows were rendered"); assert.deepEqual(itemPositions(view), [ { x: 0, y: 0 }, { x: 50, y: 0 }, { x: 100, y: 0 }, { x: 0, y: 50 } ], "after width + height change: The rows are in the correct positions"); var sortedElements = sortElementsByPosition(this.$('.ember-list-item-view')); var texts = Ember.$.map(sortedElements, function(el){ return Ember.$(el).text(); }); assert.deepEqual(texts, [ 'A:Item 1B:Item 1', 'A:Item 2B:Item 2', 'A:Item 3B:Item 3', 'A:Item 4B:Item 4' ], 'after width + height change: elements should be rendered in expected position'); }); test("height and width change after with scroll – simple", function(assert){ // start off with 2x3 grid visible and 10 elements, at top of scroll // x: visible, +: padding w/ element, 0: element not-drawn, o: padding w/o element // // x x --| // x x |- viewport // x x --| // + + // 0 0 var content = generateContent(10), width = 100, height = 150, rowHeight = 50, elementWidth = 50, itemViewClass = ListItemView.extend({ template: compile("A:{{name}}{{view view.NestedViewClass}}"), NestedViewClass: Ember.View.extend({ tagName: 'span', template: compile("B:{{name}}") }) }); var view; Ember.run(this, function(){ view = this.subject({ content: content, width: width, height: height, rowHeight: rowHeight, elementWidth: elementWidth, itemViewClass: itemViewClass, scrollTop: 0 }); }); this.render(); assert.deepEqual(itemPositions(view), [ { x: 0, y: 0 }, { x: 50, y: 0 }, { x: 0, y: 50 }, { x: 50, y: 50 }, { x: 0, y: 100 }, { x: 50, y: 100 }, { x: 0, y: 150 }, { x: 50, y: 150 } ], "initial render: The rows are rendered in the correct positions"); assert.equal(this.$('.ember-list-item-view').length, 8, "initial render: The correct number of rows were rendered"); // user is scrolled near the bottom of the list Ember.run(function(){ view.scrollTo(101); }); // x: visible, +: padding w/ element, 0: element not-drawn, o: padding w/o element // // 0 0 // o o // x x --| // x x |- viewport // x x --| assert.equal(this.$('.ember-list-item-view').length, 8, "after scroll: The correct number of rows were rendered"); assert.deepEqual(itemPositions(view), [ { x: 0, y: 50 }, { x: 50, y: 50 }, { x: 0, y: 100 }, { x: 50, y: 100 }, { x: 0, y: 150 }, { x: 50, y: 150 }, /* padding / { x: 0, y: 200 }, { x: 50, y: 200 }], "after scroll: The rows are in the correct positions"); // rotate to a with 3x2 grid visible and 8 elements Ember.run(function() { view.set('width', 150); view.set('height', 100); }); // x: visible, +: padding w/ element, 0: element not-drawn, o: padding w/o element // // 0 0 0 // x x x // x x x --| // x o o --|- viewport assert.equal(this.$('.ember-list-item-view').length, 9, "after width + height change: the correct number of rows were rendered"); assert.deepEqual(itemPositions(view), [ /* / { x: 50, y: 0 }, { x: 100, y: 0 }, { x: 0, y: 50 }, { x: 50, y: 50 }, { x: 100, y: 50 }, { x: 0, y: 100 }, { x: 50, y: 100 }, { x: 100, y: 100 }, { x: 0, y: 150 }], "after width + height change: The rows are in the correct positions"); var sortedElements = sortElementsByPosition(this.$('.ember-list-item-view')); var texts = Ember.$.map(sortedElements, function(el){ return Ember.$(el).text(); }); assert.deepEqual(texts, [ 'A:Item 2B:Item 2', 'A:Item 3B:Item 3', 'A:Item 4B:Item 4', 'A:Item 5B:Item 5', 'A:Item 6B:Item 6', 'A:Item 7B:Item 7', 'A:Item 8B:Item 8', 'A:Item 9B:Item 9', 'A:Item 10B:Item 10' ], 'after width + height change: elements should be rendered in expected position'); }); test("height and width change after with scroll – 1x2 -> 2x2 with 5 items", function(assert){ // x: visible, +: padding w/ element, 0: element not-drawn, o: padding w/o element // // x --| // x --|- viewport // + // 0 // 0 var content = generateContent(5), width = 50, height = 100, rowHeight = 50, elementWidth = 50, itemViewClass = ListItemView.extend({ template: compile("A:{{name}}{{view view.NestedViewClass}}"), NestedViewClass: Ember.View.extend({ tagName: 'span', template: compile("B:{{name}}") }) }); var view; Ember.run(this, function(){ view = this.subject({ content: content, width: width, height: height, rowHeight: rowHeight, elementWidth: elementWidth, itemViewClass: itemViewClass, scrollTop: 0 }); }); this.render(); assert.deepEqual(itemPositions(view), [ { x: 0, y: 0 }, { x: 0, y: 50 }, { x: 0, y: 100 } ], "initial render: The rows are rendered in the correct positions"); assert.equal(this.$('.ember-list-item-view').length, 3, "initial render: The correct number of rows were rendered"); // user is scrolled near the bottom of the list Ember.run(function(){ view.scrollTo(151); }); // x: visible, +: padding w/ element, 0: element not-drawn, o: padding w/o element // // 0 // 0 // o // x --| // x --|- viewport // 0 assert.equal(this.$('.ember-list-item-view').length, 3, "after scroll: The correct number of rows were rendered"); assert.deepEqual(itemPositions(view), [ { x: 0, y: 100 }, { x: 0, y: 150 }, /* padding / { x: 0, y: 200 }], "after scroll: The rows are in the correct positions"); // rotate to a with 2x2 grid visible and 8 elements Ember.run(function() { view.set('width', 100); view.set('height', 100); }); // x: visible, +: padding w/ element, 0: element not-drawn, o: padding w/o element // // 0 0 // x x --| // x o --|- viewport // o assert.equal(this.$('.ember-list-item-view').length, 5, "after width + height change: the correct number of rows were rendered"); assert.deepEqual(itemPositions(view), [ { x: 0, y: 0 }, { x: 50, y: 0 }, { x: 0, y: 50 }, { x: 50, y: 50 }, { x: 0, y: 100 } ], "The rows are in the correct positions"); var sortedElements = sortElementsByPosition(this.$('.ember-list-item-view')); var texts = Ember.$.map(sortedElements, function(el){ return Ember.$(el).text(); }); assert.deepEqual(texts, [ 'A:Item 1B:Item 1', 'A:Item 2B:Item 2', 'A:Item 3B:Item 3', 'A:Item 4B:Item 4', 'A:Item 5B:Item 5' ], 'elements should be rendered in expected position'); }); test("elementWidth change", function(assert){ var i, positionSorted, content = generateContent(100), height = 200, width = 200, rowHeight = 50, elementWidth = 100, itemViewClass = ListItemView.extend({ template: compile("{{name}}") }); var view; Ember.run(this, function(){ view = this.subject({ content: content, height: height, width: width, rowHeight: rowHeight, itemViewClass: itemViewClass, elementWidth: elementWidth }); }); this.render(); assert.equal(this.$('.ember-list-item-view').length, 10, "The correct number of rows were rendered"); assert.deepEqual(itemPositions(view), [ { x:0, y: 0 }, { x:100, y: 0 }, { x:0, y: 50 }, { x:100, y: 50 }, { x:0 , y: 100 }, { x:100, y: 100 }, { x:0, y: 150 }, { x:100, y: 150 }, { x:0, y: 200 }, { x:100, y: 200 }], "The rows are in the correct positions"); positionSorted = sortElementsByPosition(this.$('.ember-list-item-view')); for(i = 0; i < 10; i++) { assert.equal(Ember.$(positionSorted[i]).text(), "Item " + (i+1)); } Ember.run(function() { view.set('width', 100); }); assert.equal(this.$('.ember-list-item-view').length, 5, "The correct number of rows were rendered"); positionSorted = sortElementsByPosition(this.$('.ember-list-item-view')); assert.deepEqual(itemPositions(view), [ { x: 0, y: 0}, { x: 0, y: 50}, { x: 0, y: 100}, { x: 0, y: 150}, { x: 0, y: 200} ], "The rows are in the correct positions"); for(i = 0; i < 5; i++) { assert.equal(Ember.$(positionSorted[i]).text(), "Item " + (i+1)); } // Test a width smaller than elementWidth, should behave the same as width === elementWidth Ember.run(function () { view.set('width', 50); }); assert.equal(this.$('.ember-list-item-view').length, 5, "The correct number of rows were rendered"); positionSorted = sortElementsByPosition(this.$('.ember-list-item-view')); assert.deepEqual(itemPositions(view), [ { x: 0, y: 0}, { x: 0, y: 50}, { x: 0, y: 100}, { x: 0, y: 150}, { x: 0, y: 200} ], "The rows are in the correct positions"); for(i = 0; i < 5; i++) { assert.equal(Ember.$(positionSorted[i]).text(), "Item " + (i+1)); } assert.ok(this.$().is('.ember-list-view-list'), 'has correct list related class'); Ember.run(function() { view.set('width', 200); }); assert.ok(this.$().is('.ember-list-view-grid'), 'has correct grid related class'); positionSorted = sortElementsByPosition(this.$('.ember-list-item-view')); assert.equal(this.$('.ember-list-item-view').length, 10, "The correct number of rows were rendered"); assert.deepEqual(itemPositions(view), [ { x:0, y: 0 }, { x:100, y: 0 }, { x:0, y: 50 }, { x:100, y: 50 }, { x:0 , y: 100 }, { x:100, y: 100 }, { x:0, y: 150 }, { x:100, y: 150 }, { x:0, y: 200 }, { x:100, y: 200 }], "The rows are in the correct positions"); for(i = 0; i < 10; i++) { assert.equal(Ember.$(positionSorted[i]).text(), "Item " + (i+1)); } }); test("elementWidth change with scroll", function(assert){ var i, positionSorted, content = generateContent(100), height = 200, width = 200, rowHeight = 50, elementWidth = 100, itemViewClass = ListItemView.extend({ template: compile("{{name}}") }); var view; Ember.run(this, function(){ view = this.subject({ content: content, height: height, width: width, rowHeight: rowHeight, itemViewClass: itemViewClass, elementWidth: elementWidth }); }); this.render(); Ember.run(function(){ view.scrollTo(1000); }); assert.equal(this.$('.ember-list-item-view').length, 10, "after scroll 1000 - The correct number of rows were rendered"); assert.deepEqual(itemPositions(view), [ { x:0, y: 1000 }, { x:100, y: 1000 }, { x:0, y: 1050 }, { x:100, y: 1050 }, { x:0 , y: 1100 }, { x:100, y: 1100 }, { x:0, y: 1150 }, { x:100, y: 1150 }, { x:0, y: 1200 }, { x:100, y: 1200 }], "after scroll 1000 - The rows are in the correct positions"); positionSorted = sortElementsByPosition(this.$('.ember-list-item-view')); for (i = 0; i < 10; i++) { assert.equal(Ember.$(positionSorted[i]).text(), "Item " + (i + 41)); } Ember.run(function() { view.set('width', 100); }); assert.equal(this.$('.ember-list-item-view').length, 5, " after width 100 -The correct number of rows were rendered"); positionSorted = sortElementsByPosition(this.$('.ember-list-item-view')); assert.deepEqual(itemPositions(view), [ { x:0, y: 2000 }, { x:0, y: 2050 }, { x:0 , y: 2100 }, { x:0, y: 2150 }, { x:0, y: 2200 }], "after width 100 - The rows are in the correct positions"); for(i = 0; i < 5; i++) { assert.equal(Ember.$(positionSorted[i]).text(), "Item " + (i + 41)); } Ember.run(function() { view.set('width', 200); }); positionSorted = sortElementsByPosition(this.$('.ember-list-item-view')); assert.equal(this.$('.ember-list-item-view').length, 10, "after width 200 - The correct number of rows were rendered"); assert.deepEqual(itemPositions(view), [ { x:0, y: 1000 }, { x:100, y: 1000 }, { x:0, y: 1050 }, { x:100, y: 1050 }, { x:0 , y: 1100 }, { x:100, y: 1100 }, { x:0, y: 1150 }, { x:100, y: 1150 }, { x:0, y: 1200 }, { x:100, y: 1200 }], "after width 200 - The rows are in the correct positions"); for(i = 0; i < 10; i++) { assert.equal(Ember.$(positionSorted[i]).text(), "Item " + (i + 41)); } }); test("A property of an item can be changed", function(assert) { var content = generateContent(100), height = 500, rowHeight = 50, itemViewClass = ListItemView.extend({ template: compile("{{name}}") }); var view; Ember.run(this, function(){ view = this.subject({ content: content, height: height, rowHeight: rowHeight, itemViewClass: itemViewClass }); }); this.render(); //Change name Ember.run(function() { content.set('0.name', 'First change'); }); assert.equal(this.$('.ember-list-item-view:eq(0)').text(), "First change", "The item's name has been updated"); //Scroll down, change name, and scroll back up Ember.run(function() { view.scrollTo(600); }); Ember.run(function() { content.set('0.name', 'Second change'); }); Ember.run(function() { view.scrollTo(0); }); assert.equal(this.$('.ember-list-item-view:eq(0)').text(), "Second change", "The item's name has been updated"); }); test("The list view is wrapped in an extra div to support JS-emulated scrolling", function(assert) { var view; Ember.run(this, function(){ view = this.subject({ content: Ember.A(), height: 100, rowHeight: 50 }); }); this.render(); assert.equal(this.$('.ember-list-container').length, 1, "expected a ember-list-container wrapper div"); assert.equal(this.$('.ember-list-container > .ember-list-item-view').length, 0, "expected ember-list-items inside the wrapper div"); }); test("When scrolled past the totalHeight, views should not be recycled in. This is to support overscroll", function(assert) { var view; Ember.run(this, function(){ view = this.subject({ content: generateContent(2), height:100, rowHeight: 50, itemViewClass: ListItemView.extend({ template: compile("Name: {{name}}") }) }); }); this.render(); Ember.run(function(){ view.scrollTo(150); }); var positionSorted = sortElementsByPosition(this.$('.ember-list-item-view')); assert.equal(this.$('.ember-list-item-view').length, 2, "after width 200 - The correct number of rows were rendered"); assert.deepEqual(itemPositions(view), [ { x:0, y: 0 }, { x:0, y: 50 }] , "went beyond scroll max via overscroll"); assert.equal(Ember.$(positionSorted[0]).text(), "Name: Item " + 1); assert.equal(Ember.$(positionSorted[1]).text(), "Name: Item " + 2); }); test("When list-view is unable to scroll, scrollTop should be zero", function(assert) { var view; Ember.run(this, function(){ view = this.subject({ content: generateContent(2), height:400, rowHeight: 100, itemViewClass: ListItemView.extend({ template: compile("Name: {{name}}") }) }); }); this.render(); Ember.run(function(){ view.scrollTo(1); }); assert.equal(view.get('scrollTop'), 0, "Scrolltop should be zero"); }); test("Creating a ListView without height and rowHeight properties should throw an exception", function(assert) { assert.throws(()=>{ Ember.run(()=>{ this.subject({ content: generateContent(4) }); }); this.render(); }, /A ListView must be created with a height and a rowHeight./, "Throws exception."); }); test("Creating a ListView without height and rowHeight properties should throw an exception", function(assert) { assert.throws(()=>{ Ember.run(()=>{ this.subject({ content: generateContent(4) }); }); this.render(); }, /A ListView must be created with a height and a rowHeight./, "Throws exception."); }); test("handle strange ratios between height/rowHeight", function(assert) { var view; Ember.run(this, function(){ view = this.subject({ content: generateContent(15), height: 235, rowHeight: 73, itemViewClass: ListItemView.extend({ template: compile("Name: {{name}}") }) }); }); this.render(); var positionSorted = sortElementsByPosition(this.$('.ember-list-item-view')); assert.equal(this.$('.ember-list-item-view').length, 5); assert.deepEqual(itemPositions(view), [ { x:0, y: 0 }, { x:0, y: 73 }, { x:0, y: 146 }, { x:0, y: 219 }, { x:0, y: 292 } ] , "went beyond scroll max via overscroll"); for (var i = 0; i < positionSorted.length; i++) { assert.equal(Ember.$(positionSorted[i]).text(), "Name: Item " + (i + 1)); } view.scrollTo(1000); positionSorted = sortElementsByPosition(this.$('.ember-list-item-view')); assert.equal(this.$('.ember-list-item-view').length, 5); // expected // ----- // 0 | // 1 | // 2 | // 3 | // 4 | <--- not rendered // 5 | // 6 | // 7 | // 8 | // 9 | // ---- // 10 | <- buffer // ---- // 11 | <-- partially visible // 12 | <--- visible // 13 | // 14 | // ---- assert.deepEqual(itemPositions(view), [ { x:0, y: 730 }, // <-- buffer { x:0, y: 803 }, // <-- partially visible { x:0, y: 876 }, // <-- in view { x:0, y: 949 }, // <-- in view { x:0, y: 1022 } // <-- in view ], "went beyond scroll max via overscroll"); assert.equal(Ember.$(positionSorted[0]).text(), "Name: Item 11"); assert.equal(Ember.$(positionSorted[1]).text(), "Name: Item 12"); assert.equal(Ember.$(positionSorted[2]).text(), "Name: Item 13"); assert.equal(Ember.$(positionSorted[3]).text(), "Name: Item 14"); assert.equal(Ember.$(positionSorted[4]).text(), "Name: Item 15"); }); test("handle bindable rowHeight", function(assert) { var view; Ember.run(this, function(){ view = this.subject({ content: generateContent(15), height: 400, rowHeight: 100, itemViewClass: ListItemView.extend({ template: compile("Name: {{name}}") }) }); }); this.render(); var positionSorted = sortElementsByPosition(this.$('.ember-list-item-view')); assert.equal(this.$('.ember-list-item-view').length, 5); assert.equal(view.get('totalHeight'), 1500); // expected // ----- // 0 | // 1 | // 2 | // 3 | // ----- // 4 | <--- buffer // ----- // 5 | // 6 | // 7 | // 8 | // 9 | // 10 | // 11 | // 12 | // 13 | // 14 | // ----- // assert.deepEqual(itemPositions(view), [ { x:0, y: 0 }, // <- visible { x:0, y: 100 }, // <- visible { x:0, y: 200 }, // <- visible { x:0, y: 300 }, // <- visible { x:0, y: 400 } // <- buffer ] , "inDOM views are correctly positioned: before rowHeight change"); assert.equal(Ember.$(positionSorted[0]).text(), "Name: Item 1"); assert.equal(Ember.$(positionSorted[1]).text(), "Name: Item 2"); assert.equal(Ember.$(positionSorted[2]).text(), "Name: Item 3"); assert.equal(Ember.$(positionSorted[3]).text(), "Name: Item 4"); Ember.run(view, 'set', 'rowHeight', 200); positionSorted = sortElementsByPosition(this.$('.ember-list-item-view')); assert.equal(this.$('.ember-list-item-view').length, 3); assert.equal(view.get('totalHeight'), 3000); // expected // ----- // 0 | // 1 | // ----| // 2 | <--- buffer // ----| // 3 | // 4 | // 5 | // 6 | // 7 | // 8 | // 9 | // 10 | // 11 | // 12 | // 13 | // 14 | // ----- assert.deepEqual(itemPositions(view), [ { x:0, y: 0 }, // <-- visible { x:0, y: 200 }, // <-- visible { x:0, y: 400 } // <-- buffer ], "inDOM views are correctly positioned: after rowHeight change"); assert.equal(Ember.$(positionSorted[0]).text(), "Name: Item 1"); assert.equal(Ember.$(positionSorted[1]).text(), "Name: Item 2"); assert.equal(Ember.$(positionSorted[2]).text(), "Name: Item 3"); }); var scrollYChanged, reuseChildren; moduleForView("list-view", "acceptance", { setup: function() { scrollYChanged = 0; reuseChildren = 0; }, subject: function(options, factory) { return factory.extend({ init: function () { this.on('scrollYChanged', function () { scrollYChanged++; }); this._super(); }, _reuseChildren: function () { reuseChildren++; this._super(); } }).create(options); } }); test("should trigger scrollYChanged correctly", function (assert) { var view; Ember.run(this, function(){ view = this.subject({ content: generateContent(10), height: 100, rowHeight: 50 }); }); this.render(); assert.equal(scrollYChanged, 0, 'scrollYChanged should not fire on init'); view.scrollTo(1); assert.equal(scrollYChanged, 1, 'scrollYChanged should fire after scroll'); view.scrollTo(1); assert.equal(scrollYChanged, 1, 'scrollYChanged should not fire for same value'); }); moduleForView("list-view", "acceptance", { setup: function() { scrollYChanged = 0; reuseChildren = 0; }, subject: function(options, factory) { return factory.extend({ _reuseChildren: function () { reuseChildren++; this._super(); } }).create(options); } }); test("should trigger reuseChildren correctly", function (assert) { var view; Ember.run(this, function() { view = this.subject({ content: generateContent(10), height: 100, rowHeight: 50 }); }); this.render(); assert.equal(reuseChildren, 1, 'initialize the content'); view.scrollTo(1); assert.equal(reuseChildren, 1, 'should not update the content'); view.scrollTo(51); assert.equal(reuseChildren, 2, 'should update the content'); }); function yPosition(position){ return position.y; } function xPosition(position){ return position.x; } */ }); ================================================ FILE: tests/dummy/app/app.js ================================================ import Application from '@ember/application'; import Resolver from './resolver'; import loadInitializers from 'ember-load-initializers'; import config from './config/environment'; const App = Application.extend({ modulePrefix: config.modulePrefix, podModulePrefix: config.podModulePrefix, Resolver }); loadInitializers(App, config.modulePrefix); export default App; ================================================ FILE: tests/dummy/app/components/.gitkeep ================================================ ================================================ FILE: tests/dummy/app/controllers/.gitkeep ================================================ ================================================ FILE: tests/dummy/app/controllers/mixed.js ================================================ import Controller from '@ember/controller'; export default class extends Controller {} ================================================ FILE: tests/dummy/app/controllers/percentages.js ================================================ import Controller from '@ember/controller'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class PercentagesController extends Controller { @tracked columns = [20, 60, 20]; @action changeColumn(col) { switch (col) { case 1: this.columns = [25, 50, 25]; break; case 2: this.columns = [20, 20, 40, 20]; break; case 3: this.columns = [33.33, 33.33, 33.33]; break; case 4: this.columns = [50, 50]; break; case 5: this.columns = [100]; break; default: this.columns = [50, 50]; break; } } } ================================================ FILE: tests/dummy/app/controllers/scroll-position.js ================================================ import Controller from '@ember/controller'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; export default class ScrollPositionController extends Controller { @tracked itemWidth = 100; @tracked itemHeight = 100; @tracked containerWidth = 315; @tracked containerHeight = 600; @tracked scrollLeft = 0; @tracked scrollTop = 0; @action updateContainerWidth(ev) { this.containerWidth = parseInt(ev.target.value, 10); } @action updateContainerHeight(ev) { this.containerHeight = parseInt(ev.target.value, 10); } @action makeSquare() { this.itemWidth = 100; this.itemHeight = 100; } @action makeRow() { this.itemWidth = 300; this.itemHeight = 100; } @action makeLongRect() { this.itemWidth = 100; this.itemHeight = 50; } @action makeTallRect() { this.itemWidth = 50; this.itemHeight = 100; } @action scrollChange(scrollLeft, scrollTop){ this.scrollLeft = scrollLeft; this.scrollTop = scrollTop; } } ================================================ FILE: tests/dummy/app/controllers/simple.js ================================================ import Controller from '@ember/controller'; import { action } from '@ember/object'; import { tracked } from '@glimmer/tracking'; function shuffle(array) { var currentIndex = array.length, temporaryValue, randomIndex ; // While there remain elements to shuffle... while (0 !== currentIndex) { // Pick a remaining element... randomIndex = Math.floor(Math.random() * currentIndex); currentIndex -= 1; // And swap it with the current element. temporaryValue = array[currentIndex]; array[currentIndex] = array[randomIndex]; array[randomIndex] = temporaryValue; } return array; } export default class SimpleController extends Controller { @tracked itemWidth = 100; @tracked itemHeight = 100; @tracked containerWidth = 315; @tracked containerHeight = 600; @action updateContainerWidth(ev) { this.containerWidth = parseInt(ev.target.value, 10); } @action updateContainerHeight(ev) { this.containerHeight = parseInt(ev.target.value, 10); } @action shuffle() { this.model = shuffle(this.get('model').slice(0)); } @action makeSquare() { this.itemWidth = 100; this.itemHeight = 100; } @action makeRow() { this.itemWidth = 300; this.itemHeight = 100; } @action makeLongRect() { this.itemWidth = 100; this.itemHeight = 50; } @action makeTallRect() { this.itemWidth = 50; this.itemHeight = 100; } } ================================================ FILE: tests/dummy/app/helpers/.gitkeep ================================================ ================================================ FILE: tests/dummy/app/helpers/size-to-style.js ================================================ import { htmlSafe } from '@ember/template'; import { helper } from '@ember/component/helper'; export default helper(function ([width, height]) { return htmlSafe(`position: relative; width: ${width}px; height: ${height}px;`); }); ================================================ FILE: tests/dummy/app/index.html ================================================ Ember Collection Demos {{content-for "head"}} {{content-for "head-footer"}} {{content-for "body"}} {{content-for "body-footer"}} ================================================ FILE: tests/dummy/app/models/.gitkeep ================================================ ================================================ FILE: tests/dummy/app/resolver.js ================================================ import Resolver from 'ember-resolver'; export default Resolver; ================================================ FILE: tests/dummy/app/router.js ================================================ import EmberRouter from '@ember/routing/router'; import config from './config/environment'; const Router = EmberRouter.extend({ location: config.locationType, rootURL: config.rootURL }); Router.map(function() { this.route('simple'); this.route('scroll-position'); this.route('mixed'); this.route('percentages'); }); export default Router; ================================================ FILE: tests/dummy/app/routes/.gitkeep ================================================ ================================================ FILE: tests/dummy/app/routes/mixed.js ================================================ import Route from '@ember/routing/route'; function getRandomInt() { return Math.floor(Math.random() * (251) + 75); } export default Route.extend({ model: function() { var items = []; for (var i = 0; i < 1000; i++) { var width = getRandomInt(); var height = getRandomInt(); items.push({ name: 'Item ' + (i + 1) + '(' + width + 'x' + height + ')', width: width, height: height }); } return items; } }); ================================================ FILE: tests/dummy/app/routes/percentages.js ================================================ import Route from '@ember/routing/route'; import makeModel from '../utils/make-model'; export default Route.extend({ model: makeModel() }); ================================================ FILE: tests/dummy/app/routes/scroll-position.js ================================================ import Route from '@ember/routing/route'; import makeModel from '../utils/make-model'; export default Route.extend({ model: makeModel() }); ================================================ FILE: tests/dummy/app/routes/simple.js ================================================ import Route from '@ember/routing/route'; import makeModel from '../utils/make-model'; export default Route.extend({ model: makeModel() }); ================================================ FILE: tests/dummy/app/styles/app.css ================================================ body { padding-top: 70px; } code { padding: 0; padding-top: 0.2em; padding-bottom: 0.2em; margin: 0; font-size: 85%; background-color: rgba(0,0,0,0.04); border-radius: 3px; } .ember-list-view { overflow: auto; position: relative; width: 100%; } .ember-list-item-view { position: absolute; } .mobile-list .ember-list-view { -webkit-overflow-scrolling: touch; overflow-scrolling: touch; } .mobile-images-list .ember-list-view { background: white; -webkit-overflow-scrolling: touch; overflow-scrolling: touch; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; -o-user-select: none; user-select: none; } .mobile-large-images-list .photo-grid-list-item { width: 154px; height: 154px; float: left; } .mobile-large-images-list .medium-frame { background-color: white; padding: 4px; height: 133px; width: 133px; margin: 6px 7px 7px 6px; -webkit-box-shadow: rgba(0, 0, 0, 0.2) 2px 2px 2px; -moz-box-shadow: rgba(0, 0, 0, 0.2) 2px 2px 2px; box-shadow: rgba(0, 0, 0, 0.2) 2px 2px 2px; } .mobile-small-images-list .photo-grid-list-item { width: 154px; height: 154px; float: left; } .mobile-small-images-list .medium-frame { background-color: white; padding: 4px; height: 133px; width: 133px; margin: 6px 7px 7px 6px; -webkit-box-shadow: rgba(0, 0, 0, 0.2) 2px 2px 2px; -moz-box-shadow: rgba(0, 0, 0, 0.2) 2px 2px 2px; box-shadow: rgba(0, 0, 0, 0.2) 2px 2px 2px; } .multi-height .ember-list-item-view { width: 500px; } .multi-height .row { position: absolute; width: 500px; } .multi-height .dog { height: 50px; background-color: teal; } .multi-height .cat { height: 100px; background-color: pink; } .multi-height .other { height: 150px; background-color: purple; } .pull-to-refresh-list .ember-list-view { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; -o-user-select: none; user-select: none; } .pull-to-refresh-list .ember-list-item-view { color: white; width: 100px; height: 100px; } .pull-to-refresh-list .ember-list-item-view img{ width: 100px; height: 100px; } .pull-to-refresh-list .ember-list-item-view .text { position: absolute; top: 0; left: 0; } .pull-to-refresh-animation { background-color: yellow; height: 45px; left:0; right: 0; position: absolute; text-align: center; padding-top:30px; } .spinner { width: 30px; height: 30px; background-color: #333; margin: 100px auto; -webkit-animation: rotateplane 1.2s infinite ease-in-out; animation: rotateplane 1.2s infinite ease-in-out; } @-webkit-keyframes rotateplane { 0% { -webkit-transform: perspective(120px) } 50% { -webkit-transform: perspective(120px) rotateY(180deg) } 100% { -webkit-transform: perspective(120px) rotateY(180deg) rotateX(180deg) } } @keyframes rotateplane { 0% { transform: perspective(120px) rotateX(0deg) rotateY(0deg); -webkit-transform: perspective(120px) rotateX(0deg) rotateY(0deg) } 50% { transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg); -webkit-transform: perspective(120px) rotateX(-180.1deg) rotateY(0deg) } 100% { transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg); -webkit-transform: perspective(120px) rotateX(-180deg) rotateY(-179.9deg); } } .pull-to-refresh-list .ember-list-item-view { background: black; } .pull-to-refresh-list .ember-list-container { -webkit-transform: translate3d(0px, 0px, 0); } .virtual .ember-list-view { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; -o-user-select: none; user-select: none; border: 1px solid red; } .virtual .ember-list-item-view { position: absolute; color: steelblue; width: 100px; height: 100px; } .virtual .ember-list-item-view img{ width: 100px; height: 100px; } .virtual .ember-list-item-view .text { position: absolute; top: 0; left: 0; } .virtual .ember-list-container { -webkit-transform: translate3d(0px, 0px, 0); } .list-item { box-sizing: border-box; width: 100%; height: 100%; border: 1px solid black; } ================================================ FILE: tests/dummy/app/templates/application.hbs ================================================
{{outlet}}
================================================ FILE: tests/dummy/app/templates/components/.gitkeep ================================================ ================================================ FILE: tests/dummy/app/templates/index.hbs ================================================

Ember Collection Demos

These are the demos for the ember-collection addon. Their source code is within the the repo's tests/dummy application.

Fixed Grid Layout

Use the fixed-grid-layout when all items are the same size. The width and height of each item are bound as is the width and height of the container. Ember collection will re-layout items when any of these items change.

Demo

Mixed Grid Layout

Use the mixed-grid-layout when items can be a different size. The collection being itterated over is passed to the mixed-grid-layout helper. Each item in the collection must provide a width and height property of the item.

Demo

Scroll Position

Use the scroll-top and scroll-left attributes to programtically scroll to a specific location in the collection. This can also be used to set an initial scroll position.

Demo

Percentage Columns

Use the percentage-columns-layout to render a fixed number of columns that are sized using percentages. The columns paramter should be an array of integers that add to 100.

Demo

================================================ FILE: tests/dummy/app/templates/mixed.hbs ================================================
{{item.name}}
================================================ FILE: tests/dummy/app/templates/percentages.hbs ================================================
{{item.name}}
================================================ FILE: tests/dummy/app/templates/scroll-position.hbs ================================================

Scroll Position

{{!-- template-lint-disable require-input-label --}}

Container Width: {{this.containerWidth}} Container Height: {{this.containerHeight}}

Item Height: {{this.itemHeight}} Item Width: {{this.itemWidth}}

Scroll Left: Scroll Top:

Note: The usage of this component remembers its scroll position. Try it by navigating away from this route and then returning.


{{item.name}}
================================================ FILE: tests/dummy/app/templates/simple.hbs ================================================

Simple

{{!-- template-lint-disable require-input-label --}} Container Width: {{this.containerWidth}} Container Height: {{this.containerHeight}}

Item Height: {{this.itemHeight}} Item Width: {{this.itemWidth}}


{{item.name}}
================================================ FILE: tests/dummy/app/utils/fixtures.js ================================================ export var types = [ {id: 1, type: "cat", name: "Andrew"}, {id: 2, type: "cat", name: "Andrew"}, {id: 3, type: "cat", name: "Bruce"}, {id: 4, type: "other", name: "Xbar"}, {id: 5, type: "dog", name: "Caroline"}, {id: 6, type: "cat", name: "David"}, {id: 7, type: "other", name: "Xbar"}, {id: 8, type: "other", name: "Xbar"}, {id: 9, type: "dog", name: "Edward"}, {id: 10, type: "dog", name: "Francis"}, {id: 11, type: "dog", name: "George"}, {id: 12, type: "other", name: "Xbar"}, {id: 13, type: "dog", name: "Harry"}, {id: 14, type: "cat", name: "Ingrid"}, {id: 15, type: "other", name: "Xbar"}, {id: 16, type: "cat", name: "Jenn"}, {id: 17, type: "cat", name: "Kelly"}, {id: 18, type: "other", name: "Xbar"}, {id: 19, type: "other", name: "Xbar"}, {id: 20, type: "cat", name: "Larry"}, {id: 21, type: "other", name: "Xbar"}, {id: 22, type: "cat", name: "Manny"}, {id: 23, type: "dog", name: "Nathan"}, {id: 24, type: "cat", name: "Ophelia"}, {id: 25, type: "dog", name: "Patrick"}, {id: 26, type: "other", name: "Xbar"}, {id: 27, type: "other", name: "Xbar"}, {id: 28, type: "other", name: "Xbar"}, {id: 29, type: "other", name: "Xbar"}, {id: 30, type: "other", name: "Xbar"}, {id: 31, type: "cat", name: "Quincy"}, {id: 32, type: "dog", name: "Roger"}, ]; ================================================ FILE: tests/dummy/app/utils/images.js ================================================ var images = [ 'images/ebryn.jpg', 'images/iterzic.jpg', 'images/kselden.jpg', 'images/machty.jpg', 'images/rwjblue.jpg', 'images/stefanpenner.jpg', 'images/tomdale.jpg', 'images/trek.jpg', 'images/wagenet.jpg', 'images/wycats.jpg' ]; var smallImages = [ 'images/small/Ba_Gua_Feng-Shui-Mirror.gif', 'images/small/Bonsai.gif', 'images/small/Chouchin_Reinensai_Lantern.gif', 'images/small/Chouchin_Kuroshiro_Lantern_.gif', 'images/small/Chouchin_Shinku_Lantern.gif', 'images/small/Fuurin_Glass_Wind_Chime.gif', 'images/small/Geta_Wooden_Sandal_.gif', 'images/small/Gunsen_Fan_.gif', 'images/small/iChing_Kouka_Heads-Coin.gif', 'images/small/iChing_Kouka_Tails_Coin.gif', 'images/small/Ishidourou_Snow_Lantern.gif', 'images/small/Kakejiku_Hanging_Scroll.gif', 'images/small/Katana_and_Sheath.gif', 'images/small/Kimono_Buru_Blue.gif', 'images/small/Kimono_Chairo_Tan.gif', 'images/small/Koi.gif', 'images/small/Shamisen.gif', 'images/small/Shodou_Calligraphy.gif', 'images/small/Torii.gif', 'images/small/Tsukubai_Water_Basin.gif' ]; var strangeRatios = [ 'images/strange-ratios/horizontal-rectangle.jpg', 'images/strange-ratios/square.jpg', 'images/strange-ratios/vertical-rectangle.jpg' ]; export default { images, smallImages, strangeRatios }; ================================================ FILE: tests/dummy/app/utils/make-model.js ================================================ import images from './images'; export default function makeModel(count = 1000, imageArrayName = 'images') { var imagesArray = images[imageArrayName]; return function model() { var result = []; for (var i = 0; i < count; i++) { result.push({ name: `Item ${i+1}`, imageSrc: imagesArray[i%imagesArray.length] }); } return result; }; } ================================================ FILE: tests/dummy/config/ember-cli-update.json ================================================ { "schemaVersion": 0, "packages": [ { "name": "ember-cli", "version": "3.12.0", "blueprints": [ { "name": "addon", "outputRepo": "https://github.com/ember-cli/ember-addon-output", "codemodsSource": "ember-addon-codemods-manifest@1", "isBaseBlueprint": true, "options": [ "--yarn", "--no-welcome" ] } ] } ] } ================================================ FILE: tests/dummy/config/environment.js ================================================ 'use strict'; module.exports = function(environment) { let ENV = { modulePrefix: 'dummy', environment, rootURL: '/', locationType: 'history', EmberENV: { FEATURES: { // Here you can enable experimental features on an ember canary build // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true }, EXTEND_PROTOTYPES: { // Prevent Ember Data from overriding Date.parse. Date: false } }, APP: { // Here you can pass flags/options to your application instance // when it is created } }; if (environment === 'development') { // ENV.APP.LOG_RESOLVER = true; // ENV.APP.LOG_ACTIVE_GENERATION = true; // ENV.APP.LOG_TRANSITIONS = true; // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; // ENV.APP.LOG_VIEW_LOOKUPS = true; } if (environment === 'test') { // Testem prefers this... ENV.locationType = 'none'; // keep test console output quieter ENV.APP.LOG_ACTIVE_GENERATION = false; ENV.APP.LOG_VIEW_LOOKUPS = false; ENV.APP.rootElement = '#ember-testing'; ENV.APP.autoboot = false; } if (environment === 'production') { // here you can enable a production-specific feature } return ENV; }; ================================================ FILE: tests/dummy/config/optional-features.json ================================================ { "application-template-wrapper": false, "default-async-observers": true, "jquery-integration": false, "template-only-glimmer-components": true } ================================================ FILE: tests/dummy/config/targets.js ================================================ 'use strict'; const browsers = [ 'last 1 Chrome versions', 'last 1 Firefox versions', 'last 1 Safari versions' ]; const isCI = !!process.env.CI; const isProduction = process.env.EMBER_ENV === 'production'; if (isCI || isProduction) { browsers.push('ie 11'); } module.exports = { browsers }; ================================================ FILE: tests/dummy/public/crossdomain.xml ================================================ ================================================ FILE: tests/dummy/public/robots.txt ================================================ # http://www.robotstxt.org User-agent: * Disallow: ================================================ FILE: tests/helpers/destroy-app.js ================================================ import { run } from '@ember/runloop'; export default function destroyApp(application) { run(application, 'destroy'); } ================================================ FILE: tests/helpers/helpers.js ================================================ import { A } from '@ember/array'; import { get } from '@ember/object'; import Ember from 'ember'; var compile = Ember.Handlebars.compile; function generateContent(n) { var content = A([]); for (var i = 0; i < n; i++) { content.push({name: "Item " + (i+1)}); } return content; } function findScrollable(context) { return context.querySelector('.ember-collection > div:first-child'); // scrollable's element } function findContainer(context) { return context.querySelector('.ember-collection > div:first-child > div:first-child'); // scrollable's content element } function findItems(context) { return Array.prototype.slice.call( context.querySelectorAll('.ember-collection > div:first-child > div:first-child > div') // scrollable's content's children (cells) ); } function findVisibleItems(context) { let items = Array.prototype.slice.call( context.querySelectorAll('.ember-collection > div:first-child > div:first-child > div') ) return items.filter(item => { let style = getComputedStyle(item); return style.display !== 'none' && style.visibility === 'visible'; }); } function extractPosition(element) { let parentRect = element.parentElement.getBoundingClientRect(); let elementRect = element.getBoundingClientRect(); if (elementRect.width > 0 && elementRect.height > 0) { return { left: elementRect.left - parentRect.left, top: elementRect.top - parentRect.top, width: elementRect.width, height: elementRect.height }; } return null; } function sortItemsByPosition(view, visibleOnly) { var find = visibleOnly ? findVisibleItems : findItems; var items = find(view); return sortElementsByPosition(items); } function sortElementsByPosition (elements) { return elements .filter(elem => extractPosition(elem)) .sort(function(a, b) { return sortByPosition(extractPosition(a), extractPosition(b)); }); } function sortByPosition(a, b) { if (b.top === a.top){ return (a.left - b.left); } return (a.top - b.top); } function itemPositions(view) { return A(findItems(view)).toArray().map(function(e) { return extractPosition(e); }).sort(sortByPosition); } function checkContent(view, assert, expectedFirstItem, expectedCount) { var elements = sortItemsByPosition(view.element, true); var content = A(view.get('content') || []); assert.ok( expectedFirstItem + expectedCount <= get(content, 'length'), 'No more items than are in content are rendered.'); var buffer = view.get('buffer') === undefined ? 5 : view.get('buffer'); // TODO: we are recapitulating calculations done by fixed grid, as // we don't have access to the layout. This will not work with // mixed grid layout. // // In the future, if a listener for actual first item and count are // included in interface, we can limit ourselves to just recomputing // the number that should be in the buffer. var width = view.get('width') | 0; var itemWidth = view.get('itemWidth') || 1; var istart = Math.max(expectedFirstItem - buffer, 0); // TODO: padding is one extra row -- how to calculate with mixed grid var padding = Math.floor(width / itemWidth); // include buffer before var scount = expectedCount + Math.min(expectedFirstItem, buffer); // include padding (in case of non-integral scroll) var numItems = get(content, 'length'); var pcount = scount + Math.min(Math.max(numItems - istart - scount, 0), padding); // include buffer after var count = pcount + Math.min(Math.max(numItems - istart - pcount, 0), buffer); assert.equal( elements.length, count, "Rendered expected number of elements."); for (let i = 0; i < count; i++) { let elt = elements[i]; let item = content.objectAt(i + istart); assert.dom(elt).hasText(item.name, 'Item ' + (i + 1) + ' rendered'); } } export { itemPositions, generateContent, extractPosition, compile, findContainer, findScrollable, findItems, findVisibleItems, checkContent, sortItemsByPosition }; ================================================ FILE: tests/helpers/module-for-acceptance.js ================================================ import { module } from 'qunit'; import { resolve } from 'rsvp'; import startApp from '../helpers/start-app'; import destroyApp from '../helpers/destroy-app'; export default function(name, options = {}) { module(name, { beforeEach() { this.application = startApp(); if (options.beforeEach) { return options.beforeEach.apply(this, arguments); } }, afterEach() { let afterEach = options.afterEach && options.afterEach.apply(this, arguments); return resolve(afterEach).then(() => destroyApp(this.application)); } }); } ================================================ FILE: tests/helpers/module-for-view.js ================================================ import { deprecate } from '@ember/application/deprecations'; import { tryInvoke } from '@ember/utils'; import { run } from '@ember/runloop'; import { merge } from '@ember/polyfills'; import Ember from 'ember'; import TestModule from 'ember-test-helpers/test-module'; import { getResolver } from 'ember-test-helpers/test-resolver'; import { createModule } from 'ember-qunit/qunit-module'; var TestModuleForView = TestModule.extend({ init: function(viewName, description, callbacks) { this.viewName = viewName; this._super.call(this, 'component:' + viewName, description, callbacks); this.setupSteps.push(this.setupView); }, initNeeds: function() { this.needs = []; // toplevel refers to class extended from Ember.View if (this.subjectName !== 'component:toplevel') { this.needs.push(this.subjectName); } if (this.callbacks.needs) { this.needs = this.needs.concat(this.callbacks.needs); delete this.callbacks.needs; } }, setupView: function() { var _this = this; var resolver = getResolver(); var container = this.container; var context = this.context; var templateName = 'template:' + this.viewName; var template = resolver.resolve(templateName); if (template) { container.register(templateName, template); container.injection(this.subjectName, 'template', templateName); } context.dispatcher = Ember.EventDispatcher.create(); context.dispatcher.setup({}, '#ember-testing'); this.callbacks.render = function(options) { var containerView = Ember.ContainerView.create(merge({container: container}, options)); var view = run(function(){ var subject = context.subject(); containerView.pushObject(subject); containerView.appendTo('#ember-testing'); return subject; }); _this.teardownSteps.unshift(function() { run(function() { tryInvoke(containerView, 'destroy'); }); }); return view.$(); }; this.callbacks.append = function() { deprecate('this.append() is deprecated. Please use this.render() instead.'); return this.callbacks.render(); }; context.$ = function() { var $view = this.render(); var subject = this.subject(); if (arguments.length){ return subject.$.apply(subject, arguments); } else { return $view; } }; }, defaultSubject: function(options, factory) { return factory.create(options); } }); export default function moduleForView(name, description, callbacks) { createModule(TestModuleForView, name, description, callbacks); } ================================================ FILE: tests/helpers/start-app.js ================================================ import Application from '../../app'; import config from '../../config/environment'; import { merge } from '@ember/polyfills'; import { run } from '@ember/runloop'; export default function startApp(attrs) { let attributes = merge({}, config.APP); attributes.autoboot = true; attributes = merge(attributes, attrs); // use defaults, but you can override; return run(() => { let application = Application.create(attributes); application.setupForTesting(); application.injectTestHelpers(); return application; }); } ================================================ FILE: tests/index.html ================================================ Dummy Tests {{content-for "head"}} {{content-for "test-head"}} {{content-for "head-footer"}} {{content-for "test-head-footer"}} {{content-for "body"}} {{content-for "test-body"}}
{{content-for "body-footer"}} {{content-for "test-body-footer"}} ================================================ FILE: tests/templates/fixed-grid.js ================================================ import { hbs } from 'ember-cli-htmlbars'; export default hbs` {{!-- template-lint-disable no-curly-component-invocation --}}
{{#ember-collection items=this.content cell-layout=(fixed-grid-layout this.itemWidth this.itemHeight) estimated-width=this.width estimated-height=this.height scroll-left=this.offsetX scroll-top=this.offsetY buffer=this.buffer class="ember-collection" as |item| ~}}
{{item.name}}
{{~/ember-collection~}}
`; ================================================ FILE: tests/templates/indexed.js ================================================ import { hbs } from 'ember-cli-htmlbars'; export default hbs` {{!-- template-lint-disable no-curly-component-invocation --}}
{{#ember-collection items=this.content cell-layout=(fixed-grid-layout this.itemWidth this.itemHeight) estimated-width=this.width estimated-height=this.height scroll-left=this.offsetX scroll-top=this.offsetY buffer=this.buffer class="ember-collection" as |item index| ~}}
{{index}}:
{{~/ember-collection~}}
`; ================================================ FILE: tests/templates/percentage.js ================================================ import { hbs } from 'ember-cli-htmlbars'; export default hbs` {{!-- template-lint-disable no-curly-component-invocation --}}
{{#ember-collection items=this.content cell-layout=(percentage-columns-layout this.content.length this.columns this.itemHeight) estimated-width=this.width estimated-height=this.height scroll-left=this.offsetX scroll-top=this.offsetY buffer=this.buffer class="ember-collection" as |item| ~}}
{{item.name}}
{{~/ember-collection~}}
`; ================================================ FILE: tests/test-helper.js ================================================ /* global QUnit */ import Application from '../app'; import config from '../config/environment'; import { setApplication } from '@ember/test-helpers'; import { start } from 'ember-qunit'; import { setup } from 'qunit-dom'; setup(QUnit.assert); setApplication(Application.create(config.APP)); start(); ================================================ FILE: tests/unit/.gitkeep ================================================ ================================================ FILE: tests/unit/content-test.js ================================================ import ArrayProxy from '@ember/array/proxy'; import { A } from '@ember/array'; import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render, settled } from '@ember/test-helpers'; import { generateContent, sortItemsByPosition, findItems, findVisibleItems, findContainer, checkContent } from '../helpers/helpers'; import fixedGridTemplate from '../templates/fixed-grid'; import indexedTemplate from '../templates/indexed'; var nItems = 100; var itemWidth = 100; var itemHeight = 40; var width = 500; var renderedWidth = 520; // adjusted for scrollbar var height = 400; module('manipulate content', function(hooks) { setupRenderingTest(hooks); test("replacing the list content", async function(assert) { var content = generateContent(nItems); this.setProperties({height, width: renderedWidth, itemHeight, itemWidth, content}); await render(fixedGridTemplate); this.set('content', A([{name: 'The only item'}])); await settled(); assert.equal(findItems(this.element) .filter(function(elem) { return getComputedStyle(elem).display !== 'none'; }) .length, 1, "The rendered list was updated"); assert.equal( findItems(this.element)[0].getBoundingClientRect().height, itemHeight, "The items have the correct height"); checkContent(this, assert, 0, 1); }); test("adding to the front of the list content", async function(assert) { var content = generateContent(nItems); this.setProperties({height, width: renderedWidth, itemHeight, itemWidth, content}); await render(fixedGridTemplate); content.unshiftObject({name: "Item -1"}); await settled(); var positionSorted = sortItemsByPosition(this.element); assert.dom(positionSorted[0]) .hasTextContaining("Item -1", "The item has been inserted in the list"); var expectedRows = Math.ceil((nItems + 1) / (width / itemWidth)); assert.equal( findContainer(this.element).getBoundingClientRect().height, expectedRows * itemHeight, "The scrollable view has the correct height"); checkContent(this, assert, 0, 50); }); test("inserting in the middle of visible content", async function(assert) { var content = generateContent(nItems); this.setProperties({height, width: renderedWidth, itemHeight, itemWidth, content}); await render(fixedGridTemplate); content.insertAt(2, {name: "Item 2'"}); await settled(); var positionSorted = sortItemsByPosition(this.element); assert.dom(positionSorted[0]) .hasTextContaining("Item 1", "The item has been inserted in the list"); assert.dom(positionSorted[2]) .hasTextContaining("Item 2'", "The item has been inserted in the list"); checkContent(this, assert, 0, 50); }); test("clearing the content", async function(assert) { var content = generateContent(nItems); this.setProperties({height, width: renderedWidth, itemHeight, itemWidth, content}); await render(fixedGridTemplate); content.clear(); await settled(); assert.equal(findItems(this.element) .filter(function(elem) { return getComputedStyle(elem).display !== 'none'; }) .length, 0, "The rendered list does not contain any elements."); }); test("deleting the first element", async function(assert) { var content = generateContent(nItems); this.setProperties({height, width: renderedWidth, itemHeight, itemWidth, content}); await render(fixedGridTemplate); var positionSorted = sortItemsByPosition(this.element); assert.dom(positionSorted[0]) .hasTextContaining("Item 1", "Item 1 has not been removed from the list."); content.removeAt(0); await settled(); positionSorted = sortItemsByPosition(this.element); assert.dom(positionSorted[0]) .hasTextContaining("Item 2", "Item 1 has been remove from the list."); checkContent(this, assert, 0, 50); }); test("working with an ArrayProxy", async function(assert) { var content = ArrayProxy.create({content: A(generateContent(nItems)) }); this.setProperties({height, width: renderedWidth, itemHeight, itemWidth, content}); await render(fixedGridTemplate); assert.equal(findItems(this.element) .filter(function(elem) { return getComputedStyle(elem).display !== 'none'; }) .length, 60, "The rendered list was updated"); assert.equal( findItems(this.element)[0].getBoundingClientRect().height, itemHeight, "The items have the correct height"); checkContent(this, assert, 0, 50); }); test("indexes update correctly", async function(assert) { var content = generateContent(30); var filterIndexes = []; this.setProperties({height, width, itemHeight, itemWidth, content, filterIndexes}); await render(indexedTemplate); this.set('content', [content[1], content[3], content[7], content[13]]); await settled(); function joinContent(context) { return findVisibleItems(context).map(item => item.textContent).join('').split(':').sort().join(':'); } assert.equal(joinContent(this.element), ":0:1:2:3", "The indexes updated correctly"); this.set('content', [content[1], content[3], content[7], content[13], content[27]]); await settled(); assert.equal(joinContent(this.element), ":0:1:2:3:4", "The indexes updated correctly"); }); }); ================================================ FILE: tests/unit/fixed-grid-test.js ================================================ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render } from '@ember/test-helpers'; import { generateContent, sortItemsByPosition } from '../helpers/helpers'; import template from '../templates/fixed-grid'; module('display in fixed grid', function(hooks) { setupRenderingTest(hooks); test('display 5 in 6', async function(assert) { var width = 150, height = 500, itemWidth = 50, itemHeight = 50; var offsetY = 100; var content = generateContent(5); this.setProperties({ width, height, itemWidth, itemHeight, content, offsetY }); await render(template); var positionSorted = sortItemsByPosition(this.element); assert.dom(positionSorted[0]) .hasTextContaining("Item 1", "The first item has not been hidden"); }); }); ================================================ FILE: tests/unit/layout-test.js ================================================ import { hbs } from 'ember-cli-htmlbars'; import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render } from '@ember/test-helpers'; import { generateContent } from '../helpers/helpers'; var nItems = 5; var itemWidth = 100; var itemHeight = 40; var width = 500; var height = 400; var columns = [25, 50, 15, 10]; module('Basic layout tests', function(hooks) { setupRenderingTest(hooks); test("ember-collection calls formatItemStyle", async function(assert) { var content = generateContent(nItems); var callCount = 0; var fakeLayout = { indexAt: function() { return 0; }, count: function() { return nItems; }, contentSize: function() { return {width, height}; }, formatItemStyle: function() { callCount++; } }; var template = hbs`
{{item.name}}
`; this.setProperties({height, width, itemHeight, itemWidth, content, columns, fakeLayout}); await render(template); assert.equal(callCount, nItems, 'formatItemStyle is called for each rendered item'); }); }); ================================================ FILE: tests/unit/multi-height-list-view-test.js ================================================ import { get } from '@ember/object'; import { A } from '@ember/array'; import { run } from '@ember/runloop'; import { setupRenderingTest } from 'ember-qunit'; import '@ember/test-helpers'; import { module, skip } from 'qunit'; import { sortItemsByPosition, findItems } from '../helpers/helpers'; // import { hbs } from 'ember-cli-htmlbars'; // TODO: Remove these declarations. They're just there to keep JSHint happy. let compile, itemPositions, ListItemView, ReusableListItemView; module('multi-height', function(hooks) { setupRenderingTest(hooks); skip("Correct height based on content", function(assert) { var content = [ { id: 1, type: "cat", height: 100, name: "Andrew" }, { id: 3, type: "cat", height: 100, name: "Bruce" }, { id: 4, type: "other", height: 150, name: "Xbar" }, { id: 5, type: "dog", height: 50, name: "Caroline" }, { id: 6, type: "cat", height: 100, name: "David" }, { id: 7, type: "other", height: 150, name: "Xbar" }, { id: 8, type: "other", height: 150, name: "Xbar" }, { id: 9, type: "dog", height: 50, name: "Edward" }, { id: 10, type: "dog", height: 50, name: "Francis" }, { id: 11, type: "dog", height: 50, name: "George" }, { id: 12, type: "other", height: 150, name: "Xbar" }, { id: 13, type: "dog", height: 50, name: "Harry" }, { id: 14, type: "cat", height: 100, name: "Ingrid" }, { id: 15, type: "other", height: 150, name: "Xbar" }, { id: 16, type: "cat", height: 100, name: "Jenn" }, { id: 17, type: "cat", height: 100, name: "Kelly" }, { id: 18, type: "other", height: 150, name: "Xbar" }, { id: 19, type: "other", height: 150, name: "Xbar" }, { id: 20, type: "cat", height: 100, name: "Larry" }, { id: 21, type: "other", height: 150, name: "Xbar" }, { id: 22, type: "cat", height: 100, name: "Manny" }, { id: 23, type: "dog", height: 50, name: "Nathan" }, { id: 24, type: "cat", height: 100, name: "Ophelia" }, { id: 25, type: "dog", height: 50, name: "Patrick" }, { id: 26, type: "other", height: 150, name: "Xbar" }, { id: 27, type: "other", height: 150, name: "Xbar" }, { id: 28, type: "other", height: 150, name: "Xbar" }, { id: 29, type: "other", height: 150, name: "Xbar" }, { id: 30, type: "other", height: 150, name: "Xbar" }, { id: 31, type: "cat", height: 100, name: "Quincy" }, { id: 32, type: "dog", height: 50, name: "Roger" }, ]; var view; run(this, function(){ view = this.subject({ content: A(content), height: 300, width: 500, rowHeight: 100, itemViews: { cat: ListItemView.extend({ template: compile("Meow says {{name}} expected: cat === {{type}} {{id}}") }), dog: ListItemView.extend({ template: compile("Woof says {{name}} expected: dog === {{type}} {{id}}") }), other: ListItemView.extend({ template: compile("Potato says {{name}} expected: other === {{type}} {{id}}") }) }, itemViewForIndex: function(idx) { return this.itemViews[A(this.get('content')).objectAt(idx).type]; }, heightForIndex: function(idx) { return get(A(this.get('content')).objectAt(idx), 'height'); } }); }); this.render(); assert.equal(view.get('totalHeight'), 3350); var positionSorted = sortItemsByPosition(this); assert.equal(findItems(this).length, 4); assert.dom(positionSorted[0]).hasText("Meow says Andrew expected: cat === cat 1"); assert.dom(positionSorted[1]).hasText("Meow says Bruce expected: cat === cat 3"); assert.dom(positionSorted[2]).hasText("Potato says Xbar expected: other === other 4"); assert.dom(positionSorted[3]).hasText("Woof says Caroline expected: dog === dog 5"); assert.deepEqual(itemPositions(view), [ { x:0, y: 0 }, // <-- in view { x:0, y: 100 }, // <-- in view { x:0, y: 200 }, // <-- in view { x:0, y: 350 } // <-- buffer ], 'went beyond scroll max via overscroll'); run(view, 'scrollTo', 1000); positionSorted = sortItemsByPosition(this); assert.dom(positionSorted[0]).hasText("Potato says Xbar expected: other === other 12"); assert.dom(positionSorted[1]).hasText("Woof says Harry expected: dog === dog 13"); assert.dom(positionSorted[2]).hasText("Meow says Ingrid expected: cat === cat 14"); assert.dom(positionSorted[3]).hasText("Potato says Xbar expected: other === other 15"); assert.deepEqual(itemPositions(view), [ { x:0, y: 950 }, // <-- partially in view { x:0, y: 1100 }, // <-- in view { x:0, y: 1150 }, // <-- in view { x:0, y: 1250 } // <-- partially in view ], 'went beyond scroll max via overscroll'); }); skip("Correct height based on view", function(assert) { var content = [ { id: 1, type: "cat", name: "Andrew" }, { id: 3, type: "cat", name: "Bruce" }, { id: 4, type: "other", name: "Xbar" }, { id: 5, type: "dog", name: "Caroline" }, { id: 6, type: "cat", name: "David" }, { id: 7, type: "other", name: "Xbar" }, { id: 8, type: "other", name: "Xbar" }, { id: 9, type: "dog", name: "Edward" }, { id: 10, type: "dog", name: "Francis" }, { id: 11, type: "dog", name: "George" }, { id: 12, type: "other", name: "Xbar" }, { id: 13, type: "dog", name: "Harry" }, { id: 14, type: "cat", name: "Ingrid" }, { id: 15, type: "other", name: "Xbar" }, { id: 16, type: "cat", name: "Jenn" }, { id: 17, type: "cat", name: "Kelly" }, { id: 18, type: "other", name: "Xbar" }, { id: 19, type: "other", name: "Xbar" }, { id: 20, type: "cat", name: "Larry" }, { id: 21, type: "other", name: "Xbar" }, { id: 22, type: "cat", name: "Manny" }, { id: 23, type: "dog", name: "Nathan" }, { id: 24, type: "cat", name: "Ophelia" }, { id: 25, type: "dog", name: "Patrick" }, { id: 26, type: "other", name: "Xbar" }, { id: 27, type: "other", name: "Xbar" }, { id: 28, type: "other", name: "Xbar" }, { id: 29, type: "other", name: "Xbar" }, { id: 30, type: "other", name: "Xbar" }, { id: 31, type: "cat", name: "Quincy" }, { id: 32, type: "dog", name: "Roger" }, ]; var view; run(this, function(){ view = this.subject({ content: A(content), height: 300, width: 500, rowHeight: 100, itemViews: { cat: ListItemView.extend({ rowHeight: 100, template: compile("Meow says {{name}} expected: cat === {{type}} {{id}}") }), dog: ListItemView.extend({ rowHeight: 50, template: compile("Woof says {{name}} expected: dog === {{type}} {{id}}") }), other: ListItemView.extend({ rowHeight: 150, template: compile("Potato says {{name}} expected: other === {{type}} {{id}}") }) }, itemViewForIndex: function(idx){ return this.itemViews[get(A(this.get('content')).objectAt(idx), 'type')]; }, heightForIndex: function(idx) { // proto() is a quick hack, lets just store this on the class.. return this.itemViewForIndex(idx).proto().rowHeight; } }); }); this.render(); assert.equal(view.get('totalHeight'), 3350); var positionSorted = sortItemsByPosition(this); assert.equal(findItems(this).length, 4); assert.dom(positionSorted[0]).hasText("Meow says Andrew expected: cat === cat 1"); assert.dom(positionSorted[1]).hasText("Meow says Bruce expected: cat === cat 3"); assert.dom(positionSorted[2]).hasText("Potato says Xbar expected: other === other 4"); assert.dom(positionSorted[3]).hasText("Woof says Caroline expected: dog === dog 5"); assert.deepEqual(itemPositions(view), [ { x:0, y: 0 }, // <-- in view { x:0, y: 100 }, // <-- in view { x:0, y: 200 }, // <-- in view { x:0, y: 350 } // <-- buffer ], 'went beyond scroll max via overscroll'); run(view, 'scrollTo', 1000); positionSorted = sortItemsByPosition(this); assert.dom(positionSorted[0]).hasText("Potato says Xbar expected: other === other 12"); assert.dom(positionSorted[1]).hasText("Woof says Harry expected: dog === dog 13"); assert.dom(positionSorted[2]).hasText("Meow says Ingrid expected: cat === cat 14"); assert.dom(positionSorted[3]).hasText("Potato says Xbar expected: other === other 15"); assert.deepEqual(itemPositions(view), [ { x:0, y: 950 }, // <-- partially in view { x:0, y: 1100 }, // <-- in view { x:0, y: 1150 }, // <-- in view { x:0, y: 1250 } // <-- partially in view ], 'went beyond scroll max via overscroll'); }); skip("handle bindable rowHeight with multi-height (only fallback case)", function(assert) { var content = [ { id: 1, type: "cat", name: "Andrew" }, { id: 3, type: "cat", name: "Bruce" }, { id: 4, type: "other", name: "Xbar" }, { id: 5, type: "dog", name: "Caroline" }, { id: 6, type: "cat", name: "David" }, { id: 7, type: "other", name: "Xbar" }, { id: 8, type: "other", name: "Xbar" }, { id: 9, type: "dog", name: "Edward" }, { id: 10, type: "dog", name: "Francis" }, { id: 11, type: "dog", name: "George" }, { id: 12, type: "other", name: "Xbar" }, { id: 13, type: "dog", name: "Harry" }, { id: 14, type: "cat", name: "Ingrid" }, { id: 15, type: "other", name: "Xbar" }, { id: 16, type: "cat", name: "Jenn" }, { id: 17, type: "cat", name: "Kelly" }, { id: 18, type: "other", name: "Xbar" }, { id: 19, type: "other", name: "Xbar" }, { id: 20, type: "cat", name: "Larry" }, { id: 21, type: "other", name: "Xbar" }, { id: 22, type: "cat", name: "Manny" }, { id: 23, type: "dog", name: "Nathan" }, { id: 24, type: "cat", name: "Ophelia" }, { id: 25, type: "dog", name: "Patrick" }, { id: 26, type: "other", name: "Xbar" }, { id: 27, type: "other", name: "Xbar" }, { id: 28, type: "other", name: "Xbar" }, { id: 29, type: "other", name: "Xbar" }, { id: 30, type: "other", name: "Xbar" }, { id: 31, type: "cat", name: "Quincy" }, { id: 32, type: "dog", name: "Roger" } ]; var view; run(this, function(){ view = this.subject({ content: A(content), height: 300, width: 500, rowHeight: 100, itemViews: { other: ListItemView.extend({ rowHeight: 150, template: compile("Potato says {{name}} expected: other === {{type}} {{id}}") }) }, itemViewForIndex: function(idx){ return this.itemViews[get(A(this.get('content')).objectAt(idx), 'type')] || ReusableListItemView; }, heightForIndex: function(idx) { var view = this.itemViewForIndex(idx); return view.proto().rowHeight || this.get('rowHeight'); } }); }); this.render(); assert.equal(findItems(this).length, 4); assert.equal(view.get('totalHeight'), 3750); // expected // ----- // 0 | // 1 | // 2 | // ----- // 3 | <--- buffer // ----- // 4 | // 5 | // 6 | // 7 | // 8 | // 9 | // 10 | // 11 | // 12 | // 13 | // 14 | // ----- // assert.deepEqual(itemPositions(view), [ { x:0, y: 0 }, // <- visible { x:0, y: 100 }, // <- visible { x:0, y: 200 }, // <- visible { x:0, y: 350 } // <- buffer ] , "inDOM views are correctly positioned: before rowHeight change"); run(view, 'set', 'rowHeight', 200); assert.equal(findItems(this).length, 3); assert.equal(view.get('totalHeight'), 5550); // expected // ----- // 0 | // 1 | // ----| // 2 | <--- buffer // ----| // 3 | // 4 | // 5 | // 6 | // 7 | // 8 | // 9 | // 10 | // 11 | // 12 | // 13 | // 14 | // ----- assert.deepEqual(itemPositions(view), [ { x:0, y: 0 }, // <-- visible { x:0, y: 200 }, // <-- visible { x:0, y: 400 } // <-- buffer ], "inDOM views are correctly positioned: after rowHeight change"); }); }); ================================================ FILE: tests/unit/percentage-layout-test.js ================================================ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render, setupOnerror, resetOnerror } from '@ember/test-helpers'; import { generateContent, sortItemsByPosition, itemPositions } from '../helpers/helpers'; import template from '../templates/percentage'; import { gte } from 'ember-compatibility-helpers'; var itemWidth = 100; var itemHeight = 50; var width = 1000; var height = 400; // Since we are testing percentage based layouts we only want to test the top / left. // The widths are calculated by percentages so then can be difficult to reproduce the browsers rounding function extractTopLeftRounded(items) { return items.map(function(item) { return {top: Math.round(item.top), left: Math.round(item.left)}; }); } module('percentage layout', function(hooks) { setupRenderingTest(hooks); hooks.beforeEach(function() { // eslint-disable-next-line no-console console.debug('Note: Logged console errors are expected in the Asserts when... tests'); }) test("cells have correct width", async function(assert) { let columns = [25, 50, 15, 10]; let content = generateContent(8); this.setProperties({height, width, itemHeight, itemWidth, content, columns}); await render(template); let items = sortItemsByPosition(this.element); let positions = extractTopLeftRounded(itemPositions(this.element)); // test the positioning done by the layout. assert.deepEqual(positions, [ {top: 0, left: 0}, {top: 0, left: 250}, {top: 0, left: 750}, {top: 0, left: 900}, {top: 50, left: 0}, {top: 50, left: 250}, {top: 50, left: 750}, {top: 50, left: 900}, ]); // test that the widths match what was provided in `columns` assert.equal(items[0].style.width, '25%'); assert.equal(items[1].style.width, '50%'); assert.equal(items[2].style.width, '15%'); assert.equal(items[3].style.width, '10%'); assert.equal(items[4].style.width, '25%'); assert.equal(items[5].style.width, '50%'); assert.equal(items[6].style.width, '15%'); assert.equal(items[7].style.width, '10%'); assert.equal(items[0].getBoundingClientRect().height, itemHeight); }); test("columns can use decimals", async function(assert) { let columns = [33.333, 66.666]; let content = generateContent(6); this.setProperties({height, width, itemHeight, itemWidth, content, columns}); await render(template); let items = sortItemsByPosition(this.element); let positions = extractTopLeftRounded(itemPositions(this.element)); // test the positioning done by the layout assert.deepEqual(positions, [ {top: 0, left: 0}, {top: 0, left: 333}, {top: 50, left: 0}, {top: 50, left: 333}, {top: 100, left: 0}, {top: 100, left: 333} ]); // test that the widths match what was provided in `columns` assert.equal(items[0].style.width, '33.333%'); assert.equal(items[1].style.width, '66.666%'); assert.equal(items[2].style.width, '33.333%'); assert.equal(items[3].style.width, '66.666%'); assert.equal(items[0].getBoundingClientRect().height, itemHeight); }); if (gte('2.18.0')) { test("Asserts when columns are larger than 100", async function(assert) { assert.expect(1); let columns = [100, 10]; let content = generateContent(10); try { setupOnerror(() => { assert.ok(true); }); this.setProperties({height, width, itemHeight, itemWidth, content, columns}); await render(template); } finally { resetOnerror(); } }); test("Asserts when columns do not equal 100", async function(assert) { assert.expect(1); let columns = [10, 10]; let content = generateContent(10); try { setupOnerror(() => { assert.ok(true); }); this.setProperties({height, width, itemHeight, itemWidth, content, columns}); await render(template); } finally { resetOnerror(); } }); } else { // TODO: write versions of these tests that work in 2.12 and 2.16 } }); ================================================ FILE: tests/unit/raf-test.js ================================================ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render } from '@ember/test-helpers'; import { generateContent, sortItemsByPosition } from '../helpers/helpers'; import { hbs } from 'ember-cli-htmlbars'; let originalRaf = window.requestAnimationFrame; let template = hbs`{{#if this.showComponent}}
{{item.name}}
{{/if}}`; module('raf', function(hooks) { setupRenderingTest(hooks); hooks.beforeEach(function() { this.setup = function() { window.requestAnimationFrame = undefined; }; this.teardown = function() { window.requestAnimationFrame = originalRaf; }; }); test('works without requestAnimationFrame', async function(assert) { var width = 150, height = 500, itemWidth = 50, itemHeight = 50; var offsetY = 100; var content = generateContent(5); this.setProperties({ width, height, itemWidth, itemHeight, content, offsetY, showComponent: true }); await render(template); var positionSorted = sortItemsByPosition(this.element); assert.dom(positionSorted[0]) .hasTextContaining("Item 1", "We rendered without requestAnimationFrame"); // Force the component to be torn down. this.setProperties({showComponent: false}); }); }); ================================================ FILE: tests/unit/recycling-tests.js ================================================ import { run } from '@ember/runloop'; import Ember from 'ember'; import { setupRenderingTest } from 'ember-qunit'; import '@ember/test-helpers'; import { module, skip } from 'qunit'; import { generateContent, findItems, findVisibleItems } from '../helpers/helpers'; // import { hbs } from 'ember-cli-htmlbars'; // TODO: Remove these declarations. They're just there to keep JSHint happy. let compile, ListItemView, ReusableListItemView; module('View recycling', function(hooks) { setupRenderingTest(hooks); skip("recycling complex views long list", function(assert){ var content = generateContent(100), height = 50, rowHeight = 50, itemViewClass = ListItemView.extend({ innerViewClass: Ember.View.extend({ didInsertElement: function(){ innerViewInsertionCount++; }, willDestroyElement: function(){ innerViewDestroyCount++; } }), template: compile("{{name}} {{#view view.innerViewClass}}{{/view}}") }); var listViewInsertionCount, listViewDestroyCount, innerViewInsertionCount, innerViewDestroyCount; listViewInsertionCount = 0; listViewDestroyCount = 0; innerViewInsertionCount = 0; innerViewDestroyCount = 0; var view; run(this, function(){ view = this.subject({ content: content, height: height, rowHeight: rowHeight, itemViewClass: itemViewClass, scrollTop: 0, didInsertElement: function() { listViewInsertionCount++; }, willDestroyElement: function() { listViewDestroyCount++; } }); }); assert.equal(listViewInsertionCount, 0, "expected number of listView's didInsertElement"); assert.equal(listViewDestroyCount, 0, "expected number of listView's willDestroyElement"); this.render(); assert.equal(listViewInsertionCount, 1, "expected number of listView's didInsertElement"); assert.equal(listViewDestroyCount, 0, "expected number of listView's willDestroyElement"); assert.equal(innerViewInsertionCount, 2, "expected number of innerView's didInsertElement"); assert.equal(innerViewDestroyCount, 0, "expected number of innerView's didInsertElement"); assert.equal(findItems(this).length, 2, "The correct number of rows were rendered"); innerViewInsertionCount = 0; innerViewDestroyCount = 0; run(function() { view.scrollTo(50); }); assert.equal(findItems(this).length, 2, "The correct number of rows were rendered"); assert.equal(innerViewInsertionCount, 1, "expected number of innerView's didInsertElement"); assert.equal(innerViewDestroyCount, 1, "expected number of innerView's willDestroyElement"); assert.equal(listViewInsertionCount, 1, "expected number of listView's didInsertElement"); assert.equal(listViewDestroyCount, 0, "expected number of listView's willDestroyElement"); innerViewInsertionCount = 0; innerViewDestroyCount = 0; run(function() { view.scrollTo(0); }); assert.equal(findItems(this).length, 2, "The correct number of rows were rendered"); assert.equal(innerViewInsertionCount, 1, "expected number of innerView's didInsertElement"); assert.equal(innerViewDestroyCount, 1, "expected number of innerView's willDestroyElement"); assert.equal(listViewInsertionCount, 1, "expected number of listView's didInsertElement"); assert.equal(listViewDestroyCount, 0, "expected number of listView's willDestroyElement"); }); skip("recycling complex views short list", function(assert){ var content = generateContent(2), height = 50, rowHeight = 50, itemViewClass = ListItemView.extend({ innerViewClass: Ember.View.extend({ didInsertElement: function(){ innerViewInsertionCount++; }, willDestroyElement: function(){ innerViewDestroyCount++; } }), template: compile("{{name}} {{#view view.innerViewClass}}{{/view}}") }); var listViewInsertionCount, listViewDestroyCount, innerViewInsertionCount, innerViewDestroyCount; listViewInsertionCount = 0; listViewDestroyCount = 0; innerViewInsertionCount = 0; innerViewDestroyCount = 0; var view; run(this, function(){ view = this.subject({ content: content, height: height, rowHeight: rowHeight, itemViewClass: itemViewClass, scrollTop: 0, didInsertElement: function() { listViewInsertionCount++; }, willDestroyElement: function() { listViewDestroyCount++; } }); }); assert.equal(listViewInsertionCount, 0, "expected number of listView's didInsertElement (pre-append)"); assert.equal(listViewDestroyCount, 0, "expected number of listView's willDestroyElement (pre-append)"); this.render(); assert.equal(listViewInsertionCount, 1, "expected number of listView's didInsertElement (post-append)"); assert.equal(listViewDestroyCount, 0, "expected number of listView's willDestroyElement (post-append)"); assert.equal(innerViewInsertionCount, 2, "expected number of innerView's didInsertElement (post-append)"); assert.equal(innerViewDestroyCount, 0, "expected number of innerView's didInsertElement (post-append)"); assert.equal(findItems(this).length, 2, "The correct number of rows were rendered"); innerViewInsertionCount = 0; innerViewDestroyCount = 0; view.scrollTo(50); assert.equal(findItems(this).length, 2, "The correct number of rows were rendered (post-scroll to 50)"); assert.equal(innerViewInsertionCount, 0, "expected number of innerView's didInsertElement (post-scroll to 50)"); assert.equal(innerViewDestroyCount, 0, "expected number of innerView's willDestroyElement (post-scroll to 50)"); assert.equal(listViewInsertionCount, 1, "expected number of listView's didInsertElement (post-scroll to 50)"); assert.equal(listViewDestroyCount, 0, "expected number of listView's willDestroyElement (post-scroll to 50)"); innerViewInsertionCount = 0; innerViewDestroyCount = 0; view.scrollTo(0); assert.equal(findItems(this).length, 2, "The correct number of rows were rendered (post-scroll to 0)"); assert.equal(innerViewInsertionCount, 0, "expected number of innerView's didInsertElement (post-scroll to 0)"); assert.equal(innerViewDestroyCount, 0, "expected number of innerView's willDestroyElement (post-scroll to 0)"); assert.equal(listViewInsertionCount, 1, "expected number of listView's didInsertElement (post-scroll to 0)"); assert.equal(listViewDestroyCount, 0, "expected number of listView's willDestroyElement (post-scroll to 0)"); }); skip("recycling complex views long list, with ReusableListItemView", function(assert){ var content = generateContent(50), height = 50, rowHeight = 50, itemViewClass = Ember.ReusableListItemView.extend({ innerViewClass: Ember.View.extend({ didInsertElement: function(){ innerViewInsertionCount++; }, willDestroyElement: function(){ innerViewDestroyCount++; } }), didInsertElement: function(){ this._super(); listItemViewInsertionCount++; }, willDestroyElement: function(){ this._super(); listItemViewDestroyCount++; }, template: compile("{{name}} {{#view view.innerViewClass}}{{/view}}") }); var listViewInsertionCount, listViewDestroyCount, listItemViewInsertionCount, listItemViewDestroyCount, innerViewInsertionCount, innerViewDestroyCount; listViewInsertionCount = 0; listViewDestroyCount = 0; listItemViewInsertionCount = 0; listItemViewDestroyCount = 0; innerViewInsertionCount = 0; innerViewDestroyCount = 0; var view; run(this, function(){ view = this.subject({ content: content, height: height, rowHeight: rowHeight, itemViewClass: itemViewClass, scrollTop: 0, didInsertElement: function() { listViewInsertionCount++; }, willDestroyElement: function() { listViewDestroyCount++; } }); }); assert.equal(listViewInsertionCount, 0, "expected number of listView's didInsertElement (pre-append)"); assert.equal(listViewDestroyCount, 0, "expected number of listView's willDestroyElement (pre-append)"); assert.equal(listItemViewInsertionCount, 0, "expected number of listItemView's didInsertElement (pre-append)"); assert.equal(listItemViewDestroyCount, 0, "expected number of listItemView's willDestroyElement (pre-append)"); assert.equal(innerViewInsertionCount, 0, "expected number of innerView's didInsertElement (pre-append)"); assert.equal(innerViewDestroyCount, 0, "expected number of innerView's willDestroyElement (pre-append)"); this.render(); assert.equal(listViewInsertionCount, 1, "expected number of listView's didInsertElement (post-append)"); assert.equal(listViewDestroyCount, 0, "expected number of listView's willDestroyElement (post-append)"); assert.equal(listItemViewInsertionCount, 2, "expected number of listItemView's didInsertElement (post-append)"); assert.equal(listItemViewDestroyCount, 0, "expected number of listItemView's didInsertElement (post-append)"); assert.equal(innerViewInsertionCount, 2, "expected number of innerView's didInsertElement (post-append)"); assert.equal(innerViewDestroyCount, 0, "expected number of innerView's didInsertElement (post-append)"); assert.equal(findItems(this).length, 2, "The correct number of rows were rendered"); listItemViewInsertionCount = 0; listItemViewDestroyCount = 0; innerViewInsertionCount = 0; innerViewDestroyCount = 0; view.scrollTo(50); assert.equal(findItems(this).length, 2, "The correct number of rows were rendered (post-scroll to 50)"); assert.equal(listItemViewInsertionCount, 0, "expected number of listItemView's didInsertElement (post-scroll to 50)"); assert.equal(listItemViewDestroyCount, 0, "expected number of listItemView's willDestroyElement (post-scroll to 50)"); assert.equal(innerViewInsertionCount, 0, "expected number of innerView's didInsertElement (post-scroll to 50)"); assert.equal(innerViewDestroyCount, 0, "expected number of innerView's willDestroyElement (post-scroll to 50)"); listItemViewInsertionCount = 0; listItemViewDestroyCount = 0; innerViewInsertionCount = 0; innerViewDestroyCount = 0; view.scrollTo(0); assert.equal(findItems(this).length, 2, "The correct number of rows were rendered (post-scroll to 0)"); assert.equal(listItemViewInsertionCount, 0, "expected number of listItemView's didInsertElement (post-scroll to 0)"); assert.equal(listItemViewDestroyCount, 0, "expected number of listItemView's willDestroyElement (post-scroll to 0)"); assert.equal(innerViewInsertionCount, 0, "expected number of innerView's didInsertElement (post-scroll to 0)"); assert.equal(innerViewDestroyCount, 0, "expected number of innerView's willDestroyElement (post-scroll to 0)"); }); skip("recycling complex views short list, with ReusableListItemView", function(assert){ var content = generateContent(2), height = 50, rowHeight = 50, itemViewClass = ReusableListItemView.extend({ innerViewClass: Ember.View.extend({ didInsertElement: function(){ innerViewInsertionCount++; }, willDestroyElement: function(){ innerViewDestroyCount++; } }), didInsertElement: function(){ this._super(); listItemViewInsertionCount++; }, willDestroyElement: function(){ this._super(); listItemViewDestroyCount++; }, template: compile("{{name}} {{#view view.innerViewClass}}{{/view}}") }); var listViewInsertionCount, listViewDestroyCount, listItemViewInsertionCount, listItemViewDestroyCount, innerViewInsertionCount, innerViewDestroyCount; listViewInsertionCount = 0; listViewDestroyCount = 0; listItemViewInsertionCount = 0; listItemViewDestroyCount = 0; innerViewInsertionCount = 0; innerViewDestroyCount = 0; var view; run(this, function(){ view = this.subject({ content: content, height: height, rowHeight: rowHeight, itemViewClass: itemViewClass, scrollTop: 0, didInsertElement: function() { listViewInsertionCount++; }, willDestroyElement: function() { listViewDestroyCount++; } }); }); assert.equal(listViewInsertionCount, 0, "expected number of listView's didInsertElement (pre-append)"); assert.equal(listViewDestroyCount, 0, "expected number of listView's willDestroyElement (pre-append)"); assert.equal(listItemViewInsertionCount, 0, "expected number of listItemView's didInsertElement (pre-append)"); assert.equal(listItemViewDestroyCount, 0, "expected number of listItemView's willDestroyElement (pre-append)"); assert.equal(innerViewInsertionCount, 0, "expected number of innerView's didInsertElement (pre-append)"); assert.equal(innerViewDestroyCount, 0, "expected number of innerView's willDestroyElement (pre-append)"); this.render(); assert.equal(listViewInsertionCount, 1, "expected number of listView's didInsertElement (post-append)"); assert.equal(listViewDestroyCount, 0, "expected number of listView's willDestroyElement (post-append)"); assert.equal(listItemViewInsertionCount, 2, "expected number of listItemView's didInsertElement (post-append)"); assert.equal(listItemViewDestroyCount, 0, "expected number of listItemView's didInsertElement (post-append)"); assert.equal(innerViewInsertionCount, 2, "expected number of innerView's didInsertElement (post-append)"); assert.equal(innerViewDestroyCount, 0, "expected number of innerView's didInsertElement (post-append)"); assert.equal(findItems(this).length, 2, "The correct number of rows were rendered"); listItemViewInsertionCount = 0; listItemViewDestroyCount = 0; innerViewInsertionCount = 0; innerViewDestroyCount = 0; view.scrollTo(50); assert.equal(findItems(this).length, 2, "The correct number of rows were rendered (post-scroll to 50)"); assert.equal(listItemViewInsertionCount, 0, "expected number of listItemView's didInsertElement (post-scroll to 50)"); assert.equal(listItemViewDestroyCount, 0, "expected number of listItemView's willDestroyElement (post-scroll to 50)"); assert.equal(innerViewInsertionCount, 0, "expected number of innerView's didInsertElement (post-scroll to 50)"); assert.equal(innerViewDestroyCount, 0, "expected number of innerView's willDestroyElement (post-scroll to 50)"); listItemViewInsertionCount = 0; listItemViewDestroyCount = 0; innerViewInsertionCount = 0; innerViewDestroyCount = 0; view.scrollTo(0); assert.equal(findItems(this).length, 2, "The correct number of rows were rendered (post-scroll to 0)"); assert.equal(listItemViewInsertionCount, 0, "expected number of listItemView's didInsertElement (post-scroll to 0)"); assert.equal(listItemViewDestroyCount, 0, "expected number of listItemView's willDestroyElement (post-scroll to 0)"); assert.equal(innerViewInsertionCount, 0, "expected number of innerView's didInsertElement (post-scroll to 0)"); assert.equal(innerViewDestroyCount, 0, "expected number of innerView's willDestroyElement (post-scroll to 0)"); }); skip("recycling complex views with ReusableListItemView, handling empty slots at the end of the grid", function(assert){ var content = generateContent(20), height = 150, rowHeight = 50, width = 100, elementWidth = 50, itemViewClass = ReusableListItemView.extend({ innerViewClass: Ember.View.extend({ didInsertElement: function(){ innerViewInsertionCount++; }, willDestroyElement: function(){ innerViewDestroyCount++; } }), didInsertElement: function(){ this._super(); listItemViewInsertionCount++; }, willDestroyElement: function(){ this._super(); listItemViewDestroyCount++; }, template: compile("{{name}} {{#view view.innerViewClass}}{{/view}}") }); var listViewInsertionCount, listViewDestroyCount, listItemViewInsertionCount, listItemViewDestroyCount, innerViewInsertionCount, innerViewDestroyCount; listViewInsertionCount = 0; listViewDestroyCount = 0; listItemViewInsertionCount = 0; listItemViewDestroyCount = 0; innerViewInsertionCount = 0; innerViewDestroyCount = 0; var view; run(this, function(){ view = this.subject({ content: content, height: height, rowHeight: rowHeight, width: width, elementWidth: elementWidth, itemViewClass: itemViewClass, scrollTop: 0, didInsertElement: function() { listViewInsertionCount++; }, willDestroyElement: function() { listViewDestroyCount++; } }); }); assert.equal(listViewInsertionCount, 0, "expected number of listView's didInsertElement (pre-append)"); assert.equal(listViewDestroyCount, 0, "expected number of listView's willDestroyElement (pre-append)"); assert.equal(listItemViewInsertionCount, 0, "expected number of listItemView's didInsertElement (pre-append)"); assert.equal(listItemViewDestroyCount, 0, "expected number of listItemView's willDestroyElement (pre-append)"); assert.equal(innerViewInsertionCount, 0, "expected number of innerView's didInsertElement (pre-append)"); assert.equal(innerViewDestroyCount, 0, "expected number of innerView's willDestroyElement (pre-append)"); this.render(); assert.equal(listViewInsertionCount, 1, "expected number of listView's didInsertElement (post-append)"); assert.equal(listViewDestroyCount, 0, "expected number of listView's willDestroyElement (post-append)"); assert.equal(listItemViewInsertionCount, 8, "expected number of listItemView's didInsertElement (post-append)"); assert.equal(listItemViewDestroyCount, 0, "expected number of listItemView's didInsertElement (post-append)"); assert.equal(innerViewInsertionCount, 8, "expected number of innerView's didInsertElement (post-append)"); assert.equal(innerViewDestroyCount, 0, "expected number of innerView's didInsertElement (post-append)"); assert.equal(findItems(this).length, 8, "The correct number of items were rendered (post-append)"); assert.equal(findVisibleItems(this).length, 8, "The number of items that are not hidden with display:none (post-append)"); listItemViewInsertionCount = 0; listItemViewDestroyCount = 0; innerViewInsertionCount = 0; innerViewDestroyCount = 0; view.scrollTo(350); assert.equal(findItems(this).length, 8, "The correct number of items were rendered (post-scroll to 350)"); assert.equal(findVisibleItems(this).length, 8, "The number of items that are not hidden with display:none (post-scroll to 350)"); assert.equal(listItemViewInsertionCount, 0, "expected number of listItemView's didInsertElement (post-scroll to 350)"); assert.equal(listItemViewDestroyCount, 0, "expected number of listItemView's willDestroyElement (post-scroll to 350)"); assert.equal(innerViewInsertionCount, 0, "expected number of innerView's didInsertElement (post-scroll to 350)"); assert.equal(innerViewDestroyCount, 0, "expected number of innerView's willDestroyElement (post-scroll to 350)"); listItemViewInsertionCount = 0; listItemViewDestroyCount = 0; innerViewInsertionCount = 0; innerViewDestroyCount = 0; run(function() { view.set('width', 150); }); assert.equal(findItems(this).length, 12, "The correct number of items were rendered (post-expand to 3 columns)"); assert.equal(listItemViewInsertionCount, 4, "expected number of listItemView's didInsertElement (post-expand to 3 columns)"); assert.equal(listItemViewDestroyCount, 0, "expected number of listItemView's willDestroyElement (post-expand to 3 columns)"); assert.equal(innerViewInsertionCount, 4, "expected number of innerView's didInsertElement (post-expand to 3 columns)"); assert.equal(innerViewDestroyCount, 0, "expected number of innerView's willDestroyElement (post-expand to 3 columns)"); assert.equal(findVisibleItems(this).length, 12, "The number of items that are not hidden with display:none (post-expand to 3 columns)"); }); }); ================================================ FILE: tests/unit/scroll-top-test.js ================================================ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render, settled } from '@ember/test-helpers'; import { findScrollable, generateContent, sortItemsByPosition, checkContent } from '../helpers/helpers'; import template from '../templates/fixed-grid'; var raf = window.requestAnimationFrame; if (raf === undefined) { raf = function(callback) { setTimeout(callback, 16); }; } var size; // lifted from antiscroll MIT license function scrollbarSize() { if (size === undefined) { let div = document.createElement('div'); div.classList.add('antiscroll-inner'); div.style = 'width:50px;height:50px;overflow-y:scroll;position:absolute;top:-200px;left:-200px;'; div.innerHTML = '
'; document.body.appendChild(div); var w1 = div.offsetWidth; var w2 = div.querySelector('div').offsetWidth; div.remove(); size = w1 - w2; } return size; } function resolveAfterRaf() { return new Promise(resolve => raf(resolve)); } var content = generateContent(5); module('scrollTop', function(hooks) { setupRenderingTest(hooks); test("base case", async function(assert) { var width = 100, height = 500, itemWidth = 50, itemHeight = 50; var offsetY = 0; this.setProperties({ width, height, itemWidth, itemHeight, content, offsetY }); await render(template); assert.equal(findScrollable(this.element).scrollTop, 0); var positionSorted = sortItemsByPosition(this.element); assert.dom(positionSorted[0]) .hasTextContaining("Item 1", "The first item has not been hidden"); this.set('width', 150); await settled(); assert.equal(findScrollable(this.element).scrollTop, 0); checkContent(this, assert, 0, 5); }); test("scroll but within content length", async function(assert){ var width = 100+scrollbarSize(), height = 100, itemWidth = 50, itemHeight = 50; var offsetY = 100; this.setProperties({ width, height, itemWidth, itemHeight, content, offsetY }); await render(template); await resolveAfterRaf(); assert.equal(Math.round(findScrollable(this.element).scrollTop), 50, 'Scrolled one row.'); this.set('width', 150+scrollbarSize()); await resolveAfterRaf(); assert.equal(findScrollable(this.element).scrollTop, 0, 'No scroll with wider list.'); var positionSorted = sortItemsByPosition(this.element); assert.dom(positionSorted[0]) .hasTextContaining("Item 1", "The first item is not visible but in buffer."); checkContent(this, assert, 0, 5); }); test("scroll within content length, beyond buffer", async function(assert){ var width = 100+scrollbarSize(), height = 100, itemWidth = 50, itemHeight = 50; var offsetY = 0; this.setProperties({ width, height, itemWidth, itemHeight, offsetY, buffer: 0, content: generateContent(10) }); await render(template); let positionSorted = sortItemsByPosition(this.element); assert.dom(positionSorted[0]) .hasTextContaining("Item 1", "The first cell should be the first item."); findScrollable(this.element).scrollTop = 151; await resolveAfterRaf(); assert.equal(Math.round(findScrollable(this.element).scrollTop), 150, 'scrolled to item 7'); positionSorted = sortItemsByPosition(this.element, true); assert.dom(positionSorted[0]) .hasTextContaining("Item 7", "The items before what is on screen is not visible."); this.set('width', 200+scrollbarSize()); await resolveAfterRaf(); assert.equal(Math.round(findScrollable(this.element).scrollTop), 50, 'Scrolled down one row.'); positionSorted = sortItemsByPosition(this.element, true); assert.dom(positionSorted[0]) .hasTextContaining("Item 5", "The fifth item is first rendered."); checkContent(this, assert, 4, 5); }); test("scroll but beyond content length", async function(assert) { var width = 100+scrollbarSize(), height = 500, itemWidth = 50, itemHeight = 50; var offsetY = 1000; this.setProperties({ width, height, itemWidth, itemHeight, content, offsetY }); await render(template); assert.equal(findScrollable(this.element).scrollTop, 0); this.set('width', 150+scrollbarSize()); await settled(); assert.equal(findScrollable(this.element).scrollTop, 0); checkContent(this, assert, 0, 5); }); }); ================================================ FILE: tests/unit/starting-index-test.js ================================================ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render } from '@ember/test-helpers'; import { generateContent } from '../helpers/helpers'; import template from '../templates/fixed-grid'; module('startingIndex', function(hooks) { setupRenderingTest(hooks); test("base case", async function(assert) { var width = 100, height = 500, itemWidth = 50, itemHeight = 50; var content = generateContent(5); this.setProperties({ width, height, itemWidth, itemHeight, content }); await render(template); assert.equal(this.get('startingIndex', 0)); }); test("scroll but within content length", async function(assert) { var width = 100, height = 500, itemWidth = 50, itemHeight = 50; var content = generateContent(5); var offsetY = 100; this.setProperties({ width, height, itemWidth, itemHeight, content, offsetY }); await render(template); assert.equal(this.get('startingIndex', 0)); }); test("scroll but beyond content length", async function(assert) { var width = 100, height = 500, itemWidth = 50, itemHeight = 50; var content = generateContent(20); var offsetY = 100; this.setProperties({ width, height, itemWidth, itemHeight, content, offsetY }); await render(template); assert.equal(this.get('startingIndex', 0)); }); test("larger list", async function(assert) { var width = 100, height = 500, itemWidth = 50, itemHeight = 50; var content = generateContent(50); var offsetY = 100; this.setProperties({ width, height, itemWidth, itemHeight, content, offsetY }); await render(template); assert.equal(this.get('startingIndex', 28)); }); test("larger list (2)", async function(assert) { var width = 100, height = 200, itemWidth = 50, itemHeight = 100; var content = generateContent(50); var offsetY = 100; this.setProperties({ width, height, itemWidth, itemHeight, content, offsetY }); await render(template); assert.equal(this.get('startingIndex', 1)); }); }); ================================================ FILE: tests/unit/total-height-test.js ================================================ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render } from '@ember/test-helpers'; import { generateContent, findContainer } from '../helpers/helpers'; import template from '../templates/fixed-grid'; module('totalHeight', function(hooks) { setupRenderingTest(hooks); test("single column", async function(assert) { var width = 50, height = 500, itemHeight = 50, itemWidth = 50; var content = generateContent(20); this.setProperties({ width, height, itemWidth, itemHeight, content }); await render(template); assert.equal(findContainer(this.element).getBoundingClientRect().height, 1000); }); test("even", async function(assert) { var width = 120, height = 500, itemHeight = 50, itemWidth = 50; var content = generateContent(20); this.setProperties({ width, height, itemWidth, itemHeight, content }); await render(template); assert.equal(findContainer(this.element).getBoundingClientRect().height, 500); }); test("odd", async function(assert) { var width = 120, height = 500, itemHeight = 50, itemWidth = 50; var content = generateContent(21); this.setProperties({ width, height, itemWidth, itemHeight, content }); await render(template); assert.equal(findContainer(this.element).getBoundingClientRect().height, 550); }); });