├── .codeclimate.yml ├── .editorconfig ├── .ember-cli ├── .eslintignore ├── .eslintrc.js ├── .github ├── CODEOWNERS └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── .scss-lint.yml ├── .travis.yml ├── .watchmanconfig ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.md ├── Makefile ├── README.md ├── addon ├── .gitkeep ├── classes │ └── scrollable.js ├── components │ ├── ember-scrollable.js │ ├── ember-scrollbar.js │ └── scroll-content-element.js ├── services │ └── scrollbar-thickness.js ├── styles │ └── addon.css ├── templates │ └── components │ │ ├── ember-scrollable.hbs │ │ ├── ember-scrollbar.hbs │ │ └── scroll-content-element.hbs └── util │ ├── css.js │ ├── measurements.js │ ├── number.js │ └── timeout.js ├── app ├── .gitkeep ├── components │ ├── as-scrollable.js │ ├── ember-scrollable.js │ ├── ember-scrollbar.js │ └── scroll-content-element.js └── services │ └── scrollbar-thickness.js ├── bin ├── ci ├── lint └── setup ├── circle.yml ├── config ├── changelog.js ├── ember-try.js ├── environment.js └── release.js ├── ember-cli-build.js ├── index.js ├── package.json ├── testem.js ├── tests ├── acceptance │ └── index-test.js ├── dummy │ ├── app │ │ ├── app.js │ │ ├── components │ │ │ └── .gitkeep │ │ ├── controllers │ │ │ ├── .gitkeep │ │ │ └── index.js │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── index.html │ │ ├── models │ │ │ └── .gitkeep │ │ ├── resolver.js │ │ ├── router.js │ │ ├── routes │ │ │ └── .gitkeep │ │ ├── styles │ │ │ └── app.scss │ │ └── templates │ │ │ ├── application.hbs │ │ │ ├── components │ │ │ └── .gitkeep │ │ │ ├── cookbook.hbs │ │ │ └── index.hbs │ ├── config │ │ ├── environment.js │ │ ├── optional-features.json │ │ └── targets.js │ └── public │ │ └── robots.txt ├── helpers │ └── .gitkeep ├── index.html ├── integration │ └── components │ │ ├── ember-scrollbar-test.js │ │ └── scroll-content-element-test.js ├── test-helper.js └── unit │ ├── .gitkeep │ └── components │ └── ember-scrollable-test.js ├── vendor └── .gitkeep └── yarn.lock /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | exclude_paths: 2 | - "tests/*" 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | 12 | # misc 13 | /coverage/ 14 | 15 | # ember-try 16 | /.node_modules.ember-try/ 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | ecmaVersion: 2017, 5 | sourceType: 'module' 6 | }, 7 | plugins: [ 8 | 'ember' 9 | ], 10 | extends: [ 11 | 'eslint:recommended', 12 | 'plugin:ember/recommended' 13 | ], 14 | env: { 15 | browser: true 16 | }, 17 | rules: { 18 | }, 19 | overrides: [ 20 | // node files 21 | { 22 | files: [ 23 | 'ember-cli-build.js', 24 | 'index.js', 25 | 'testem.js', 26 | 'blueprints/*/index.js', 27 | 'config/**/*.js', 28 | 'tests/dummy/config/**/*.js' 29 | ], 30 | excludedFiles: [ 31 | 'addon/**', 32 | 'addon-test-support/**', 33 | 'app/**', 34 | 'tests/dummy/app/**' 35 | ], 36 | parserOptions: { 37 | sourceType: 'script', 38 | ecmaVersion: 2015 39 | }, 40 | env: { 41 | browser: false, 42 | node: true 43 | }, 44 | plugins: ['node'], 45 | rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, { 46 | // add your custom rules and overrides for node files here 47 | "node/no-unpublished-require": ["error", { 48 | "allowModules": ["ember-cli-changelog"] 49 | }] 50 | }) 51 | } 52 | ] 53 | }; 54 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Code owners file for repository 2 | # See: https://docs.github.com/en/repositories/managing-your-code/about-code-owners 3 | 4 | # Default owners for everything 5 | * @alphasights/authorized-approvers 6 | 7 | # Add specific ownership rules below 8 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | name: Build, Lint and Test 6 | runs-on: ubuntu-latest 7 | 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@master 11 | 12 | - name: Set Node.js 8.x 13 | uses: actions/setup-node@master 14 | with: 15 | node-version: 8.x 16 | 17 | - name: Install, lint:hbs, lint:js, tests 18 | run: | 19 | npm install 20 | npm run lint:js 21 | npm test 22 | 23 | additional: 24 | name: Ember additional ${{matrix.ember-release}} 25 | runs-on: ubuntu-latest 26 | 27 | strategy: 28 | matrix: 29 | ember-release: 30 | - ember-lts-2.18 31 | - ember-lts-3.4 32 | - ember-lts-3.8 33 | - ember-lts-3.12 34 | - ember-release 35 | - ember-beta 36 | - ember-canary 37 | - ember-default-with-jquery 38 | 39 | steps: 40 | - name: Checkout 41 | uses: actions/checkout@master 42 | 43 | - name: Set Node.js 10.x 44 | uses: actions/setup-node@master 45 | with: 46 | node-version: 10.x 47 | 48 | - name: Install, lint:js, tests 49 | continue-on-error: true 50 | run: | 51 | npm install 52 | node_modules/.bin/ember try:one ${{matrix.ember-release}} 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/ 15 | /libpeerconnection.log 16 | /npm-debug.log* 17 | /testem.log 18 | /yarn-error.log 19 | jsconfig.json 20 | 21 | # ember-try 22 | /.node_modules.ember-try/ 23 | /bower.json.ember-try 24 | /package.json.ember-try 25 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /bower_components 2 | /config/ember-try.js 3 | /dist 4 | /tests 5 | /tmp 6 | **/.gitkeep 7 | .bowerrc 8 | .editorconfig 9 | .ember-cli 10 | .eslintrc.js 11 | .gitignore 12 | .watchmanconfig 13 | .travis.yml 14 | bower.json 15 | ember-cli-build.js 16 | testem.js 17 | yarn.lock 18 | 19 | # ember-try 20 | .node_modules.ember-try/ 21 | bower.json.ember-try 22 | package.json.ember-try 23 | -------------------------------------------------------------------------------- /.scss-lint.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | BangFormat: 3 | enabled: true 4 | space_before_bang: true 5 | space_after_bang: false 6 | 7 | BorderZero: 8 | enabled: true 9 | 10 | ColorKeyword: 11 | enabled: true 12 | 13 | Comment: 14 | enabled: true 15 | 16 | DebugStatement: 17 | enabled: true 18 | 19 | DeclarationOrder: 20 | enabled: true 21 | 22 | DuplicateProperty: 23 | enabled: true 24 | 25 | ElsePlacement: 26 | enabled: true 27 | style: same_line # or 'new_line' 28 | 29 | EmptyLineBetweenBlocks: 30 | enabled: true 31 | ignore_single_line_blocks: true 32 | 33 | EmptyRule: 34 | enabled: true 35 | 36 | FinalNewline: 37 | enabled: true 38 | present: true 39 | 40 | HexLength: 41 | enabled: false 42 | 43 | HexNotation: 44 | enabled: true 45 | style: lowercase # or 'uppercase' 46 | 47 | HexValidation: 48 | enabled: true 49 | 50 | IdWithExtraneousSelector: 51 | enabled: true 52 | 53 | ImportPath: 54 | enabled: true 55 | leading_underscore: false 56 | filename_extension: false 57 | 58 | Indentation: 59 | enabled: true 60 | character: space # or 'tab' 61 | width: 2 62 | 63 | LeadingZero: 64 | enabled: true 65 | style: exclude_zero # or 'include_zero' 66 | 67 | MergeableSelector: 68 | enabled: true 69 | force_nesting: true 70 | 71 | NameFormat: 72 | enabled: true 73 | convention: hyphenated_lowercase # or 'BEM', or a regex pattern 74 | 75 | NestingDepth: 76 | enabled: false 77 | max_depth: 4 78 | 79 | PlaceholderInExtend: 80 | enabled: false 81 | 82 | PropertySortOrder: 83 | enabled: true 84 | ignore_unspecified: false 85 | 86 | PropertySpelling: 87 | enabled: true 88 | extra_properties: [] 89 | 90 | QualifyingElement: 91 | enabled: true 92 | allow_with_attribute: false 93 | allow_with_class: false 94 | allow_with_id: false 95 | 96 | SelectorDepth: 97 | enabled: false 98 | 99 | SelectorFormat: 100 | enabled: true 101 | convention: hyphenated_lowercase # or 'BEM', or 'snake_case', or 'camel_case', or a regex pattern 102 | 103 | Shorthand: 104 | enabled: true 105 | 106 | SingleLinePerProperty: 107 | enabled: true 108 | allow_single_line_rule_sets: true 109 | 110 | SingleLinePerSelector: 111 | enabled: true 112 | 113 | SpaceAfterComma: 114 | enabled: true 115 | 116 | SpaceAfterPropertyColon: 117 | enabled: true 118 | style: one_space # or 'no_space', or 'at_least_one_space', or 'aligned' 119 | 120 | SpaceAfterPropertyName: 121 | enabled: true 122 | 123 | SpaceBeforeBrace: 124 | enabled: true 125 | style: space 126 | allow_single_line_padding: false 127 | 128 | SpaceBetweenParens: 129 | enabled: true 130 | spaces: 0 131 | 132 | StringQuotes: 133 | enabled: true 134 | style: single_quotes # or double_quotes 135 | 136 | TrailingSemicolon: 137 | enabled: true 138 | 139 | TrailingZero: 140 | enabled: false 141 | 142 | UnnecessaryMantissa: 143 | enabled: true 144 | 145 | UnnecessaryParentReference: 146 | enabled: true 147 | 148 | UrlFormat: 149 | enabled: true 150 | 151 | UrlQuotes: 152 | enabled: true 153 | 154 | VendorPrefixes: 155 | enabled: true 156 | identifier_list: base 157 | include: [] 158 | exclude: [] 159 | 160 | ZeroUnit: 161 | enabled: true 162 | 163 | Compass::*: 164 | enabled: false 165 | 166 | ColorVariable: 167 | enabled: false 168 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | # we recommend testing addons with the same minimum supported node version as Ember CLI 5 | # so that your addon works for all apps 6 | - "10" 7 | 8 | sudo: required 9 | dist: trusty 10 | 11 | addons: 12 | chrome: stable 13 | 14 | cache: 15 | yarn: true 16 | 17 | env: 18 | global: 19 | # See https://git.io/vdao3 for details. 20 | - JOBS=1 21 | matrix: 22 | # we recommend new addons test the current and previous LTS 23 | # as well as latest stable release (bonus points to beta/canary) 24 | - EMBER_TRY_SCENARIO=ember-lts-2.12 25 | - EMBER_TRY_SCENARIO=ember-lts-2.16 26 | - EMBER_TRY_SCENARIO=ember-lts-2.18 27 | - EMBER_TRY_SCENARIO=ember-release 28 | - EMBER_TRY_SCENARIO=ember-beta 29 | - EMBER_TRY_SCENARIO=ember-canary 30 | - EMBER_TRY_SCENARIO=ember-default 31 | 32 | matrix: 33 | fast_finish: true 34 | allow_failures: 35 | - env: EMBER_TRY_SCENARIO=ember-beta 36 | - env: EMBER_TRY_SCENARIO=ember-canary 37 | 38 | before_install: 39 | - curl -o- -L https://yarnpkg.com/install.sh | bash 40 | - export PATH=$HOME/.yarn/bin:$PATH 41 | 42 | install: 43 | - yarn install --no-lockfile --non-interactive 44 | 45 | script: 46 | - npm run lint:js 47 | # Usually, it's ok to finish the test scenario without reverting 48 | # to the addon's original dependency state, skipping "cleanup". 49 | - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO --skip-cleanup 50 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | ## 0.5.0 5 | 6 | ### Pull Requests 7 | 8 | - [#95](https://github.com/alphasights/ember-scrollable/pull/95) Upgrade *by [Gaurav Munjal](https://github.com/Gaurav0)* 9 | 10 | ## 0.4.11 11 | 12 | ### Pull Requests 13 | 14 | - [#90](https://github.com/alphasights/ember-scrollable/pull/90) avoid creating extra closure *by [bekzod](https://github.com/bekzod)* 15 | 16 | ## 0.4.10 17 | 18 | ### Pull Requests 19 | 20 | - [#88](https://github.com/alphasights/ember-scrollable/pull/88) Update Dependencies *by [Jonathan Johnson](https://github.com/jrjohnson)* 21 | 22 | ## 0.4.9 23 | 24 | ### Pull Requests 25 | 26 | - [#83](https://github.com/alphasights/ember-scrollable/pull/83) Move mouse position detection to ember-scrollbar *by [Jan Buschtöns](https://github.com/buschtoens)* 27 | 28 | ## 0.4.8 29 | 30 | ### Pull Requests 31 | 32 | - [#81](https://github.com/alphasights/ember-scrollable/pull/81) Fixng scrollbar jumping around bug *by [Alex Alvarez](https://github.com/alexander-alvarez)* 33 | 34 | ## 0.4.7 35 | 36 | ### Pull Requests 37 | 38 | - [#78](https://github.com/alphasights/ember-scrollable/pull/78) remove all ::scrollbar pseudo selectors *by [Jan Buschtöns](https://github.com/buschtoens)* 39 | 40 | ## 0.4.6 41 | 42 | ### Pull Requests 43 | 44 | - [#74](https://github.com/alphasights/ember-scrollable/pull/74) Fixing the arbitrary delay time in test *by [Alex Alvarez](https://github.com/alexander-alvarez)* 45 | - [#75](https://github.com/alphasights/ember-scrollable/pull/75) Friendly cleanup *by [Alex Alvarez](https://github.com/alexander-alvarez)* 46 | - [#77](https://github.com/alphasights/ember-scrollable/pull/77) Upgrade Ember Lifeline *by [alphasights](https://github.com/alphasights)* 47 | 48 | ## 0.4.5 49 | 50 | ### Pull Requests 51 | 52 | - [#62](https://github.com/alphasights/ember-scrollable/pull/62) Minor fix *by [Pierre-Luc Carmel Biron](https://github.com/plcarmel)* 53 | - [#63](https://github.com/alphasights/ember-scrollable/pull/63) Adding `onScrollToX` and `onScrollToY` callbacks. *by [Alex Alvarez](https://github.com/alexander-alvarez)* 54 | - [#69](https://github.com/alphasights/ember-scrollable/pull/69) Creating Number to polyfill *by [Alex Alvarez](https://github.com/alexander-alvarez)* 55 | - [#65](https://github.com/alphasights/ember-scrollable/pull/65) calculate scrollbar width correctly *by [Jan Buschtöns](https://github.com/buschtoens)* 56 | - [#60](https://github.com/alphasights/ember-scrollable/pull/60) Mouse Events on Window Instead of DOM *by [Alex Alvarez](https://github.com/alexander-alvarez)* 57 | - [#70](https://github.com/alphasights/ember-scrollable/pull/70) Fix a return value when our component has been torn down *by [digabit](https://github.com/digabit)* 58 | 59 | ## 0.4.4 60 | 61 | ### Pull Requests 62 | 63 | - [#58](https://github.com/alphasights/ember-scrollable/pull/58) Fix: Scrollbar visible when content changes to no longer have overflow *by [Alex Alvarez](https://github.com/alexander-alvarez)* 64 | 65 | ## 0.4.3 66 | 67 | ### Pull Requests 68 | 69 | - [#54](https://github.com/alphasights/ember-scrollable/pull/54) Fixing #39: Allowing to draggable elments inside of ember-scrollable *by [alexander-alvarez/fixing](https://github.com/alexander-alvarez/fixing)* 70 | 71 | ## 0.4.2 72 | 73 | ### Pull Requests 74 | 75 | - [#53](https://github.com/alphasights/ember-scrollable/pull/53) [BUGFIX] Schedule action on afterRender due to double render *by [offirgolan](https://github.com/offirgolan)* 76 | 77 | ## 0.4.1 78 | 79 | ### Pull Requests 80 | 81 | - [#44](https://github.com/alphasights/ember-scrollable/pull/44) [BUGFIX] Remove Sass as a dependency *by [offirgolan](https://github.com/offirgolan)* 82 | - [#48](https://github.com/alphasights/ember-scrollable/pull/48) #45: Fixes [on click of the scrollbar, it scrolls back to the top] *by [alexander-alvarez/fix](https://github.com/alexander-alvarez/fix)* 83 | - [#49](https://github.com/alphasights/ember-scrollable/pull/49) Fixes #46: Show Scrollbar on mousemove in component *by [alexander-alvarez/fix](https://github.com/alexander-alvarez/fix)* 84 | 85 | ## 0.4.0 86 | 87 | ### Pull Requests 88 | 89 | - [#29](https://github.com/alphasights/ember-scrollable/pull/29) One Way Data flow & Two Scrollbars *by [Alex Alvarez](https://github.com/alexander-alvarez)* 90 | - [#35](https://github.com/alphasights/ember-scrollable/pull/35) [BUGFIX] Remove forced scrollbar width in firefox *by [Offir Golan](https://github.com/offirgolan)* 91 | - [#40](https://github.com/alphasights/ember-scrollable/pull/40) Move 'ember-lifeline' into dependencies *by [Michelle S](https://github.com/lonelyghost)* 92 | - [#42](https://github.com/alphasights/ember-scrollable/pull/42) [BUGFIX] Remove unused version checker *by [Offir Golan](https://github.com/offirgolan)* 93 | 94 | ## 0.3.4 95 | 96 | ### Pull Requests 97 | 98 | - [#25](https://github.com/alphasights/ember-scrollable/pull/25) scrollTo and onScroll *by [Jan Buschtöns](https://github.com/buschtoens)* 99 | 100 | ## 0.3.3 101 | 102 | ### Pull Requests 103 | 104 | - [#17](https://github.com/alphasights/ember-scrollable/pull/17) Allow addon to be nested at depth of n *by [Offir Golan](https://github.com/offirgolan)* 105 | - [#16](https://github.com/alphasights/ember-scrollable/pull/16) Add scrollTop action which can be triggered from outside *by [crudo1f/feature](https://github.com/crudo1f/feature)* 106 | - [#14](https://github.com/alphasights/ember-scrollable/pull/14) Bail from createScrollbar if isDestroyed. *by [blimmer/bug](https://github.com/blimmer/bug)* 107 | - [#21](https://github.com/alphasights/ember-scrollable/pull/21) Fix horizontal scroll demo *by [Kirill Shaplyko](https://github.com/Baltazore)* 108 | - [#20](https://github.com/alphasights/ember-scrollable/pull/20) Updated to Ember 2.8 *by [Kirill Shaplyko](https://github.com/Baltazore)* 109 | - [#23](https://github.com/alphasights/ember-scrollable/pull/23) Removed hide delay in tests *by [Taras Mankovski](https://github.com/taras)* 110 | 111 | ## 0.3.2 112 | 113 | ## 0.3.1 114 | 115 | ### Pull Requests 116 | 117 | - [#11](https://github.com/alphasights/ember-scrollable/pull/11) Removed duplicate didInsertElement *by [Thijs van den Anker](https://github.com/thijsvdanker)* 118 | - [#7](https://github.com/alphasights/ember-scrollable/pull/7) Check scrollbar height for large collections *by [Kirill Shaplyko](https://github.com/Baltazore)* 119 | - [#12](https://github.com/alphasights/ember-scrollable/pull/12) Loading nested dependencies via vendor *by [alphasights](https://github.com/alphasights)* 120 | 121 | ## 0.3.0 122 | 123 | ### Pull Requests 124 | 125 | - [#10](https://github.com/alphasights/ember-scrollable/pull/10) Refactored the component to make ember-scrollable primary component *by [alphasights](https://github.com/alphasights)* 126 | 127 | ## 0.2.7 128 | 129 | ### Pull Requests 130 | 131 | - [#8](https://github.com/alphasights/ember-scrollable/pull/8) Fix tests *by [alphasights](https://github.com/alphasights)* 132 | - [#9](https://github.com/alphasights/ember-scrollable/pull/9) Observe both horizontal and vertical resize for both scrollbars *by [alphasights](https://github.com/alphasights)* 133 | 134 | ## 0.0.0 135 | 136 | - Hold Your Horses, 137 | - Pack Your Parachutes, 138 | - We're Coming, 139 | - But we haven't released anything yet. 140 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'scss-lint' 4 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | rainbow (2.0.0) 5 | sass (3.4.16) 6 | scss-lint (0.38.0) 7 | rainbow (~> 2.0) 8 | sass (~> 3.4.1) 9 | 10 | PLATFORMS 11 | ruby 12 | 13 | DEPENDENCIES 14 | scss-lint 15 | 16 | BUNDLED WITH 17 | 1.10.5 18 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | demo: 2 | @echo "" 3 | @echo "===> Checking out gh-pages" 4 | @echo "" 5 | git checkout gh-pages 6 | git reset --hard master 7 | @echo "" 8 | @echo "===> Building demo" 9 | @echo "" 10 | ember build -e production -o demo 11 | @echo "" 12 | @echo "===> Committing demo" 13 | @echo "" 14 | git add demo 15 | git commit -m "Build demo" 16 | @echo "" 17 | @echo "===> Pushing gh-pages" 18 | @echo "" 19 | git push origin gh-pages -f 20 | @echo "" 21 | @echo "===> Cleaning up" 22 | @echo "" 23 | git checkout master 24 | rm -rf demo 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ember Scrollable 2 | [![Ember Version](https://embadge.io/v1/badge.svg?start=2.8.0-lts)](https://embadge.io/v1/badge.svg?start=2.8.0-lts) 3 | [![Npm Version](https://badge.fury.io/js/ember-scrollable.svg)](http://badge.fury.io/js/ember-scrollable) 4 | [![Code 5 | Climate](https://codeclimate.com/github/alphasights/ember-scrollable/badges/gpa.svg)](https://codeclimate.com/github/alphasights/ember-scrollable) 6 | [![Build Status](https://travis-ci.org/alphasights/ember-scrollable.svg?branch=master)](https://travis-ci.org/alphasights/ember-scrollable) 7 | 8 | A simple scrollbar implementation inspired by Trackpad Scroll Emulator. 9 | 10 | [Check out the demo](https://alphasights.github.io/ember-scrollable) 11 | 12 | Installation 13 | ------------------------------------------------------------------------------ 14 | 15 | * `ember install ember-scrollable` 16 | 17 | ## Basic Usage 18 | 19 | ```htmlbars 20 | {{! app/templates/index.hbs }} 21 | 22 | {{#ember-scrollable}} 23 | Some long content... 24 | {{/ember-scrollable}} 25 | ``` 26 | 27 | ## Configuring the Component 28 | 29 | The component accepts the following options: 30 | 31 | - `horizontal`: Enables horizontal scrolling (default: `false`) 32 | - `vertical`: Enables vertical scrolling (default: `true` if horizontal is unset) 33 | - `autoHide`: Enables auto hiding of the scrollbars on mouse out (default: `true`) 34 | - `scrollTo`: Set this property to manually scroll to a certain position (if in single bar mode) 35 | - `scrollToX`: Set this property to manually scroll to a certain position in the horizontal direction 36 | - `scrollToY`: Set this property to manually scroll to a certain position in the vertical direction 37 | - `onScroll(scrollOffset, event)`: action triggered whenever the user scrolls, called with the current `scrollOffset` and the original scroll `event` 38 | - `onScrolledToBottom`: action triggered when user scrolled to the bottom 39 | 40 | ## Advanced Usage 41 | 42 | ```htmlbars 43 | {{#ember-scrollable horizontal=true vertical=true}} 44 | content that is wide and long. 45 | {{/ember-scrollable}} 46 | ``` 47 | 48 | ## Developing 49 | 50 | ### Setup 51 | 52 | * `git clone https://github.com/alphasights/ember-scrollable.git` 53 | * `npm install && bower install` 54 | 55 | ### Running 56 | 57 | * `ember server` 58 | 59 | ### Linting 60 | 61 | * `npm run lint:js` 62 | * `npm run lint:js -- --fix` 63 | 64 | ### Running tests 65 | 66 | * `ember test` – Runs the test suite on the current Ember version 67 | * `ember test --server` – Runs the test suite in "watch mode" 68 | * `ember try:each` – Runs the test suite against multiple Ember versions 69 | 70 | ### Running the dummy application 71 | 72 | * `ember serve` 73 | * Visit the dummy application at [http://localhost:4200](http://localhost:4200). 74 | 75 | For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). 76 | 77 | License 78 | ------------------------------------------------------------------------------ 79 | 80 | This project is licensed under the [MIT License](LICENSE.md). 81 | -------------------------------------------------------------------------------- /addon/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphasights/ember-scrollable/3ecf8a7943a72b65fc7c49e5d96ba5494ebab536/addon/.gitkeep -------------------------------------------------------------------------------- /addon/classes/scrollable.js: -------------------------------------------------------------------------------- 1 | import { capitalize } from '@ember/string'; 2 | import { getHeight, getWidth } from '../util/measurements'; 3 | 4 | const DynamicMethods = { getHeight, getWidth }; 5 | 6 | export default class Scrollable { 7 | constructor(options) { 8 | this.scrollbarElement = options.scrollbarElement; 9 | this.contentElement = options.contentElement; 10 | } 11 | 12 | get isNecessary() { 13 | return this.scrollbarSize() < this.contentOuterSize(); 14 | } 15 | 16 | 17 | scrollbarSize() { 18 | return this.scrollbarElement[`client${capitalize(this.sizeAttr)}`]; 19 | } 20 | 21 | contentOuterSize() { 22 | return DynamicMethods[`get${capitalize(this.sizeAttr)}`](this.contentElement); 23 | } 24 | 25 | getHandlePositionAndSize(scrollOffset) { 26 | // we own this data 27 | let contentSize = this.contentOuterSize(); 28 | // we pass this to the child 29 | let scrollbarSize = this.scrollbarSize(); 30 | let scrollbarRatio = scrollbarSize / contentSize; 31 | 32 | // Calculate new height/position of drag handle. 33 | // Offset of 2px allows for a small top/bottom or left/right margin around handle. 34 | let handleOffset = Math.round(scrollbarRatio * scrollOffset) + 2; 35 | 36 | let handleSize = 0; 37 | 38 | // check if content is scrollbar is longer than content 39 | if (scrollbarRatio < 1) { 40 | let handleSizeCalculated = Math.floor(scrollbarRatio * (scrollbarSize - 2)) - 2; 41 | // check if handleSize is too small 42 | handleSize = handleSizeCalculated < 10 ? 10 : handleSizeCalculated; 43 | } 44 | 45 | return {handleOffset, handleSize}; 46 | } 47 | 48 | isScrolledToBottom(scrollBuffer, scrollOffset) { 49 | let contentSize = this.contentOuterSize(); 50 | let scrollbarSize = this.scrollbarSize(); 51 | 52 | return scrollOffset + scrollbarSize + scrollBuffer >= contentSize; 53 | } 54 | 55 | } 56 | 57 | export class Vertical extends Scrollable { 58 | constructor(options) { 59 | super(options); 60 | 61 | this.offsetAttr = 'top'; 62 | this.sizeAttr = 'height'; 63 | } 64 | } 65 | 66 | export class Horizontal extends Scrollable { 67 | constructor(options) { 68 | super(options); 69 | 70 | this.offsetAttr = 'left'; 71 | this.sizeAttr = 'width'; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /addon/components/ember-scrollable.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { computed } from '@ember/object'; 3 | import { deprecate } from '@ember/application/deprecations'; 4 | import { isPresent } from '@ember/utils'; 5 | import { inject as service } from '@ember/service'; 6 | import { bind, scheduleOnce, debounce, throttle } from '@ember/runloop'; 7 | import Component from '@ember/component'; 8 | import InboundActionsMixin from 'ember-component-inbound-actions/inbound-actions'; 9 | import DomMixin from 'ember-lifeline/mixins/dom'; 10 | import layout from '../templates/components/ember-scrollable'; 11 | import { Horizontal, Vertical } from '../classes/scrollable'; 12 | import { getHeight, getWidth } from '../util/measurements'; 13 | 14 | const hideDelay = Ember.testing ? 16 : 1000; 15 | const PAGE_JUMP_MULTIPLE = 7 / 8; 16 | 17 | export const THROTTLE_TIME_LESS_THAN_60_FPS_IN_MS = 1; // 60 fps -> 1 sec / 60 = 16ms 18 | 19 | const scrollbarSelector = '.tse-scrollbar'; 20 | const contentSelector = '.tse-content'; 21 | 22 | export default Component.extend(InboundActionsMixin, DomMixin, { 23 | layout, 24 | classNameBindings: [':ember-scrollable', ':tse-scrollable', 'horizontal', 'vertical'], 25 | 26 | /** 27 | * If true, a scrollbar will be shown horizontally 28 | * 29 | * @property horizontal 30 | * @public 31 | * @type Boolean 32 | * @default false 33 | */ 34 | horizontal: null, 35 | 36 | /** 37 | * If true, a scrollbar will be shown vertically 38 | * 39 | * @property vertical 40 | * @public 41 | * @type Boolean 42 | */ 43 | vertical: null, 44 | /** 45 | * Indicates whether the scrollbar should auto hide after a given period of time (see hideDelay), 46 | * or remain persitent alongside the content to be scrolled. 47 | * 48 | * @property autoHide 49 | * @public 50 | * @type Boolean 51 | * @default true 52 | */ 53 | autoHide: true, 54 | scrollBuffer: 50, 55 | /** 56 | * Number indicating offset from anchor point (top for vertical, left for horizontal) where the scroll handle 57 | * should be rendered. 58 | * 59 | * @property scrollTo 60 | * @public 61 | * @type Number 62 | */ 63 | scrollTo: computed('vertical', { 64 | get() { 65 | return this.get('vertical') ? this.get('scrollToY') : this.get('scrollToX'); 66 | }, 67 | set(key, value) { 68 | // TODO this is deprecated. remove eventually. 69 | deprecate('Using the `scrollTo` property directly has been deprecated, please prefer being explicit by using `scrollToX` and `scrollToY`.'); 70 | const prop = this.get('vertical') ? 'scrollToY' : 'scrollToX'; 71 | this.set(prop, value); 72 | return value; 73 | } 74 | }), 75 | 76 | /** 77 | * Position in pixels for which to scroll horizontal scrollbar. 78 | * 79 | * @property scrollToX 80 | * @public 81 | * @type Number 82 | */ 83 | scrollToX: 0, 84 | /** 85 | * Position in pixels for which to scroll vertical scrollbar. 86 | * 87 | * @property scrollToY 88 | * @public 89 | * @type Number 90 | */ 91 | scrollToY: 0, 92 | 93 | /** 94 | * Callback when the content is scrolled horizontally. 95 | * 96 | * @method onScrollX 97 | * @public 98 | * @type Function 99 | */ 100 | onScrollX() {}, 101 | 102 | /** 103 | * Callback when the content is scrolled vertically. 104 | * 105 | * @method onScrollY 106 | * @public 107 | * @type Function 108 | */ 109 | onScrollY() {}, 110 | 111 | /** 112 | * Local reference the horizontal scrollbar. 113 | * 114 | * @property horizontalScrollbar 115 | * @private 116 | */ 117 | horizontalScrollbar: null, 118 | /** 119 | * Local reference the vertical scrollbar. 120 | * 121 | * @property verticalScrollbar 122 | * @private 123 | */ 124 | verticalScrollbar: null, 125 | 126 | scrollbarThickness: service(), 127 | 128 | didReceiveAttrs() { 129 | const horizontal = this.get('horizontal'); 130 | const vertical = this.get('horizontal'); 131 | // Keep backwards compatible functionality wherein vertical is default when neither vertical or horizontal are explicitly set 132 | if (!horizontal && !isPresent(vertical)) { 133 | this.set('vertical', true); 134 | } 135 | }, 136 | 137 | didInsertElement() { 138 | this._super(...arguments); 139 | this.setupElements(); 140 | scheduleOnce('afterRender', this, this.createScrollbarAndShowIfNecessary); 141 | this.addEventListener(window, 'mouseup', this.endDrag); 142 | this.setupResize(); 143 | 144 | this.mouseMoveHandler = bind(this, this.onMouseMove) 145 | this.element.addEventListener('mousemove', this.mouseMoveHandler) 146 | }, 147 | 148 | willDestroyElement() { 149 | this._super(...arguments); 150 | this.element.removeEventListener('transitionend webkitTransitionEnd', this._resizeHandler) 151 | 152 | this.element.removeEventListener('mousemove', this.mouseMoveHandler) 153 | this.mouseMoveHandler = null 154 | }, 155 | 156 | 157 | /** 158 | * Inidcates that the horizontal scrollbar is dragging at this moment in time. 159 | * @property isHorizontalDragging 160 | * @private 161 | */ 162 | isHorizontalDragging: false, 163 | /** 164 | * Inidcates that the vertical scrollbar is dragging at this moment in time. 165 | * @property isVerticalDragging 166 | * @private 167 | */ 168 | isVerticalDragging: false, 169 | /** 170 | * Size in pixels of the handle within the horizontal scrollbar. 171 | * Determined by a ration between the scroll content and the scroll viewport 172 | * 173 | * @property horizontalHandleSize 174 | * @private 175 | */ 176 | horizontalHandleSize: null, 177 | /** 178 | * Size in pixels of the handle within the vertical scrollbar. 179 | * Determined by a ration between the scroll content and the scroll viewport 180 | * 181 | * @property verticalHandleSize 182 | * @private 183 | */ 184 | verticalHandleSize: null, 185 | /** 186 | * Amount in pixels offset from the anchor (leftmost point of horizontal scrollbar) 187 | * 188 | * @property horizontalHandleOffset 189 | * @private 190 | */ 191 | horizontalHandleOffset: 0, 192 | /** 193 | * Amount in pixels offset from the anchor (topmost point of vertical scrollbar) 194 | * 195 | * @property verticalHandleOffest 196 | * @private 197 | */ 198 | verticalHandleOffest: 0, 199 | /** 200 | * 201 | * @property dragOffset 202 | * @private 203 | */ 204 | dragOffset: 0, 205 | /** 206 | * @property mouseMoveHandler 207 | * @private 208 | */ 209 | mouseMoveHandler: null, 210 | 211 | contentSize(sizeAttr) { 212 | 213 | let getters = { 214 | 'height': getHeight, 215 | 'width': getWidth 216 | } 217 | return getters[sizeAttr](this._contentElement); 218 | }, 219 | 220 | setupElements() { 221 | this._contentElement = this.get('element').querySelector(`${contentSelector}`); 222 | }, 223 | 224 | /** 225 | * Used to create/reset scrollbar(s) if they are necessary 226 | * 227 | * @method createScrollbarAndShowIfNecessary 228 | */ 229 | createScrollbarAndShowIfNecessary() { 230 | this.createScrollbar().map((scrollbar) => { 231 | this.checkScrolledToBottom(scrollbar); 232 | if (scrollbar.isNecessary) { 233 | this.showScrollbar(); 234 | } 235 | }); 236 | }, 237 | 238 | _resizeHandler() { 239 | debounce(this, this.resizeScrollbar, 16); 240 | }, 241 | 242 | setupResize() { 243 | this.addEventListener(window, 'resize', this._resizeHandler, true); 244 | }, 245 | 246 | resizeScrollContent() { 247 | const width = getWidth(this.element); 248 | const height = getHeight(this.element); 249 | const scrollbarThickness = this.get('scrollbarThickness.thickness'); 250 | 251 | const hasHorizontal = this.get('horizontal'); 252 | const hasVertical = this.get('vertical'); 253 | 254 | if (hasHorizontal && hasVertical) { 255 | this.set('scrollContentWidth', width + scrollbarThickness); 256 | this.set('scrollContentHeight', height + scrollbarThickness); 257 | } else if (hasHorizontal) { 258 | this.set('scrollContentWidth', width); 259 | this.set('scrollContentHeight', height + scrollbarThickness); 260 | this._contentElement.height = height; 261 | } else { 262 | this.set('scrollContentWidth', width + scrollbarThickness); 263 | this.set('scrollContentHeight', height); 264 | } 265 | }, 266 | 267 | /** 268 | * Creates the corresponding scrollbar(s) from the configured `vertical` and `horizontal` properties 269 | * 270 | * @method createScrollbar 271 | * @return {Array} Scrollbar(s) that were created 272 | */ 273 | createScrollbar() { 274 | if (this.get('isDestroyed')) { 275 | return []; 276 | } 277 | const scrollbars = []; 278 | 279 | this.resizeScrollContent(); 280 | 281 | if (this.get('vertical')) { 282 | const verticalScrollbar = new Vertical({ 283 | scrollbarElement: this.element.querySelector(`${scrollbarSelector}.vertical`), 284 | contentElement: this._contentElement 285 | }); 286 | this.set('verticalScrollbar', verticalScrollbar); 287 | this.updateScrollbarAndSetupProperties(0, 'vertical'); 288 | scrollbars.push(verticalScrollbar); 289 | } 290 | if (this.get('horizontal')) { 291 | const horizontalScrollbar = new Horizontal({ 292 | scrollbarElement: this.element.querySelector(`${scrollbarSelector}.horizontal`), 293 | contentElement: this._contentElement 294 | }); 295 | this.set('horizontalScrollbar', horizontalScrollbar); 296 | this.updateScrollbarAndSetupProperties(0, 'horizontal'); 297 | scrollbars.push(horizontalScrollbar); 298 | } 299 | return scrollbars; 300 | }, 301 | 302 | /** 303 | * Show the scrollbar(s) when the user moves within the scroll viewport 304 | * 305 | * @method onMouseMove 306 | * @private 307 | */ 308 | onMouseMove() { 309 | if (this.get('autoHide')) { 310 | throttle(this, this.showScrollbar, THROTTLE_TIME_LESS_THAN_60_FPS_IN_MS); 311 | } 312 | }, 313 | 314 | /** 315 | * Called on mouse up to indicate dragging is over. 316 | * 317 | * @method endDrag 318 | * @param e 319 | * @private 320 | */ 321 | 322 | endDrag(e) { 323 | /* eslint no-unused-vars: 0 */ 324 | this.set('isVerticalDragging', false); 325 | this.set('isHorizontalDragging', false); 326 | }, 327 | 328 | /** 329 | * Calculates and setups the correct handle position using the scrollbar offset and size 330 | * 331 | * @method updateScrollbarAndSetupProperties 332 | * @param scrollOffset 333 | * @param scrollbarDirection 334 | * @private 335 | */ 336 | updateScrollbarAndSetupProperties(scrollOffset, scrollbarDirection) { 337 | const { handleOffset, handleSize } = this.get(`${scrollbarDirection}Scrollbar`).getHandlePositionAndSize(scrollOffset); 338 | this.set(`${scrollbarDirection}HandleOffset`, handleOffset); 339 | this.set(`${scrollbarDirection}HandleSize`, handleSize); 340 | }, 341 | 342 | /** 343 | * Callback for the scroll event emitted by the container of the content that can scroll. 344 | * Here we update the scrollbar to reflect the scrolled position. 345 | * 346 | * @method scrolled 347 | * @param event 348 | * @param scrollOffset 349 | * @param scrollDirection 'vertical' or 'horizontal' 350 | * @private 351 | */ 352 | scrolled(event, scrollOffset, scrollDirection) { 353 | this.updateScrollbarAndSetupProperties(scrollOffset, scrollDirection); 354 | this.showScrollbar(); 355 | 356 | this.checkScrolledToBottom(this.get(`${scrollDirection}Scrollbar`), scrollOffset); 357 | const direction = scrollDirection === 'vertical' ? 'Y' : 'X'; 358 | this.get(`onScroll${direction}`)(scrollOffset); 359 | // synchronize scrollToX / scrollToY 360 | this.set(`scrollTo${direction}`, scrollOffset); 361 | // TODO this is deprecated. remove eventually. 362 | this.sendScroll(event, scrollOffset); 363 | }, 364 | 365 | 366 | checkScrolledToBottom(scrollbar, scrollOffset) { 367 | let scrollBuffer = this.get('scrollBuffer'); 368 | 369 | if (scrollbar.isScrolledToBottom(scrollBuffer, scrollOffset)) { 370 | debounce(this, this.sendScrolledToBottom, 100); 371 | } 372 | }, 373 | 374 | sendScrolledToBottom() { 375 | if (this.get('onScrolledToBottom')) { 376 | this.get('onScrolledToBottom')(); 377 | } 378 | }, 379 | 380 | sendScroll(event, scrollOffset) { 381 | if (this.get('onScroll')) { 382 | deprecate('Using the `onScroll` callback has deprecated in favor of the explicit `onScrollX` and `onScrollY callbacks'); 383 | this.get('onScroll')(scrollOffset, event); 384 | } 385 | }, 386 | 387 | resizeScrollbar() { 388 | this.createScrollbarAndShowIfNecessary(); 389 | }, 390 | 391 | showScrollbar() { 392 | if (this.get('isDestroyed')) { 393 | return; 394 | } 395 | this.set('showHandle', true); 396 | 397 | if (!this.get('autoHide')) { 398 | return; 399 | } 400 | 401 | debounce(this, this.hideScrollbar, hideDelay); 402 | }, 403 | 404 | hideScrollbar() { 405 | if (this.get('isDestroyed')) { 406 | return; 407 | } 408 | this.set('showHandle', false); 409 | }, 410 | 411 | /** 412 | * Sets scrollTo{X,Y} by using the ratio of offset to content size. 413 | * Called when the handle in ember-scrollbar is dragged. 414 | * 415 | * @method updateScrollToProperty 416 | * @param scrollProp {String} String indicating the scrollTo attribute to be updated ('scrollToX'|'scrollToY') 417 | * @param dragPerc {Number} A Number from 0 - 1 indicating the position of the handle as percentage of the scrollbar 418 | * @param sizeAttr {String} String indicating the attribute used to get to the size of the content ('height'|'width') 419 | * @private 420 | */ 421 | updateScrollToProperty(scrollProp, dragPerc, sizeAttr) { 422 | const srcollTo = dragPerc * this.contentSize(sizeAttr); 423 | this.set(scrollProp, srcollTo); 424 | }, 425 | 426 | /** 427 | * Sets is{Horizontal,Vertical}Dragging to true or false when the handle starts or ends dragging 428 | * 429 | * @method toggleDraggingBoundary 430 | * @param isDraggingProp 'isHorizontalDragging' or 'isVerticalDragging' 431 | * @param startOrEnd true if starting to drag, false if ending 432 | * @private 433 | */ 434 | toggleDraggingBoundary(isDraggingProp, startOrEnd) { 435 | this.set(isDraggingProp, startOrEnd); 436 | }, 437 | 438 | /** 439 | * Jumps a page because user clicked on scroll bar not scroll bar handle. 440 | * 441 | * @method jumpScroll 442 | * @param jumpPositive if true the user clicked between the handle and the end, if false, the user clicked between the 443 | * anchor and the handle 444 | * @param scrollToProp 'scrollToX' or 'scrollToY' 445 | * @param sizeAttr 'width' or 'height' 446 | * @private 447 | */ 448 | jumpScroll(jumpPositive, scrollToProp, sizeAttr) { 449 | const scrollOffset = this.get(scrollToProp); 450 | let jumpAmt = PAGE_JUMP_MULTIPLE * this.contentSize(sizeAttr); 451 | let scrollPos = jumpPositive ? scrollOffset - jumpAmt : scrollOffset + jumpAmt; 452 | this.set(scrollToProp, scrollPos); 453 | }, 454 | 455 | 456 | actions: { 457 | 458 | /** 459 | * Update action should be called when size of the scroll area changes 460 | */ 461 | recalculate() { 462 | // TODO this is effectively the same as `update`, except for update returns the passed in value. Keep one, and rename `resizeScrollbar` to be clear. 463 | this.resizeScrollbar(); 464 | }, 465 | 466 | /** 467 | * Can be called when scrollbars change as a result of value change, 468 | * 469 | * for example 470 | * ``` 471 | * {{#as-scrollable as |scrollbar|}} 472 | * {{#each (compute scrollbar.update rows) as |row|}} 473 | * {{row.title}} 474 | * {{/each}} 475 | * {{/as-scrollable}} 476 | * ``` 477 | */ 478 | update(value) { 479 | scheduleOnce('afterRender', this, this.resizeScrollbar); 480 | return value; 481 | }, 482 | 483 | /** 484 | * Scroll Top action should be called when when the scroll area should be scrolled top manually 485 | */ 486 | scrollTop() { 487 | // TODO some might expect the `scrollToY` action to be called here 488 | this.set('scrollToY', 0); 489 | }, 490 | scrolled() { 491 | scheduleOnce('afterRender', this, 'scrolled', ...arguments); 492 | }, 493 | horizontalDrag(dragPerc) { 494 | scheduleOnce('afterRender', this, 'updateScrollToProperty', 'scrollToX', dragPerc, 'width'); 495 | }, 496 | verticalDrag(dragPerc) { 497 | scheduleOnce('afterRender', this, 'updateScrollToProperty', 'scrollToY', dragPerc, 'height'); 498 | }, 499 | horizontalJumpTo(jumpPositive) { 500 | this.jumpScroll(jumpPositive, 'scrollToX', 'width'); 501 | }, 502 | verticalJumpTo(jumpPositive) { 503 | this.jumpScroll(jumpPositive, 'scrollToY', 'height'); 504 | }, 505 | horizontalDragBoundary(isStart) { 506 | this.toggleDraggingBoundary('isHorizontalDragging', isStart); 507 | }, 508 | verticalBoundaryEvent(isStart) { 509 | this.toggleDraggingBoundary('isVerticalDragging', isStart); 510 | } 511 | } 512 | }); 513 | -------------------------------------------------------------------------------- /addon/components/ember-scrollbar.js: -------------------------------------------------------------------------------- 1 | import { computed } from '@ember/object'; 2 | import { isPresent } from '@ember/utils'; 3 | import { throttle } from '@ember/runloop'; 4 | import Component from '@ember/component'; 5 | import DomMixin from 'ember-lifeline/mixins/dom'; 6 | import layout from '../templates/components/ember-scrollbar'; 7 | import { styleify } from '../util/css'; 8 | import { THROTTLE_TIME_LESS_THAN_60_FPS_IN_MS } from './ember-scrollable'; 9 | import { capitalize } from '@ember/string'; 10 | 11 | const handleSelector = '.drag-handle'; 12 | 13 | /** 14 | * Handles displaying and moving the handle within the confines of it's template. 15 | * Has callbacks for intending to dragging and jump to particular positions. 16 | * 17 | * @class EmberScrollbar 18 | * @extends Ember.Component 19 | */ 20 | export default Component.extend(DomMixin, { 21 | layout, 22 | classNameBindings: [':tse-scrollbar', 'horizontal:horizontal:vertical'], 23 | onDrag(){}, 24 | onJumpTo(){}, 25 | onDragStart(){}, 26 | onDragEnd(){}, 27 | 28 | horizontal: false, 29 | isDragging: false, 30 | showHandle: false, 31 | handleSize: null, 32 | handleOffset: 0, 33 | 34 | offsetAttr: computed('horizontal', function() { 35 | return this.get('horizontal') ? 'left' : 'top'; 36 | }), 37 | 38 | jumpScrollOffsetAttr: computed('horizontal', function() { 39 | return this.get('horizontal') ? 'offsetX' : 'offsetY'; 40 | }), 41 | 42 | eventOffsetAttr: computed('horizontal', function() { 43 | return this.get('horizontal') ? 'pageX' : 'pageY'; 44 | }), 45 | 46 | sizeAttr: computed('horizontal', function() { 47 | return this.get('horizontal') ? 'width' : 'height'; 48 | }), 49 | 50 | 51 | handleStylesJSON: computed('handleOffset', 'handleSize', 'horizontal', function() { 52 | const { handleOffset, handleSize } = this.getProperties('handleOffset', 'handleSize'); 53 | if (this.get('horizontal')) { 54 | return { left: handleOffset + 'px', width: handleSize + 'px' }; 55 | } else { 56 | return { top: handleOffset + 'px', height: handleSize + 'px' }; 57 | } 58 | }), 59 | 60 | handleStyles: computed('handleStylesJSON.{top,left,width,height}', function() { 61 | return styleify(this.get('handleStylesJSON')); 62 | }), 63 | 64 | 65 | mouseDown(e) { 66 | this.jumpScroll(e); 67 | }, 68 | 69 | 70 | startDrag(e) { 71 | // Preventing the event's default action stops text being 72 | // selectable during the drag. 73 | e.preventDefault(); 74 | e.stopPropagation(); 75 | 76 | const dragOffset = this._startDrag(e); 77 | this.set('dragOffset', dragOffset); 78 | this.get('onDragStart')(e); 79 | }, 80 | 81 | mouseUp() { 82 | this.endDrag(); 83 | }, 84 | 85 | didInsertElement() { 86 | this._super(...arguments); 87 | this.addEventListener(window, 'mousemove', (e) => { 88 | throttle(this, this.updateMouseOffset, e, THROTTLE_TIME_LESS_THAN_60_FPS_IN_MS); 89 | }); 90 | }, 91 | 92 | endDrag() { 93 | this.get('onDragEnd')(); 94 | }, 95 | 96 | /** 97 | * Callback for the mouse move event. Update the mouse offsets given the new mouse position. 98 | * 99 | * @method updateMouseOffset 100 | * @param e 101 | * @private 102 | */ 103 | updateMouseOffset(e) { 104 | const { pageX, pageY } = e; 105 | const mouseOffset = this.get('horizontal') ? pageX : pageY; 106 | 107 | if (this.get('isDragging') && isPresent(mouseOffset)) { 108 | this._drag(mouseOffset, this.get('dragOffset')); 109 | } 110 | }, 111 | 112 | /** 113 | * Handles when user clicks on scrollbar, but not on the actual handle, and the scroll should 114 | * jump to the selected position. 115 | * 116 | * @method jumpScroll 117 | * @param e 118 | */ 119 | jumpScroll(e) { 120 | // If the drag handle element was pressed, don't do anything here. 121 | if (e.target === this.get('element').querySelector(handleSelector)) { 122 | return; 123 | } 124 | this._jumpScroll(e); 125 | }, 126 | 127 | 128 | // private methods 129 | /** 130 | * Convert the mouse position into a percentage of the scrollbar height/width. 131 | * and sends to parent 132 | * 133 | * @param eventOffset 134 | * @param dragOffset 135 | * @private 136 | */ 137 | _drag(eventOffset, dragOffset) { 138 | const scrollbarOffset = this._scrollbarOffset(); 139 | let dragPos = eventOffset - scrollbarOffset - dragOffset; 140 | // Convert the mouse position into a percentage of the scrollbar height/width. 141 | let dragPerc = dragPos / this._scrollbarSize(); 142 | this.get('onDrag')(dragPerc); 143 | }, 144 | 145 | /** 146 | * Calls `onJumpTo` action with a boolean indicating the direction of the jump, and the jQuery MouseDown event. 147 | * 148 | * If towardsAnchor is true, the jump is in a direction towards from the initial anchored position of the scrollbar. 149 | * i.e. for a vertical scrollbar, towardsAnchor=true indicates moving upwards, and towardsAnchor=false is downwards 150 | * for a horizontal scrollbar, towardsAnchor=true indicates moving left, and towardsAnchor=false is right 151 | * 152 | * @param e 153 | * @private 154 | */ 155 | _jumpScroll(e) { 156 | let eventOffset = this._jumpScrollEventOffset(e); 157 | let handleOffset = this._handlePositionOffset(); 158 | const towardsAnchor = (eventOffset < handleOffset); 159 | 160 | this.get('onJumpTo')(towardsAnchor, e); 161 | }, 162 | 163 | 164 | _startDrag(e) { 165 | return this._eventOffset(e) - this._handleOffset(); 166 | }, 167 | 168 | 169 | _handleOffset() { 170 | return this.get('element').querySelector(handleSelector).getBoundingClientRect()[this.get('offsetAttr')]; 171 | }, 172 | 173 | 174 | _handlePositionOffset() { 175 | let el = this.get('element').querySelector(handleSelector); 176 | let position = { 177 | left: el.offsetLeft, 178 | top: el.offsetTop 179 | } 180 | 181 | return position[this.get('offsetAttr')]; 182 | }, 183 | 184 | _scrollbarOffset() { 185 | return this.get('element').getBoundingClientRect()[this.get('offsetAttr')]; 186 | }, 187 | 188 | /** 189 | * Returns the offset from the anchor point derived from this MouseEvent 190 | * @param e MouseEvent 191 | * @return {Number} 192 | */ 193 | _jumpScrollEventOffset(e) { 194 | return e[this.get('jumpScrollOffsetAttr')]; 195 | }, 196 | 197 | 198 | _eventOffset(e) { 199 | return e[this.get('eventOffsetAttr')]; 200 | }, 201 | 202 | 203 | _scrollbarSize() { 204 | return this.get('element')[`offset${capitalize(this.get('sizeAttr'))}`]; 205 | }, 206 | 207 | actions: { 208 | startDrag(){ 209 | this.startDrag(...arguments); 210 | } 211 | } 212 | }); 213 | -------------------------------------------------------------------------------- /addon/components/scroll-content-element.js: -------------------------------------------------------------------------------- 1 | import { computed } from '@ember/object'; 2 | import { schedule } from '@ember/runloop'; 3 | import Component from '@ember/component'; 4 | import layout from '../templates/components/scroll-content-element'; 5 | import DomMixin from 'ember-lifeline/mixins/dom'; 6 | import { styleify } from '../util/css'; 7 | import Number from '../util/number'; 8 | 9 | /** 10 | * 11 | * Handles scroll events within the body of the scroll content, 12 | * properly sets scrollTop / scrollLeft property depending on the configuration and the given scrollTo. 13 | * 14 | * @class ScrollContentElement 15 | * @extends Ember.Component 16 | */ 17 | export default Component.extend(DomMixin, { 18 | /** 19 | * Adds the `tse-scroll-content` class to this element which is in charge of removing the default scrollbar(s) from 20 | * this element (the container for the content being scrolled). Also the `tse-scroll-content` class enables 21 | * overflow to trigger scroll events although this element doesn't have a native scrollbar. 22 | * 23 | * @property classNameBindings 24 | * @public 25 | * @type Array 26 | */ 27 | classNameBindings: [':tse-scroll-content'], 28 | attributeBindings: ['style'], 29 | layout, 30 | /** 31 | * Callback function when scroll event occurs. 32 | * Arguments are: jQueryEvent, scrollOffset, and horizontal'|'vertical' 33 | * @property onScroll 34 | * @public 35 | * @type Function 36 | */ 37 | onScroll(){}, 38 | 39 | /** 40 | * Height of this content. Note content must have a height that is larger than this in order to cause overflow-y, 41 | * and enabling scrolling. 42 | * 43 | * @property height 44 | * @public 45 | * @type Number 46 | */ 47 | height: null, 48 | 49 | /** 50 | * Width of this content. Note contnet must have a width that is larger than this in order to cause overflow-x 51 | * therefore enabling scrolling. 52 | * 53 | * @property width 54 | * @public 55 | * @type Number 56 | */ 57 | width: null, 58 | 59 | /** 60 | * Integer representing desired scrollLeft to be set for this element. 61 | * 62 | * @property scrollToX 63 | * @public 64 | * @type Number 65 | * @default 0 66 | */ 67 | scrollToX: 0, 68 | 69 | /** 70 | * Integer representing desired scrollTop to be set for this element. 71 | * 72 | * @property scrollToX 73 | * @public 74 | * @type Number 75 | * @default 0 76 | */ 77 | scrollToY: 0, 78 | 79 | /** 80 | * Intermediate object to collect style attributes. Height and width are set dynamically such that space is allocated 81 | * for the given scrollbars that will be rendered. 82 | * 83 | * @property stylesJSON 84 | * @private 85 | */ 86 | stylesJSON: computed('height', 'width', function() { 87 | const { height, width } = this.getProperties('height', 'width'); 88 | return { width: width + 'px', height: height + 'px' }; 89 | }), 90 | 91 | /** 92 | * String bound to the style attribute. 93 | * 94 | * @property style 95 | * @private 96 | * @type String 97 | */ 98 | style: computed('stylesJSON.{height,width}', function() { 99 | return styleify(this.get('stylesJSON')); 100 | }), 101 | 102 | /** 103 | * Previous scrollToX value. Useful for calculating changes in scrollToX. 104 | * 105 | * @property previousScrollToX 106 | * @private 107 | * @type Number 108 | */ 109 | previousScrollToX: 0, 110 | 111 | /** 112 | * Previous scrollToY value. Useful for calculating changes in scrollToY. 113 | * 114 | * @property previousScrollToY 115 | * @private 116 | * @type Number 117 | */ 118 | previousScrollToY: 0, 119 | 120 | /** 121 | * Callback from scroll event on the content of this element. 122 | * Determines direction of scroll and calls the `onScroll` action with: 123 | * - the jQueryEvent 124 | * - the scrollOffset -- indicates positioning from top/left anchor point 125 | * - 'horizontal' | 'vertical' -- indicates direction of scroll 126 | * 127 | * @method scrolled 128 | * @param e jQueryEvent 129 | */ 130 | scrolled(e) { 131 | const newScrollLeft = e.target.scrollLeft; 132 | const newScrollTop = e.target.scrollTop; 133 | 134 | if (newScrollLeft !== this.get('previousScrollToX')) { 135 | this.get('onScroll')(e, newScrollLeft, 'horizontal'); 136 | this.set(`previousScrollToX`, newScrollLeft); 137 | } else if (newScrollTop !== this.get('previousScrollToY')) { 138 | this.get('onScroll')(e, newScrollTop, 'vertical'); 139 | this.set(`previousScrollToY`, newScrollTop); 140 | } 141 | }, 142 | 143 | /** 144 | * Sets the scroll property (scrollTop, or scrollLeft) for the given the direction and offset. 145 | * 146 | * @method scrollToPosition 147 | * @private 148 | * @param offset Number -- offset amount in pixels 149 | * @param direction String -- 'X' | 'Y' -- indicates what direction is being scrolled 150 | */ 151 | scrollToPosition(offset, direction) { 152 | offset = Number.parseInt(offset, 10); 153 | 154 | if (Number.isNaN(offset)) { 155 | return; 156 | } 157 | let scrollOffsetAttr = direction === 'X' ? 'scrollLeft' : 'scrollTop'; 158 | this.get('element')[scrollOffsetAttr] = offset 159 | }, 160 | 161 | configureInitialScrollPosition() { 162 | this.scrollToPosition(this.get('scrollToX'), 'X'); 163 | this.scrollToPosition(this.get('scrollToY'), 'Y'); 164 | }, 165 | 166 | didInsertElement() { 167 | this._super(...arguments); 168 | this.addEventListener(this.element, 'scroll', this.scrolled); 169 | this.configureInitialScrollPosition(); 170 | }, 171 | 172 | didReceiveAttrs() { 173 | // Sync property changes to `scrollToX` and `scrollToY` with the `scrollTop` and `scrollLeft` attributes 174 | // of the rendered DOM element. 175 | ['X', 'Y'].forEach((direction) => { 176 | const oldOffset = this.get(`previousScrollTo${direction}`); 177 | const newOffset = this.get(`scrollTo${direction}`); 178 | 179 | if (oldOffset !== newOffset) { 180 | schedule('afterRender', this, this.scrollToPosition, newOffset, direction); 181 | } 182 | }); 183 | } 184 | 185 | }); 186 | -------------------------------------------------------------------------------- /addon/services/scrollbar-thickness.js: -------------------------------------------------------------------------------- 1 | import { computed } from '@ember/object'; 2 | import Service from '@ember/service'; 3 | import { getWidth } from '../util/measurements'; 4 | 5 | export default Service.extend({ 6 | thickness: computed(() => { 7 | let tempEl = document.createElement('div'); 8 | tempEl.setAttribute('style', 'width: 50px; position: absolute; left: -100px;'); 9 | tempEl.classList.add('scrollbar-width-tester') 10 | tempEl.innerHTML = `
`; 11 | document.body.appendChild(tempEl); 12 | 13 | let width = getWidth(tempEl) 14 | let widthMinusScrollbars = getWidth(tempEl.querySelector('.scrollbar-width-tester__inner')); 15 | 16 | return (width - widthMinusScrollbars); 17 | }) 18 | }); 19 | -------------------------------------------------------------------------------- /addon/styles/addon.css: -------------------------------------------------------------------------------- 1 | /** 2 | * TrackpadScrollEmulator 3 | * Author: Jonathan Nicol @f6design 4 | * https://github.com/jnicol/trackpad-scroll-emulator 5 | */ 6 | .tse-scrollable { 7 | position: relative; 8 | overflow: hidden; 9 | height: 100%; 10 | width: 100%; 11 | } 12 | .tse-scrollable .tse-scroll-content { 13 | position: absolute; 14 | } 15 | 16 | .tse-scrollbar { 17 | position: absolute; 18 | z-index: 99; 19 | } 20 | .tse-scrollbar .drag-handle { 21 | position: absolute; 22 | -webkit-transition: opacity 0.2s linear; 23 | -moz-transition: opacity 0.2s linear; 24 | -ms-transition: opacity 0.2s linear; 25 | -o-transition: opacity 0.2s linear; 26 | transition: opacity 0.2s linear; 27 | opacity: 0; 28 | -webkit-border-radius: 7px; 29 | -moz-border-radius: 7px; 30 | border-radius: 7px; 31 | background: #6c6e71; 32 | -webkit-background-clip: padding-box; 33 | -moz-background-clip: padding; 34 | } 35 | .tse-scrollbar:hover .drag-handle { 36 | -webkit-transition: opacity 0 linear; 37 | -moz-transition: opacity 0 linear; 38 | -ms-transition: opacity 0 linear; 39 | -o-transition: opacity 0 linear; 40 | transition: opacity 0 linear; 41 | /* When hovered, remove all transitions from drag handle */ 42 | opacity: 0.7; 43 | } 44 | .tse-scrollbar .drag-handle.visible { 45 | opacity: 0.7; 46 | } 47 | 48 | /* Vertical scroller */ 49 | .tse-scrollbar.vertical { 50 | /* make the scrollbar span full height w/o setting the height*/ 51 | top: 0; 52 | right: 0; 53 | bottom: 0; 54 | width: 11px; 55 | } 56 | .tse-scrollbar.vertical .tse-scroll-content { 57 | overflow: hidden; 58 | overflow-y: scroll; 59 | position: absolute; 60 | } 61 | .tse-scrollbar.vertical .drag-handle { 62 | right: 2px; 63 | width: 7px; 64 | } 65 | 66 | /* Horizontal scroller */ 67 | .tse-scrollable.horizontal .tse-scroll-content { 68 | overflow-x: scroll; 69 | overflow-y: hidden; 70 | } 71 | .tse-scrollable.horizontal .tse-scrollbar.horizontal { 72 | /* make the scrollbar span full length w/o setting the width */ 73 | left: 0; 74 | right: 0; 75 | bottom: 0; 76 | top: auto; 77 | width: auto; 78 | height: 11px; 79 | } 80 | .tse-scrollable.horizontal .tse-scrollbar.horizontal .drag-handle { 81 | top: 2px; 82 | right: auto; 83 | width: auto; 84 | min-width: 0; 85 | height: 7px; 86 | min-height: 0; 87 | } 88 | 89 | .tse-scrollable.horizontal .tse-scroll-content { 90 | overflow-x: scroll; 91 | overflow-y: hidden; 92 | } 93 | .tse-scrollable.vertical .tse-scroll-content { 94 | overflow-x: hidden; 95 | overflow-y: scroll; 96 | } 97 | .tse-scrollable.horizontal.vertical .tse-scroll-content { 98 | overflow-x: scroll; 99 | overflow-y: scroll; 100 | } 101 | -------------------------------------------------------------------------------- /addon/templates/components/ember-scrollable.hbs: -------------------------------------------------------------------------------- 1 | {{resize-detector (concat '#' elementId) on-resize=(action 'recalculate')}} 2 | {{#if horizontal}} 3 | {{ember-scrollbar 4 | handleOffset=horizontalHandleOffset 5 | handleSize=horizontalHandleSize 6 | horizontal=true 7 | showHandle=showHandle 8 | isDragging=isHorizontalDragging 9 | onJumpTo=(action 'horizontalJumpTo') 10 | onDrag=(action 'horizontalDrag') 11 | onDragStart=(action 'horizontalDragBoundary' true) 12 | onDragEnd=(action 'horizontalDragBoundary' false) 13 | }} 14 | {{/if}} 15 | {{#if vertical}} 16 | {{ember-scrollbar 17 | handleOffset=verticalHandleOffset 18 | handleSize=verticalHandleSize 19 | horizontal=false 20 | showHandle=showHandle 21 | isDragging=isVerticalDragging 22 | onJumpTo=(action 'verticalJumpTo') 23 | onDrag=(action 'verticalDrag') 24 | onDragStart=(action 'verticalBoundaryEvent' true) 25 | onDragEnd=(action 'verticalBoundaryEvent' false) 26 | }} 27 | {{/if}} 28 | {{#scroll-content-element 29 | height=scrollContentHeight 30 | width=scrollContentWidth 31 | scrollToX=scrollToX 32 | scrollToY=scrollToY 33 | onScroll=(action 'scrolled') 34 | }} 35 |
36 | {{yield (hash 37 | recalculate=(action 'recalculate') 38 | update=(action 'update') 39 | scrollTop=(action 'scrollTop') 40 | )}} 41 |
42 | {{/scroll-content-element}} 43 | -------------------------------------------------------------------------------- /addon/templates/components/ember-scrollbar.hbs: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /addon/templates/components/scroll-content-element.hbs: -------------------------------------------------------------------------------- 1 | {{yield}} -------------------------------------------------------------------------------- /addon/util/css.js: -------------------------------------------------------------------------------- 1 | import { isEmpty } from '@ember/utils'; 2 | import { htmlSafe } from '@ember/string'; 3 | 4 | function styleify(obj) { 5 | if (isEmpty(obj)) { 6 | return htmlSafe(''); 7 | } 8 | const styles = Object.keys(obj).reduce((styleString, key) => { 9 | const styleValue = obj[key]; 10 | if (!isEmpty(styleValue)) { 11 | styleString += `${key}: ${styleValue}; `; 12 | } 13 | return styleString; 14 | }, ''); 15 | return htmlSafe(styles); 16 | 17 | } 18 | 19 | export { styleify }; 20 | -------------------------------------------------------------------------------- /addon/util/measurements.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Replacement for jQuery $.height() 4 | * Borrowed with thanks from https://github.com/nefe/You-Dont-Need-jQuery#2.2 5 | */ 6 | export function getHeight(el) { 7 | let styles = window.getComputedStyle(el); 8 | let height = el.offsetHeight; 9 | let borderTopWidth = parseFloat(styles.borderTopWidth); 10 | let borderBottomWidth = parseFloat(styles.borderBottomWidth); 11 | let paddingTop = parseFloat(styles.paddingTop); 12 | let paddingBottom = parseFloat(styles.paddingBottom); 13 | return height - borderBottomWidth - borderTopWidth - paddingTop - paddingBottom; 14 | } 15 | /** 16 | * Replacement function for jQuery $.width() 17 | * Borrowed with thanks from https://github.com/nefe/You-Dont-Need-jQuery#2.2 18 | */ 19 | export function getWidth(el) { 20 | let styles = window.getComputedStyle(el); 21 | let width = el.offsetWidth; 22 | let borderLeftWidth = parseFloat(styles.borderLeftWidth); 23 | let borderRightWidth = parseFloat(styles.borderRightWidth); 24 | let paddingLeft = parseFloat(styles.paddingLeft); 25 | let paddingRight = parseFloat(styles.paddingRight); 26 | return width - borderLeftWidth - borderRightWidth - paddingRight - paddingLeft; 27 | } -------------------------------------------------------------------------------- /addon/util/number.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Polyfills for `Number` global. 3 | */ 4 | export default { 5 | isNaN: Number.isNaN || window.isNaN, 6 | parseInt: Number.parseInt || window.parseInt 7 | }; 8 | -------------------------------------------------------------------------------- /addon/util/timeout.js: -------------------------------------------------------------------------------- 1 | import { Promise } from 'rsvp'; 2 | import { later } from '@ember/runloop'; 3 | 4 | export function timeout(ms) { 5 | let promise = new Promise(r => { 6 | later(r, ms); 7 | }); 8 | return promise; 9 | } 10 | -------------------------------------------------------------------------------- /app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphasights/ember-scrollable/3ecf8a7943a72b65fc7c49e5d96ba5494ebab536/app/.gitkeep -------------------------------------------------------------------------------- /app/components/as-scrollable.js: -------------------------------------------------------------------------------- 1 | import EmberScrollable from 'ember-scrollable/components/ember-scrollable'; 2 | 3 | export default EmberScrollable.extend({ 4 | classNames: 'as-scrollable' 5 | }); -------------------------------------------------------------------------------- /app/components/ember-scrollable.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-scrollable/components/ember-scrollable'; -------------------------------------------------------------------------------- /app/components/ember-scrollbar.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-scrollable/components/ember-scrollbar'; -------------------------------------------------------------------------------- /app/components/scroll-content-element.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-scrollable/components/scroll-content-element'; -------------------------------------------------------------------------------- /app/services/scrollbar-thickness.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-scrollable/services/scrollbar-thickness'; 2 | -------------------------------------------------------------------------------- /bin/ci: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | bin/lint 6 | ember test 7 | -------------------------------------------------------------------------------- /bin/lint: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | bundle exec scss-lint tests/dummy/app 6 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | npm install -g bower 6 | npm install 7 | bower install 8 | bundle install 9 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | environment: 3 | PATH: "${HOME}/${CIRCLE_PROJECT_REPONAME}/bin:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin:${PATH}" 4 | node: 5 | version: 0.12.0 6 | 7 | test: 8 | override: 9 | - ./bin/ci 10 | 11 | dependencies: 12 | override: 13 | - "cd bin && wget https://s3.amazonaws.com/travis-phantomjs/phantomjs-2.0.0-ubuntu-12.04.tar.bz2 && tar xjvf phantomjs-2.0.0-ubuntu-12.04.tar.bz2" 14 | - "./bin/setup" 15 | -------------------------------------------------------------------------------- /config/changelog.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | // For details on each option run `ember help release` 4 | module.exports = { 5 | 6 | // angular style guide: https://github.com/angular/angular.js/blob/v1.4.8/CONTRIBUTING.md#commit 7 | // jquery style guide: https://contribute.jquery.org/commits-and-pull-requests/#commit-guidelines 8 | // ember style guide: https://github.com/emberjs/ember.js/blob/master/CONTRIBUTING.md#commit-tagging 9 | style: 'ember', // 'ember' 'jquery' 10 | 11 | head: 'master', 12 | base: '-last', // a branch or tag name, `-last` defaults to the version in package.json 13 | 14 | hooks: { 15 | /* 16 | parser: function(commit) { return commit; } 17 | filter: function(commit) { return true; }, 18 | groupSort: function(commits) { return { commits: commits }; }, 19 | format: function(commit) { return commit.title; }, 20 | */ 21 | } 22 | 23 | }; 24 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | 5 | module.exports = function() { 6 | return Promise.all([ 7 | getChannelURL('release'), 8 | getChannelURL('beta'), 9 | getChannelURL('canary') 10 | ]).then((urls) => { 11 | return { 12 | useYarn: true, 13 | scenarios: [ 14 | { 15 | name: 'ember-lts-2.12', 16 | npm: { 17 | devDependencies: { 18 | 'ember-source': '~2.12.0' 19 | } 20 | } 21 | }, 22 | { 23 | name: 'ember-lts-2.16', 24 | npm: { 25 | devDependencies: { 26 | 'ember-source': '~2.16.0' 27 | } 28 | } 29 | }, 30 | { 31 | name: 'ember-lts-2.18', 32 | npm: { 33 | devDependencies: { 34 | 'ember-source': '~2.18.0' 35 | } 36 | } 37 | }, 38 | { 39 | name: 'ember-release', 40 | npm: { 41 | devDependencies: { 42 | 'ember-source': urls[0] 43 | } 44 | } 45 | }, 46 | { 47 | name: 'ember-beta', 48 | npm: { 49 | devDependencies: { 50 | 'ember-source': urls[1] 51 | } 52 | } 53 | }, 54 | { 55 | name: 'ember-canary', 56 | npm: { 57 | devDependencies: { 58 | 'ember-source': urls[2] 59 | } 60 | } 61 | }, 62 | { 63 | name: 'ember-default', 64 | npm: { 65 | devDependencies: {} 66 | } 67 | } 68 | ] 69 | }; 70 | }); 71 | }; 72 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(/* environment, appConfig */) { 4 | return { }; 5 | }; 6 | -------------------------------------------------------------------------------- /config/release.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | /* eslint no-console: 0 */ 3 | 4 | var execSync = require('child_process').execSync; 5 | var generateChangelog = require('ember-cli-changelog/lib/tasks/release-with-changelog'); 6 | 7 | module.exports = { 8 | publish: true, 9 | 10 | beforeCommit: generateChangelog, 11 | 12 | afterPublish: function(project, versions) { 13 | runCommand('ember github-pages:commit --message "Released ' + versions.next + '"'); 14 | runCommand('git push origin gh-pages:gh-pages'); 15 | } 16 | }; 17 | 18 | function runCommand(command) { 19 | console.log('running: ' + command); 20 | var output = execSync(command, { encoding: 'utf8' }); 21 | console.log(output); 22 | } 23 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function(defaults) { 6 | let app = new EmberAddon(defaults, { 7 | snippetSearchPaths: ['tests/dummy/app'] 8 | }); 9 | 10 | /* 11 | This build file specifies the options for the dummy test app of this 12 | addon, located in `/tests/dummy` 13 | This build file does *not* influence how the addon or the app using it 14 | behave. You most likely want to be modifying `./index.js` or app's build file 15 | */ 16 | 17 | return app.toTree(); 18 | }; 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | 'use strict'; 4 | 5 | module.exports = { 6 | name: 'ember-scrollable', 7 | 8 | included: function(app) { 9 | while (app.app) { 10 | app = app.app; 11 | } 12 | this.app = app; 13 | 14 | this._super.included.apply(this, arguments); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-scrollable", 3 | "version": "1.0.2", 4 | "description": "OSX-like scrollbar components in ember", 5 | "keywords": [ 6 | "ember-addon" 7 | ], 8 | "repository": "https://github.com/alphasights/ember-scrollable", 9 | "license": "MIT", 10 | "author": "Eugenio Depalo ", 11 | "directories": { 12 | "doc": "doc", 13 | "test": "tests" 14 | }, 15 | "scripts": { 16 | "build": "ember build", 17 | "lint:js": "eslint .", 18 | "start": "ember serve", 19 | "test": "ember test", 20 | "test:all": "ember try:each" 21 | }, 22 | "dependencies": { 23 | "ember-cli-babel": "^6.8.0", 24 | "ember-cli-htmlbars": "^2.0.1", 25 | "ember-component-inbound-actions": "^1.3.0", 26 | "ember-element-resize-detector": "~0.4.0", 27 | "ember-lifeline": "^4.1.5" 28 | }, 29 | "devDependencies": { 30 | "@ember/jquery": "^1.1.0", 31 | "@ember/optional-features": "^1.0.0", 32 | "broccoli-asset-rev": "^2.7.0", 33 | "ember-ajax": "^3.0.0", 34 | "ember-cli": "~3.3.0", 35 | "ember-cli-app-version": "^3.1.0", 36 | "ember-cli-changelog": "0.3.4", 37 | "ember-cli-dependency-checker": "^2.0.0", 38 | "ember-cli-eslint": "^4.2.1", 39 | "ember-cli-github-pages": "0.1.2", 40 | "ember-cli-htmlbars-inline-precompile": "^1.0.0", 41 | "ember-cli-inject-live-reload": "^1.4.1", 42 | "ember-cli-qunit": "^4.3.2", 43 | "ember-cli-release": "1.0.0-beta.2", 44 | "ember-cli-sass": "7.1.1", 45 | "ember-cli-shims": "^1.2.0", 46 | "ember-cli-sri": "^2.1.0", 47 | "ember-cli-uglify": "^2.0.0", 48 | "ember-code-snippet": "2.0.0", 49 | "ember-disable-prototype-extensions": "^1.1.2", 50 | "ember-drag-drop": "0.7.0", 51 | "ember-export-application-global": "^2.0.0", 52 | "ember-load-initializers": "^1.1.0", 53 | "ember-maybe-import-regenerator": "^0.1.6", 54 | "ember-native-dom-helpers": "0.5.5", 55 | "ember-resolver": "^4.0.0", 56 | "ember-source": "~3.3.0", 57 | "ember-source-channel-url": "^1.0.1", 58 | "ember-try": "^0.2.23", 59 | "eslint-plugin-ember": "^5.0.0", 60 | "eslint-plugin-node": "^6.0.1", 61 | "loader.js": "^4.2.3", 62 | "qunit-dom": "^0.6.2" 63 | }, 64 | "engines": { 65 | "node": ">= 10.*" 66 | }, 67 | "ember-addon": { 68 | "configPath": "tests/dummy/config" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test_page: 'tests/index.html?hidepassed', 3 | disable_watching: true, 4 | launch_in_ci: [ 5 | 'Chrome' 6 | ], 7 | launch_in_dev: [ 8 | 'Chrome' 9 | ], 10 | browser_args: { 11 | Chrome: { 12 | ci: [ 13 | // --no-sandbox is needed when running Chrome inside a container 14 | process.env.CI ? '--no-sandbox' : null, 15 | '--headless', 16 | '--disable-gpu', 17 | '--disable-dev-shm-usage', 18 | '--disable-software-rasterizer', 19 | '--mute-audio', 20 | '--remote-debugging-port=0', 21 | '--window-size=1440,900' 22 | ].filter(Boolean) 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /tests/acceptance/index-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupApplicationTest } from 'ember-qunit'; 3 | import { visit, currentURL } from '@ember/test-helpers'; 4 | import { click, fillIn, find, triggerEvent } from 'ember-native-dom-helpers'; 5 | import { timeout } from 'ember-scrollable/util/timeout'; 6 | import { THROTTLE_TIME_LESS_THAN_60_FPS_IN_MS } from 'ember-scrollable/components/ember-scrollable'; 7 | 8 | module('Acceptance | ember-scrollbar', function(hooks) { 9 | setupApplicationTest(hooks); 10 | 11 | function elementHeight(elem) { 12 | return elem.getBoundingClientRect().height; 13 | } 14 | 15 | test('vertical scrollbar', async function(assert) { 16 | await visit('/'); 17 | 18 | assert.equal(currentURL(), '/'); 19 | 20 | assert.ok(find('.vertical-demo .ember-scrollable'), 'vertical demo rendered'); 21 | assert.ok(find('.vertical-demo .ember-scrollable .drag-handle'), 'vertical demo handle rendered'); 22 | }); 23 | 24 | test('resizable scrollbar', async function(assert) { 25 | let elem; 26 | const toggleButtonSelector = '.resize-demo button'; 27 | await visit('/'); 28 | 29 | elem = find('.resize-demo .ember-scrollable'); 30 | 31 | assert.ok(find('.resize-demo .ember-scrollable'), 'resize demo rendered'); 32 | assert.ok(find('.resize-demo .ember-scrollable .drag-handle'), 'resize handle rendered'); 33 | assert.equal(elementHeight(elem), 200); 34 | 35 | await click(toggleButtonSelector); // make tall 36 | 37 | assert.equal(elementHeight(elem), 400); 38 | 39 | await click(toggleButtonSelector); // make small 40 | assert.equal(elementHeight(elem), 200); 41 | }); 42 | 43 | test('scrollTo and onScroll', async function(assert) { 44 | await visit('/'); 45 | let offset; 46 | 47 | assert.ok(find('.event-and-setter-demo .ember-scrollable'), 'scrolling demo rendered'); 48 | assert.ok(find('.event-and-setter-demo .ember-scrollable .drag-handle'), 'scrolling handle rendered'); 49 | 50 | offset = 123; 51 | 52 | await fillIn('#targetScrollOffset input', offset); 53 | await timeout(THROTTLE_TIME_LESS_THAN_60_FPS_IN_MS); 54 | assert.ok(find('#currentScrollOffset').innerText.indexOf(String(offset)) !== -1, 'scrollOffset matches'); 55 | }); 56 | 57 | 58 | test('When element resized from no-overflow => overflow => no-overflow, no scrollbar is visible on mouseover', async function(assert) { 59 | let scrollArea; 60 | let toggleButtonSelector = '.no-scrollbar-demo button'; 61 | await visit('/'); 62 | 63 | assert.ok(find('.no-scrollbar-demo .ember-scrollable'), 'resize demo rendered'); 64 | assert.ok(find('.no-scrollbar-demo .ember-scrollable .drag-handle:not(.visible)'), 'resize handle rendered, but not visible'); 65 | scrollArea = find('.no-scrollbar-demo .ember-scrollable .scrollable-content'); 66 | assert.equal(elementHeight(scrollArea), 18, 'there is no overflow as 18 < 200px'); 67 | 68 | await click(toggleButtonSelector); 69 | assert.equal(elementHeight(scrollArea), 494, 'there is overflow as 494 > 200px'); 70 | 71 | triggerEvent(scrollArea, 'mousemove'); 72 | assert.ok(find('.no-scrollbar-demo .ember-scrollable .drag-handle.visible'), 'handle shows up visible'); 73 | 74 | await click(toggleButtonSelector); 75 | assert.equal(elementHeight(scrollArea), 18); 76 | assert.ok(find('.no-scrollbar-demo .ember-scrollable .drag-handle:not(.visible)'), 'handle goes away when overflow is gone'); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | const App = Application.extend({ 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix, 9 | Resolver 10 | }); 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphasights/ember-scrollable/3ecf8a7943a72b65fc7c49e5d96ba5494ebab536/tests/dummy/app/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphasights/ember-scrollable/3ecf8a7943a72b65fc7c49e5d96ba5494ebab536/tests/dummy/app/controllers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/index.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | import Controller from '@ember/controller'; 4 | 5 | export default Controller.extend({ 6 | isShort: true, 7 | actions: { 8 | log(message) { 9 | console.log(message); 10 | }, 11 | toggleHeight() { 12 | this.toggleProperty('isShort'); 13 | } 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphasights/ember-scrollable/3ecf8a7943a72b65fc7c49e5d96ba5494ebab536/tests/dummy/app/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphasights/ember-scrollable/3ecf8a7943a72b65fc7c49e5d96ba5494ebab536/tests/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from './config/environment'; 3 | 4 | const Router = EmberRouter.extend({ 5 | location: config.locationType, 6 | rootURL: config.rootURL 7 | }); 8 | 9 | Router.map(function () { 10 | this.route('cookbook'); 11 | }); 12 | 13 | export default Router; 14 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphasights/ember-scrollable/3ecf8a7943a72b65fc7c49e5d96ba5494ebab536/tests/dummy/app/routes/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | } 5 | 6 | html { 7 | box-sizing: border-box; 8 | } 9 | 10 | *, 11 | *:before, 12 | *:after { 13 | box-sizing: inherit; 14 | } 15 | 16 | button { 17 | margin: 10px 0; 18 | } 19 | 20 | section { 21 | clear: both; 22 | margin: 0 10px 10px; 23 | overflow: auto; 24 | 25 | h1 { 26 | border-bottom: 1px solid #ededed; 27 | border-top: 1px solid #ededed; 28 | margin: 10px 0; 29 | padding: 10px; 30 | } 31 | } 32 | 33 | pre { 34 | margin: 0; 35 | } 36 | 37 | .output, 38 | .code { 39 | float: left; 40 | width: 50%; 41 | } 42 | 43 | .vertical-demo, 44 | .no-auto-hide-demo { 45 | p { 46 | margin: 0; 47 | padding: 10px; 48 | 49 | &:nth-child(odd) { 50 | background: #ededed; 51 | } 52 | } 53 | } 54 | 55 | .horizontal-demo .tse-scrollable { 56 | height: 110px; 57 | 58 | .scrollable-content { 59 | display: inline-block; 60 | height: 110px; 61 | white-space: nowrap; 62 | } 63 | 64 | ul { 65 | list-style-type: none; 66 | margin: 0; 67 | overflow: auto; 68 | padding-left: 0; 69 | 70 | > li { 71 | background: #666666; 72 | color: #ffffff; 73 | display: inline-block; 74 | font-size: 24px; 75 | height: 100px; 76 | line-height: 100px; 77 | margin-right: 10px; 78 | text-align: center; 79 | width: 100px; 80 | } 81 | } 82 | } 83 | 84 | .output { 85 | padding-right: 10px; 86 | } 87 | 88 | .is-short { 89 | height: 200px; 90 | } 91 | 92 | .is-tall { 93 | height: 400px; 94 | } 95 | 96 | .double-scroll-container { 97 | height: 400px; 98 | width: 300px; 99 | 100 | .scrollable-content { 101 | display: inline-block; 102 | } 103 | .wide-content { 104 | width: 500px; 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 | {{outlet}} 2 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphasights/ember-scrollable/3ecf8a7943a72b65fc7c49e5d96ba5494ebab536/tests/dummy/app/templates/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/templates/cookbook.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#link-to 'index'}} 3 | Home 4 | {{/link-to}} 5 | This page is for demonstrating more advanced / potentially real world uses of ember-scrollable. Please feel free to make additions if you see fit. 6 |
7 |
8 |

Synchronized Scrolling

9 | 10 |
11 | {{!-- BEGIN-SNIPPET vertical --}} 12 |
13 | {{#ember-scrollable scrollToY=scrollToY onScrollY=(action (mut scrollToY))}} 14 |

15 | {{#draggable-object content=this}} 16 | Drag Me! 17 | {{/draggable-object}} 18 |

19 |

Some more content

20 |

Some content

21 |

Some more content

22 |

Some content

23 |

Some more content

24 |

Some content

25 |

Some more content

26 |

Some content

27 |

Some more content

28 |

Some content

29 |

Some more content

30 |

Some content

31 |

Some more content

32 |

Some content

33 |

Some more content

34 |

Some content

35 |

Some more content

36 |

Some content

37 |

Some more content

38 | {{/ember-scrollable}} 39 |
40 | {{!-- END-SNIPPET --}} 41 |
42 | 43 |
44 | {{!-- BEGIN-SNIPPET vertical --}} 45 |
46 | {{#ember-scrollable scrollToY=scrollToY onScrollY=(action (mut scrollToY))}} 47 |

48 | {{#draggable-object content=this}} 49 | Drag Me! 50 | {{/draggable-object}} 51 |

52 |

Some more content

53 |

Some content

54 |

Some more content

55 |

Some content

56 |

Some more content

57 |

Some content

58 |

Some more content

59 |

Some content

60 |

Some more content

61 |

Some content

62 |

Some more content

63 |

Some content

64 |

Some more content

65 |

Some content

66 |

Some more content

67 |

Some content

68 |

Some more content

69 |

Some content

70 |

Some more content

71 | {{/ember-scrollable}} 72 |
73 | {{!-- END-SNIPPET --}} 74 |
75 |
76 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/index.hbs: -------------------------------------------------------------------------------- 1 |
2 | {{#link-to 'cookbook'}} 3 | Cookbook 4 | {{/link-to}} 5 |
6 |
7 |

Vertical

8 | 9 |
10 | {{!-- BEGIN-SNIPPET vertical --}} 11 |
12 | {{#ember-scrollable onScrolledToBottom=(action 'log' 'Bottom reached!')}} 13 |

Some content

14 |

Some more content

15 |

Some content

16 |

Some more content

17 |

Some content

18 |

Some more content

19 |

Some content

20 |

Some more content

21 |

Some content

22 |

Some more content

23 |

Some content

24 |

Some more content

25 |

Some content

26 |

Some more content

27 |

Some content

28 |

Some more content

29 |

Some content

30 |

Some more content

31 |

Some content

32 |

Some more content

33 | {{/ember-scrollable}} 34 |
35 | {{!-- END-SNIPPET --}} 36 |
37 | 38 |
39 | {{code-snippet name="vertical.hbs"}} 40 |
41 |
42 | 43 |
44 |

Horizontal

45 | 46 |
47 | {{!-- BEGIN-SNIPPET horizontal --}} 48 | {{#ember-scrollable horizontal=true}} 49 |
    50 |
  • 1
  • 51 |
  • 2
  • 52 |
  • 3
  • 53 |
  • 4
  • 54 |
  • 5
  • 55 |
  • 6
  • 56 |
  • 7
  • 57 |
  • 8
  • 58 |
  • 9
  • 59 |
  • 10
  • 60 |
61 | {{/ember-scrollable}} 62 | {{!-- END-SNIPPET --}} 63 |
64 | 65 |
66 | {{code-snippet name="horizontal.hbs"}} 67 |
68 |
69 | 70 |
71 |

No Auto Hide

72 | 73 |
74 | {{!-- BEGIN-SNIPPET no-auto-hide --}} 75 |
76 | {{#ember-scrollable autoHide=false}} 77 |

Some content

78 |

Some more content

79 |

Some content

80 |

Some more content

81 |

Some content

82 |

Some more content

83 |

Some content

84 |

Some more content

85 |

Some content

86 |

Some more content

87 | {{/ember-scrollable}} 88 |
89 | {{!-- END-SNIPPET --}} 90 |
91 | 92 |
93 | {{code-snippet name="no-auto-hide.hbs"}} 94 |
95 |
96 | 97 |
98 |

No Scroll

99 | 100 |
101 | {{!-- BEGIN-SNIPPET no-scrollbar-demo --}} 102 |
103 | {{#ember-scrollable}} 104 |

Some content

105 | {{#unless isShort}} 106 |

Some content

107 |

Some content

108 |

Some content

109 |

Some content

110 |

Some content

111 |

Some content

112 |

Some content

113 |

Some content

114 |

Some content

115 |

Some content

116 |

Some content

117 |

Some content

118 |

Some content

119 |

Some content

120 | {{/unless}} 121 | {{/ember-scrollable}} 122 |
123 | {{!-- END-SNIPPET --}} 124 |
125 | 126 |
127 | {{code-snippet name="no-scrollbar-demo.hbs"}} 128 |
129 |
130 | 131 |
132 |

Double Scroll

133 | 134 |
135 | {{!-- BEGIN-SNIPPET dual-scroll --}} 136 |
137 | {{#ember-scrollable horizontal=true vertical=true onScrolledToBottom=(action 'log' 'Bottom reached!')}} 138 |
139 |

Some content

140 |

Some more content

141 |

Some content

142 |

Some more content

143 |

Some content

144 |

Some more content

145 |

Some content

146 |

Some more content

147 |

Some content

148 |

Some more content

149 |

Some content

150 |

Some more content

151 |

Some content

152 |

Some more content

153 |

Some content

154 |

Some more content

155 |

Some content

156 |

Some more content

157 |

Some content

158 |

Some more content

159 |
160 | {{/ember-scrollable}} 161 |
162 | {{!-- END-SNIPPET --}} 163 |
164 | 165 |
166 | {{code-snippet name="double.hbs"}} 167 |
168 |
169 | 170 | 171 |
172 |

Auto resizing

173 | 174 |

The scrollbar automatically recalculates when the height of the parent container changes.

175 | 176 |
177 | {{!-- BEGIN-SNIPPET vertical --}} 178 |
179 | {{#ember-scrollable onScrolledToBottom=(action 'log' 'Bottom reached!')}} 180 |

Some content

181 |

Some more content

182 |

Some content

183 |

Some more content

184 |

Some content

185 |

Some more content

186 |

Some content

187 |

Some more content

188 |

Some content

189 |

Some more content

190 |

Some content

191 |

Some more content

192 |

Some content

193 |

Some more content

194 |

Some content

195 |

Some more content

196 |

Some content

197 |

Some more content

198 |

Some content

199 |

Some more content

200 | {{/ember-scrollable}} 201 |
202 | {{!-- END-SNIPPET --}} 203 |
204 | 205 |
206 | {{code-snippet name="vertical.hbs"}} 207 |
208 |
209 | 210 |
211 |

Scroll event and setting the offset

212 | 213 |
214 | {{!-- BEGIN-SNIPPET event-and-setter --}} 215 | 218 |
219 | 222 | 223 |
224 | 225 |
226 | {{#ember-scrollable 227 | scrollToY=targetScrollOffset 228 | onScrollY=(action (mut currentScrollOffset)) 229 | }} 230 |

Some content

231 |

Some more content

232 |

Some content

233 |

Some more content

234 |

Some content

235 |

Some more content

236 |

Some content

237 |

Some more content

238 |

Some content

239 |

Some more content

240 |

Some content

241 |

Some more content

242 |

Some content

243 |

Some more content

244 |

Some content

245 |

Some more content

246 |

Some content

247 |

Some more content

248 |

Some content

249 |

Some more content

250 | {{/ember-scrollable}} 251 |
252 | {{!-- END-SNIPPET --}} 253 |
254 | 255 |
256 | {{code-snippet name="event-and-setter.hbs"}} 257 |
258 |
259 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(environment) { 4 | let ENV = { 5 | modulePrefix: 'dummy', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. 'with-controller': true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false 17 | } 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | }, 24 | 25 | 'ember-composable-helpers': { 26 | only: ['concat'], 27 | except: [] 28 | } 29 | }; 30 | 31 | if (environment === 'development') { 32 | // ENV.APP.LOG_RESOLVER = true; 33 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 34 | // ENV.APP.LOG_TRANSITIONS = true; 35 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 36 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 37 | } 38 | 39 | if (environment === 'test') { 40 | // Testem prefers this... 41 | ENV.locationType = 'none'; 42 | 43 | // keep test console output quieter 44 | ENV.APP.LOG_ACTIVE_GENERATION = false; 45 | ENV.APP.LOG_VIEW_LOOKUPS = false; 46 | 47 | ENV.APP.rootElement = '#ember-testing'; 48 | ENV.APP.autoboot = false; 49 | } 50 | 51 | if (environment === 'production') { 52 | ENV.locationType = 'hash'; 53 | ENV.rootURL = '/ember-scrollable/'; 54 | } 55 | 56 | return ENV; 57 | }; 58 | -------------------------------------------------------------------------------- /tests/dummy/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "jquery-integration": true 3 | } 4 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions' 7 | ]; 8 | 9 | const isCI = !!process.env.CI; 10 | const isProduction = process.env.EMBER_ENV === 'production'; 11 | 12 | if (isCI || isProduction) { 13 | browsers.push('ie 11'); 14 | } 15 | 16 | module.exports = { 17 | browsers 18 | }; 19 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphasights/ember-scrollable/3ecf8a7943a72b65fc7c49e5d96ba5494ebab536/tests/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 29 | 30 | 31 | {{content-for "body"}} 32 | {{content-for "test-body"}} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {{content-for "body-footer"}} 41 | {{content-for "test-body-footer"}} 42 | 43 | 44 | -------------------------------------------------------------------------------- /tests/integration/components/ember-scrollbar-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render } from '@ember/test-helpers'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | import { click, find, triggerEvent } from 'ember-native-dom-helpers'; 6 | import jQuery from 'jquery'; 7 | 8 | 9 | module('Integration | Component | ember scrollbar', function(hooks) { 10 | setupRenderingTest(hooks); 11 | 12 | hooks.beforeEach(function() { 13 | this.actions = {}; 14 | this.send = (actionName, ...args) => this.actions[actionName].apply(this, args); 15 | }); 16 | 17 | const handleClass = '.drag-handle'; 18 | const barClass = '.tse-scrollbar'; 19 | 20 | function leftElementOffset(selector){ 21 | return find(selector).getBoundingClientRect().left; 22 | } 23 | 24 | function topElementOffset(selector){ 25 | return find(selector).getBoundingClientRect().top; 26 | } 27 | 28 | test('Horizontal: offset and size get routed properly', async function(assert) { 29 | assert.expect(4); 30 | 31 | this.setProperties({ 32 | size: 40, 33 | offset: 10 34 | }); 35 | await render(hbs` 36 |
37 |
38 | 39 | {{ember-scrollbar 40 | handleOffset=offset 41 | handleSize=size 42 | horizontal=true 43 | showHandle=true 44 | }} 45 | 46 |
47 |
`); 48 | assert.equal(jQuery(handleClass).position().left, this.get('offset')); 49 | assert.equal(Number.parseInt(jQuery(handleClass).css('width')), this.get('size')); 50 | assert.equal(this.get('element').querySelector(handleClass).offsetLeft, this.get('offset')); 51 | assert.equal(Number.parseInt(this.get('element').querySelector(handleClass).style.width), this.get('size')); 52 | }); 53 | 54 | test('Vertical: offset and size get routed properly', async function(assert) { 55 | assert.expect(4); 56 | 57 | this.setProperties({ 58 | size: 40, 59 | offset: 10 60 | }); 61 | await render(hbs` 62 |
63 |
64 | 65 | {{ember-scrollbar 66 | handleOffset=offset 67 | handleSize=size 68 | horizontal=false 69 | showHandle=true 70 | }} 71 | 72 |
73 |
`); 74 | 75 | assert.equal(jQuery(handleClass).position().top, this.get('offset')); 76 | assert.equal(Number.parseInt(jQuery(handleClass).css('height')), this.get('size')); 77 | assert.equal(jQuery(handleClass).position().top, this.get('offset')); 78 | assert.equal(Number.parseInt(jQuery(handleClass).css('height')), this.get('size')); 79 | }); 80 | 81 | 82 | test('click event on handle triggers startDrag, but not onJumpTo', async function(assert) { 83 | assert.expect(1); 84 | 85 | this.setProperties({ 86 | size: 40, 87 | offset: 10 88 | }); 89 | 90 | this.actions.onDragStart = function() { 91 | assert.ok(true); 92 | }; 93 | 94 | this.actions.onJumpTo = function() { 95 | assert.ok(false); 96 | }; 97 | 98 | await render(hbs` 99 |
100 |
101 | 102 | {{ember-scrollbar 103 | handleOffset=offset 104 | handleSize=size 105 | horizontal=true 106 | showHandle=true 107 | onDragStart=(action 'onDragStart') 108 | onJumpTo=(action 'onJumpTo') 109 | }} 110 | 111 |
112 |
`); 113 | 114 | click(handleClass); 115 | }); 116 | 117 | test('clicking on bar triggers onJumpTo and not startDrag', async function(assert) { 118 | assert.expect(1); 119 | 120 | this.setProperties({ 121 | size: 40, 122 | offset: 10 123 | }); 124 | 125 | this.actions.onDragStart = function() { 126 | assert.ok(false); 127 | }; 128 | 129 | this.actions.onJumpTo = function() { 130 | assert.ok(true); 131 | }; 132 | 133 | await render(hbs` 134 |
135 |
136 | 137 | {{ember-scrollbar 138 | handleOffset=offset 139 | handleSize=size 140 | horizontal=true 141 | showHandle=true 142 | onDragStart=(action 'onDragStart') 143 | onJumpTo=(action 'onJumpTo') 144 | }} 145 | 146 |
147 |
`); 148 | 149 | // WHEN we click on the bar and not the handle 150 | click(barClass); 151 | }); 152 | 153 | 154 | test('Horizontal: onJumpTo first argument is true when click to the left of handle', async function(assert) { 155 | assert.expect(1); 156 | 157 | const deltaX = 9; // some number less than 10, therefore `towardsAnchor` will be true 158 | this.setProperties({ 159 | size: 40, 160 | offset: 10 161 | }); 162 | this.actions.onJumpTo = function(towardsAnchor) { 163 | assert.ok(towardsAnchor, 'towardsAnchor should be true if going towards anchor'); 164 | }; 165 | 166 | await render(hbs` 167 |
168 |
169 | 170 | {{ember-scrollbar 171 | handleOffset=offset 172 | handleSize=size 173 | horizontal=true 174 | showHandle=true 175 | onJumpTo=(action 'onJumpTo') 176 | }} 177 | 178 |
179 |
`); 180 | 181 | // WHEN 182 | const clientX = leftElementOffset(barClass) + deltaX; 183 | click(barClass, { clientX }); 184 | 185 | }); 186 | 187 | test('Horizontal: onJumpTo first argument is false when click to the right of handle', async function(assert) { 188 | assert.expect(1); 189 | 190 | const deltaX = 30; // more than offset of 10 191 | this.setProperties({ 192 | size: 40, 193 | offset: 10 194 | }); 195 | this.actions.onJumpTo = function(towardsAnchor) { 196 | assert.notOk(towardsAnchor, 'towardsAnchor should be false if going away from anchor'); 197 | }; 198 | 199 | await render(hbs` 200 |
201 |
202 | 203 | {{ember-scrollbar 204 | handleOffset=offset 205 | handleSize=size 206 | horizontal=true 207 | showHandle=true 208 | onJumpTo=(action 'onJumpTo') 209 | }} 210 | 211 |
212 |
`); 213 | 214 | // WHEN 215 | const clientX = leftElementOffset(barClass) + deltaX; 216 | click(barClass, { clientX }); 217 | 218 | }); 219 | 220 | 221 | test('Vertical: onJumpTo first argument is true when click to the top of handle', async function(assert) { 222 | assert.expect(1); 223 | 224 | const deltaY = 2; // less than offset of 10 225 | this.setProperties({ 226 | size: 40, 227 | offset: 10 228 | }); 229 | this.actions.onJumpTo = function(towardsAnchor) { 230 | assert.ok(towardsAnchor, 'towardsAnchor should be true if going towards anchor'); 231 | }; 232 | 233 | await render(hbs` 234 |
235 |
236 | 237 | {{ember-scrollbar 238 | handleOffset=offset 239 | handleSize=size 240 | horizontal=false 241 | showHandle=true 242 | onJumpTo=(action 'onJumpTo') 243 | }} 244 | 245 |
246 |
`); 247 | 248 | // WHEN 249 | const clientY = topElementOffset(barClass) + deltaY; 250 | click(barClass, { clientY }); 251 | 252 | }); 253 | 254 | test('Vertical: onJumpTo first argument is false when clicking below the vertical handle', async function(assert) { 255 | assert.expect(1); 256 | 257 | const deltaY = 30; // more than offset of 10 258 | this.setProperties({ 259 | size: 40, 260 | offset: 10 261 | }); 262 | this.actions.onJumpTo = function(towardsAnchor) { 263 | assert.notOk(towardsAnchor, 'towardsAnchor should be false if going away from anchor'); 264 | }; 265 | 266 | await render(hbs` 267 |
268 |
269 | 270 | {{ember-scrollbar 271 | handleOffset=offset 272 | handleSize=size 273 | horizontal=false 274 | showHandle=true 275 | onJumpTo=(action 'onJumpTo') 276 | }} 277 | 278 |
279 |
`); 280 | 281 | // WHEN 282 | const clientY = topElementOffset(barClass) + deltaY; 283 | click(barClass, { clientY }); 284 | 285 | }); 286 | 287 | 288 | test('mouseup event triggers onDragEnd', async function(assert) { 289 | assert.expect(1); 290 | 291 | this.setProperties({ 292 | size: 40, 293 | offset: 10 294 | }); 295 | let called = false; 296 | this.actions.onDragEnd = function() { 297 | called = true; 298 | }; 299 | 300 | await render(hbs` 301 |
302 |
303 | 304 | {{ember-scrollbar 305 | handleOffset=offset 306 | handleSize=size 307 | horizontal=true 308 | showHandle=true 309 | onDragEnd=(action 'onDragEnd') 310 | }} 311 | 312 |
313 |
`); 314 | 315 | triggerEvent(handleClass, 'mouseup'); 316 | 317 | assert.ok(called); 318 | }); 319 | 320 | 321 | test('Vertical: onDrag is called when a change occurs when onDragging is true and mousemove event is triggered', async function(assert) { 322 | assert.expect(1); 323 | 324 | this.setProperties({ 325 | size: 40, 326 | offset: 10, 327 | isDragging: false 328 | }); 329 | 330 | this.actions.onDrag = function() { 331 | // THEN 332 | assert.ok(true); 333 | }; 334 | 335 | await render(hbs` 336 |
337 |
338 | 339 | {{ember-scrollbar 340 | handleOffset=offset 341 | handleSize=size 342 | horizontal=false 343 | dragOffset=30 344 | isDragging=isDragging 345 | showHandle=true 346 | onDrag=(action 'onDrag') 347 | }} 348 | 349 |
350 |
`); 351 | 352 | // WHEN 353 | this.set('isDragging', true); 354 | triggerEvent(window, 'mousemove', { pageX: 0, pageY: 0 }); 355 | }); 356 | 357 | test('Horizontal: onDrag is called when a change occurs when onDragging is true and mousemove event is triggered', async function(assert) { 358 | assert.expect(1); 359 | 360 | this.setProperties({ 361 | size: 40, 362 | offset: 10, 363 | isDragging: false 364 | }); 365 | this.actions.onDrag = function() { 366 | // THEN 367 | assert.ok(true); 368 | }; 369 | 370 | await render(hbs` 371 |
372 |
373 | 374 | {{ember-scrollbar 375 | handleOffset=offset 376 | handleSize=size 377 | horizontal=true 378 | dragOffset=30 379 | isDragging=isDragging 380 | showHandle=true 381 | onDrag=(action 'onDrag') 382 | }} 383 | 384 |
385 |
`); 386 | 387 | // WHEN 388 | this.set('isDragging', true); 389 | triggerEvent(window, 'mousemove', { pageX: 0, pageY: 0 }); 390 | }); 391 | 392 | // TODO verify that the drag percentage is calculated from mouse offset and drag offset and is a percentage between 0 and 1 of the scrollbar size 393 | }); 394 | -------------------------------------------------------------------------------- /tests/integration/components/scroll-content-element-test.js: -------------------------------------------------------------------------------- 1 | import { Promise } from 'rsvp'; 2 | import { module, test } from 'qunit'; 3 | import { setupRenderingTest } from 'ember-qunit'; 4 | import { settled } from '@ember/test-helpers'; 5 | import hbs from 'htmlbars-inline-precompile'; 6 | import jQuery from 'jquery'; 7 | 8 | module('Integration | Component | scroll content element', function(hooks) { 9 | setupRenderingTest(hooks); 10 | hooks.beforeEach(function() { 11 | this.actions = {}; 12 | }); 13 | 14 | const flushScrollAndWait = function() { 15 | return settled().then(() => { 16 | return new Promise((resolve) => { 17 | window.requestAnimationFrame(resolve); 18 | }); 19 | }); 20 | }; 21 | 22 | const cssSelector = '.tse-scroll-content'; 23 | 24 | const VERTICAL_TEMPLATE = hbs` 25 |
26 | {{#scroll-content-element 27 | height=10 28 | width=10 29 | scrollToY=scrollToY 30 | onScroll=(action 'scrolled') 31 | }} 32 |
33 | Content 34 |
35 | {{/scroll-content-element}} 36 |
37 | `; 38 | 39 | const HORIZONTAL_TEMPLATE = hbs` 40 |
41 | {{#scroll-content-element 42 | height=10 43 | width=10 44 | scrollToX=scrollToX 45 | onScroll=(action 'scrolled') 46 | }} 47 |
48 | Content 49 |
50 | {{/scroll-content-element}} 51 |
52 | `; 53 | 54 | 55 | function testInitialOffsetTriggersAScrollEvent(assert, template, scrollProp, direction) { 56 | assert.expect(2); 57 | const done = assert.async(); 58 | this.setProperties({ 59 | [scrollProp]: 5 60 | }); 61 | 62 | const scrolledCallArgs = []; 63 | this.actions.scrolled = function(e, scollOffset, scrollDir) { 64 | scrolledCallArgs.push([scollOffset, scrollDir]); 65 | }; 66 | 67 | // Template block usage: 68 | this.render(template); 69 | 70 | flushScrollAndWait().then(() => { 71 | assert.deepEqual(scrolledCallArgs[0], [5, direction]); 72 | assert.deepEqual(scrolledCallArgs.length, 1); 73 | done(); 74 | }); 75 | return done; 76 | } 77 | 78 | test('Vertical: Initial offset triggers a scroll event', function(assert) { 79 | return testInitialOffsetTriggersAScrollEvent.apply(this, [assert, VERTICAL_TEMPLATE, 'scrollToY', 'vertical']); 80 | }); 81 | 82 | test('Horizontal: Initial offset triggers a scroll event', function(assert) { 83 | return testInitialOffsetTriggersAScrollEvent.apply(this, [assert, HORIZONTAL_TEMPLATE, 'scrollToX', 'horizontal']); 84 | }); 85 | 86 | 87 | function testDefaultOffsetNoScrollEventTriggered(assert, template, scrollProp) { 88 | assert.expect(1); 89 | 90 | this.setProperties({ 91 | [scrollProp]: 0 92 | }); 93 | 94 | const scrolledCallArgs = []; 95 | this.actions.scrolled = function(e, scollOffset, scrollDir) { 96 | scrolledCallArgs.push([scollOffset, scrollDir]); 97 | }; 98 | 99 | // Template block usage: 100 | this.render(template); 101 | 102 | 103 | return settled().then(() => { 104 | assert.equal(scrolledCallArgs.length, 0); 105 | }); 106 | } 107 | 108 | test('Vertical: Default offset, no event triggered', function(assert) { 109 | return testDefaultOffsetNoScrollEventTriggered.apply(this, [assert, VERTICAL_TEMPLATE, 'scrollToY']); 110 | }); 111 | 112 | test('Horizontal: Default offset, no event triggered', function(assert) { 113 | return testDefaultOffsetNoScrollEventTriggered.apply(this, [assert, HORIZONTAL_TEMPLATE, 'scrollToX']); 114 | }); 115 | 116 | 117 | function testScrollOccursAndEventTriggersWithDirectionAndOffset(assert, template, scrollProp, direction) { 118 | assert.expect(3); 119 | 120 | const initialPosition = 10; 121 | const firstMovement = 5; 122 | const secondMovement = 25; 123 | 124 | const done = assert.async(); 125 | 126 | this.setProperties({ 127 | [scrollProp]: initialPosition 128 | }); 129 | 130 | const scrollMethod = direction === 'horizontal' ? 'scrollLeft' : 'scrollTop'; 131 | 132 | const scrolledCallArgs = []; 133 | this.actions.scrolled = function(e, scrollOffset, scrollDir) { 134 | scrolledCallArgs.push([scrollOffset, scrollDir]); 135 | }; 136 | 137 | // Template block usage: 138 | this.render(template); 139 | // Initial non-zero offset triggers a scroll event. 140 | 141 | flushScrollAndWait().then(() => { 142 | // WHEN the scrollX position has moved left to 0px 143 | jQuery(cssSelector)[scrollMethod](firstMovement); 144 | 145 | flushScrollAndWait().then(() => { 146 | // and then right to 25px; 147 | jQuery(cssSelector)[scrollMethod](secondMovement); 148 | flushScrollAndWait().then(() => { 149 | //THEN scroll gets called accordingly, and a horizontal scroll is detected 150 | assert.deepEqual(scrolledCallArgs[0], [initialPosition, direction]); 151 | assert.deepEqual(scrolledCallArgs[1], [firstMovement, direction]); 152 | assert.deepEqual(scrolledCallArgs[2], [secondMovement, direction]); 153 | done(); 154 | }); 155 | }); 156 | }); 157 | 158 | return done; 159 | } 160 | 161 | test('Vertical: scroll occurs and reports back the scrollTop value and direction', function(assert) { 162 | return testScrollOccursAndEventTriggersWithDirectionAndOffset.apply(this, [assert, VERTICAL_TEMPLATE, 'scrollToY', 'vertical']); 163 | }); 164 | 165 | test('Horizontal scroll occurs and reports back the scrollLeft value and direction', function(assert) { 166 | return testScrollOccursAndEventTriggersWithDirectionAndOffset.apply(this, [assert, HORIZONTAL_TEMPLATE, 'scrollToX', 'horizontal']); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from '../app'; 2 | import config from '../config/environment'; 3 | import { setApplication } from '@ember/test-helpers'; 4 | import { start } from 'ember-qunit'; 5 | 6 | setApplication(Application.create(config.APP)); 7 | 8 | start(); 9 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphasights/ember-scrollable/3ecf8a7943a72b65fc7c49e5d96ba5494ebab536/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/components/ember-scrollable-test.js: -------------------------------------------------------------------------------- 1 | import { run } from '@ember/runloop'; 2 | import { moduleForComponent, test } from 'ember-qunit'; 3 | 4 | moduleForComponent('ember-scrollable', 'Unit | Component | ember scrollable', { 5 | // Specify the other units that are required for this test 6 | needs: ['service:scrollbar-thickness'], 7 | unit: true 8 | }); 9 | 10 | test('ensure createScrollbar returns an array if destroyed', function(assert) { 11 | let component; 12 | run(() => { 13 | component = this.subject(); 14 | component.destroy(); 15 | }); 16 | 17 | let scrollbars = component.createScrollbar(); 18 | 19 | assert.equal(scrollbars.length, 0); 20 | }); 21 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphasights/ember-scrollable/3ecf8a7943a72b65fc7c49e5d96ba5494ebab536/vendor/.gitkeep --------------------------------------------------------------------------------