├── .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 | [](https://embadge.io/v1/badge.svg?start=2.8.0-lts) 3 | [](http://badge.fury.io/js/ember-scrollable) 4 | [](https://codeclimate.com/github/alphasights/ember-scrollable) 6 | [](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 = `
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 |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 |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 |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 |The scrollbar automatically recalculates when the height of the parent container changes.
175 | 176 |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 |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 |