├── .babelrc ├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .markdownlintrc ├── .npmignore ├── .nvmrc ├── CHANGELOG.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── configs ├── config.js └── rollup.config.js ├── dist ├── jquery.stickybits.js ├── jquery.stickybits.min.js ├── stickybits.es.js ├── stickybits.js ├── stickybits.min.js └── umbrella.stickybits.js ├── jsconfig.json ├── package.json ├── renovate.json ├── scripts └── acceptance.js ├── src ├── jquery.stickybits.js ├── stickybits.js └── umbrella.stickybits.js ├── tests ├── .eslintrc.json ├── acceptance │ ├── bottom │ │ ├── index.html │ │ └── test.js │ ├── cleanup │ │ ├── index.html │ │ └── test.js │ ├── monitoring │ │ ├── index.html │ │ └── test.js │ ├── multiple-sticky-classes │ │ ├── index.html │ │ └── test.js │ ├── multiple │ │ ├── index.html │ │ └── test.js │ ├── offset │ │ ├── index.html │ │ └── test.js │ ├── scrollTo │ │ ├── index.html │ │ └── test.js │ ├── stacked │ │ ├── index.html │ │ └── test.js │ ├── test.css │ ├── update │ │ ├── index.html │ │ └── test.js │ └── use-fixed │ │ ├── index.html │ │ └── test.js └── unit │ └── test.stickybits.js ├── types └── index.d.ts └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { 4 | "loose": true, 5 | "targets": { 6 | "browsers": [ 7 | "defaults", 8 | "ie >= 9" 9 | ] 10 | } 11 | }] 12 | ], 13 | } 14 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | defaults: &defaults 2 | working_directory: ~/code 3 | docker: 4 | - image: circleci/node:12 5 | environment: 6 | NPM_CONFIG_LOGLEVEL: error # make npm commands less noisy 7 | JOBS: max # https://gist.github.com/ralphtheninja/f7c45bdee00784b41fed 8 | 9 | restore_cache: &restore_cache 10 | restore_cache: 11 | keys: 12 | - code-{{ .Branch }}-{{ checksum ".nvmrc" }}-{{ checksum "yarn.lock" }} 13 | 14 | save_cache: &save_cache 15 | save_cache: 16 | key: code-{{ .Branch }}-{{ checksum ".nvmrc" }}-{{ checksum "yarn.lock" }} 17 | paths: 18 | - node_modules 19 | 20 | version: 2 21 | jobs: 22 | build: 23 | <<: *defaults 24 | steps: 25 | - checkout 26 | - *restore_cache 27 | - run: yarn 28 | - run: yarn build 29 | - *save_cache 30 | lint: 31 | <<: *defaults 32 | steps: 33 | - checkout 34 | - *restore_cache 35 | - run: yarn lint:ci 36 | test: 37 | <<: *defaults 38 | steps: 39 | - checkout 40 | - *restore_cache 41 | - run: yarn test 42 | 43 | workflows: 44 | version: 2 45 | all: 46 | jobs: 47 | - build 48 | - lint: 49 | requires: 50 | - build 51 | - test: 52 | requires: 53 | - build 54 | -------------------------------------------------------------------------------- /.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 | [*.js] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.hbs] 21 | insert_final_newline = false 22 | indent_style = space 23 | indent_size = 2 24 | 25 | [*.css] 26 | indent_style = space 27 | indent_size = 2 28 | 29 | [*.html] 30 | indent_style = space 31 | indent_size = 2 32 | 33 | [*.{diff,md}] 34 | trim_trailing_whitespace = false 35 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | tests/* 3 | node_modules/* 4 | coverage/* 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "dollarshaveclub", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "globals": { 8 | "document": true, 9 | "expect": true, 10 | "window": true 11 | }, 12 | "rules": { 13 | "indent": 1, 14 | "semi": [2, "never"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, available at [Contributor Covenant, Code of Conduct](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html) 71 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Requested Update 2 | 3 | ## Why Is This Update Needed? 4 | 5 | ## Are There Examples Of This Requested Update Elsewhere? 6 | 7 | > Read about references issues [here](https://help.github.com/articles/closing-issues-using-keywords/). Provide paragraph text responses to each header. 8 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Fixes 2 | 3 | - Fixes # 4 | 5 | ## Proposed Changes 6 | 7 | - Change 8 | 9 | ---- 10 | 11 | > Read about referenced issues [here](https://help.github.com/articles/closing-issues-using-keywords/). Replace words with this Pull Request's context. 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components/* 2 | node_modules/* 3 | .tags 4 | .tags1 5 | .DS_Store 6 | tests/.DS_Store 7 | .nyc_output/ 8 | coverage 9 | coverage/ 10 | coverage.lcov 11 | coverage_*/ 12 | tmp 13 | .idea 14 | npm-debug.log 15 | package-lock.json 16 | -------------------------------------------------------------------------------- /.markdownlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "MD007": { 4 | "indent": 2 5 | }, 6 | "line-length": false, 7 | "first-line-h1": false, 8 | "first-header-h1": false, 9 | "no-duplicate-header": false, 10 | "no-inline-html": false, 11 | "no-hard-tabs": false, 12 | "single-h1": false, 13 | "whitespace": false 14 | } 15 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [3.7.4](https://github.com/dollarshaveclub/stickybits/compare/3.7.3...3.7.4) (2020-02-18) 2 | 3 | 4 | ### fix 5 | 6 | * fixes issue with not window scroll issue (#660) ([f8de58c1dea01c56ecd4a6f50f03f90303eff6ca](https://github.com/dollarshaveclub/stickybits/commit/f8de58c1dea01c56ecd4a6f50f03f90303eff6ca)), closes [#660](https://github.com/dollarshaveclub/stickybits/issues/660) 7 | 8 | ### version 9 | 10 | * patch version bump (#659) ([07831c50f667e2926811dc08218f65b368d30efc](https://github.com/dollarshaveclub/stickybits/commit/07831c50f667e2926811dc08218f65b368d30efc)), closes [#659](https://github.com/dollarshaveclub/stickybits/issues/659) 11 | 12 | 13 | 14 | ## [3.7.3](https://github.com/dollarshaveclub/stickybits/compare/3.7.2...3.7.3) (2020-01-13) 15 | 16 | 17 | 18 | 19 | ## [3.7.2](https://github.com/dollarshaveclub/stickybits/compare/3.7.1...3.7.2) (2019-12-18) 20 | 21 | 22 | 23 | 24 | ## [3.7.1](https://github.com/dollarshaveclub/stickybits/compare/3.7.0...3.7.1) (2019-12-05) 25 | 26 | 27 | 28 | 29 | # [3.7.0](https://github.com/dollarshaveclub/stickybits/compare/3.6.8...3.7.0) (2019-11-08) 30 | 31 | 32 | 33 | 34 | ## [3.6.8](https://github.com/dollarshaveclub/stickybits/compare/3.6.7...3.6.8) (2019-11-05) 35 | 36 | 37 | 38 | 39 | ## [3.6.7](https://github.com/dollarshaveclub/stickybits/compare/3.6.6...3.6.7) (2019-08-26) 40 | 41 | 42 | 43 | 44 | ## [3.6.6](https://github.com/dollarshaveclub/stickybits/compare/3.6.5...3.6.6) (2019-05-17) 45 | 46 | 47 | 48 | 49 | ## [3.6.5](https://github.com/dollarshaveclub/stickybits/compare/3.6.4...3.6.5) (2019-03-31) 50 | 51 | 52 | 53 | 54 | ## [3.6.4](https://github.com/dollarshaveclub/stickybits/compare/3.6.3...3.6.4) (2019-03-03) 55 | 56 | 57 | 58 | 59 | ## [3.6.3](https://github.com/dollarshaveclub/stickybits/compare/3.6.2...3.6.3) (2019-03-03) 60 | 61 | 62 | 63 | 64 | ## [3.6.2](https://github.com/dollarshaveclub/stickybits/compare/3.6.1...3.6.2) (2019-02-20) 65 | 66 | 67 | 68 | 69 | ## [3.6.1](https://github.com/dollarshaveclub/stickybits/compare/3.6.0...3.6.1) (2018-12-27) 70 | 71 | 72 | 73 | 74 | # [3.6.0](https://github.com/dollarshaveclub/stickybits/compare/3.5.8...3.6.0) (2018-12-26) 75 | 76 | 77 | 78 | 79 | ## [3.5.8](https://github.com/dollarshaveclub/stickybits/compare/3.5.7...3.5.8) (2018-11-30) 80 | 81 | 82 | 83 | 84 | ## [3.5.7](https://github.com/dollarshaveclub/stickybits/compare/3.5.6...3.5.7) (2018-10-17) 85 | 86 | 87 | 88 | 89 | ## [3.5.6](https://github.com/dollarshaveclub/stickybits/compare/3.5.5...3.5.6) (2018-10-04) 90 | 91 | 92 | 93 | 94 | ## [3.5.5](https://github.com/dollarshaveclub/stickybits/compare/3.5.4...3.5.5) (2018-09-13) 95 | 96 | 97 | 98 | 99 | ## [3.5.4](https://github.com/dollarshaveclub/stickybits/compare/3.5.3...3.5.4) (2018-09-08) 100 | 101 | 102 | 103 | 104 | ## [3.5.3](https://github.com/dollarshaveclub/stickybits/compare/3.5.2...3.5.3) (2018-08-29) 105 | 106 | 107 | 108 | 109 | ## [3.5.2](https://github.com/dollarshaveclub/stickybits/compare/3.5.1...3.5.2) (2018-08-26) 110 | 111 | 112 | 113 | 114 | ## [3.5.1](https://github.com/dollarshaveclub/stickybits/compare/3.5.0...3.5.1) (2018-08-25) 115 | 116 | 117 | 118 | 119 | # [3.5.0](https://github.com/dollarshaveclub/stickybits/compare/3.4.1...3.5.0) (2018-08-25) 120 | 121 | 122 | 123 | 124 | ## [3.4.1](https://github.com/dollarshaveclub/stickybits/compare/3.4.0...3.4.1) (2018-06-30) 125 | 126 | 127 | 128 | 129 | # [3.4.0](https://github.com/dollarshaveclub/stickybits/compare/3.3.7...3.4.0) (2018-06-28) 130 | 131 | 132 | 133 | 134 | ## [3.3.7](https://github.com/dollarshaveclub/stickybits/compare/3.3.5...3.3.7) (2018-06-19) 135 | 136 | 137 | 138 | 139 | ## [3.3.5](https://github.com/dollarshaveclub/stickybits/compare/3.3.2...3.3.5) (2018-06-14) 140 | 141 | 142 | 143 | 144 | ## [3.3.2](https://github.com/dollarshaveclub/stickybits/compare/3.3.1...3.3.2) (2018-05-05) 145 | 146 | 147 | 148 | 149 | ## [3.3.1](https://github.com/dollarshaveclub/stickybits/compare/3.3.0...3.3.1) (2018-04-29) 150 | 151 | 152 | 153 | 154 | # [3.3.0](https://github.com/dollarshaveclub/stickybits/compare/3.2.4...3.3.0) (2018-04-25) 155 | 156 | 157 | 158 | 159 | ## [3.2.4](https://github.com/dollarshaveclub/stickybits/compare/3.2.3...3.2.4) (2018-04-18) 160 | 161 | 162 | ### tests 163 | 164 | * use npm ci (#279) ([5050a5727110e71b6991245f2e2dcaa5b583d039](https://github.com/dollarshaveclub/stickybits/commit/5050a5727110e71b6991245f2e2dcaa5b583d039)), closes [#279](https://github.com/dollarshaveclub/stickybits/issues/279) 165 | 166 | 167 | 168 | ## [3.2.3](https://github.com/dollarshaveclub/stickybits/compare/3.2.0...3.2.3) (2018-04-10) 169 | 170 | 171 | 172 | 173 | # [3.2.0](https://github.com/dollarshaveclub/stickybits/compare/3.1.1...3.2.0) (2018-03-08) 174 | 175 | 176 | 177 | 178 | ## [3.1.1](https://github.com/dollarshaveclub/stickybits/compare/3.1.0...3.1.1) (2018-02-26) 179 | 180 | 181 | 182 | 183 | # [3.1.0](https://github.com/dollarshaveclub/stickybits/compare/3.0.5...3.1.0) (2018-02-25) 184 | 185 | 186 | 187 | 188 | ## [3.0.5](https://github.com/dollarshaveclub/stickybits/compare/3.0.4...3.0.5) (2018-02-25) 189 | 190 | 191 | 192 | 193 | ## [3.0.4](https://github.com/dollarshaveclub/stickybits/compare/3.0.3...3.0.4) (2018-02-13) 194 | 195 | 196 | 197 | 198 | ## [3.0.3](https://github.com/dollarshaveclub/stickybits/compare/3.0.1...3.0.3) (2018-02-13) 199 | 200 | 201 | 202 | 203 | ## [3.0.1](https://github.com/dollarshaveclub/stickybits/compare/3.0.0...3.0.1) (2018-01-31) 204 | 205 | 206 | 207 | 208 | # [3.0.0](https://github.com/dollarshaveclub/stickybits/compare/2.1.2...3.0.0) (2018-01-31) 209 | 210 | 211 | 212 | 213 | ## [2.1.2](https://github.com/dollarshaveclub/stickybits/compare/2.1.1...2.1.2) (2018-01-24) 214 | 215 | 216 | 217 | 218 | ## [2.1.1](https://github.com/dollarshaveclub/stickybits/compare/2.0.13...2.1.1) (2018-01-16) 219 | 220 | 221 | 222 | 223 | ## [2.0.13](https://github.com/dollarshaveclub/stickybits/compare/2.0.10...2.0.13) (2017-12-06) 224 | 225 | 226 | 227 | 228 | ## [2.0.10](https://github.com/dollarshaveclub/stickybits/compare/2.0.9...2.0.10) (2017-11-09) 229 | 230 | 231 | 232 | 233 | ## [2.0.9](https://github.com/dollarshaveclub/stickybits/compare/2.0.8...2.0.9) (2017-10-31) 234 | 235 | 236 | 237 | 238 | ## [2.0.8](https://github.com/dollarshaveclub/stickybits/compare/2.0.7...2.0.8) (2017-10-21) 239 | 240 | 241 | 242 | 243 | ## [2.0.7](https://github.com/dollarshaveclub/stickybits/compare/2.0.6...2.0.7) (2017-10-20) 244 | 245 | 246 | 247 | 248 | ## [2.0.6](https://github.com/dollarshaveclub/stickybits/compare/2.0.4...2.0.6) (2017-10-17) 249 | 250 | 251 | 252 | 253 | ## [2.0.4](https://github.com/dollarshaveclub/stickybits/compare/2.0.3...2.0.4) (2017-10-10) 254 | 255 | 256 | 257 | 258 | ## [2.0.3](https://github.com/dollarshaveclub/stickybits/compare/2.0.2...2.0.3) (2017-10-06) 259 | 260 | 261 | 262 | 263 | ## [2.0.2](https://github.com/dollarshaveclub/stickybits/compare/2.0.1...2.0.2) (2017-10-02) 264 | 265 | 266 | 267 | 268 | ## [2.0.1](https://github.com/dollarshaveclub/stickybits/compare/1.5.3...2.0.1) (2017-09-29) 269 | 270 | 271 | 272 | 273 | ## [1.5.3](https://github.com/dollarshaveclub/stickybits/compare/1.5.2...1.5.3) (2017-08-06) 274 | 275 | 276 | 277 | 278 | ## [1.5.2](https://github.com/dollarshaveclub/stickybits/compare/1.5.0...1.5.2) (2017-08-02) 279 | 280 | 281 | 282 | 283 | # [1.5.0](https://github.com/dollarshaveclub/stickybits/compare/1.4.4...1.5.0) (2017-07-25) 284 | 285 | 286 | 287 | 288 | ## [1.4.4](https://github.com/dollarshaveclub/stickybits/compare/1.3.12...1.4.4) (2017-07-25) 289 | 290 | 291 | 292 | 293 | ## [1.3.12](https://github.com/dollarshaveclub/stickybits/compare/1.3.10...1.3.12) (2017-07-17) 294 | 295 | 296 | 297 | 298 | ## [1.3.10](https://github.com/dollarshaveclub/stickybits/compare/1.3.8...1.3.10) (2017-07-15) 299 | 300 | 301 | 302 | 303 | ## [1.3.8](https://github.com/dollarshaveclub/stickybits/compare/1.3.5...1.3.8) (2017-07-04) 304 | 305 | 306 | 307 | 308 | ## [1.2.10](https://github.com/dollarshaveclub/stickybits/compare/1.2.8...1.2.10) (2017-05-22) 309 | 310 | 311 | 312 | 313 | ## [1.2.8](https://github.com/dollarshaveclub/stickybits/compare/1.2.7...1.2.8) (2017-04-20) 314 | 315 | 316 | 317 | 318 | ## [1.2.7](https://github.com/dollarshaveclub/stickybits/compare/1.2.6...1.2.7) (2017-04-19) 319 | 320 | 321 | 322 | 323 | ## [1.2.6](https://github.com/dollarshaveclub/stickybits/compare/1.2.5...1.2.6) (2017-04-19) 324 | 325 | 326 | 327 | 328 | ## [1.2.5](https://github.com/dollarshaveclub/stickybits/compare/1.2.4...1.2.5) (2017-04-19) 329 | 330 | 331 | 332 | 333 | ## [1.2.4](https://github.com/dollarshaveclub/stickybits/compare/1.2.3...1.2.4) (2017-04-19) 334 | 335 | 336 | 337 | 338 | ## [1.2.3](https://github.com/dollarshaveclub/stickybits/compare/1.1.3...1.2.3) (2017-04-19) 339 | 340 | 341 | 342 | 343 | ## [1.1.3](https://github.com/dollarshaveclub/stickybits/compare/1.1.2...1.1.3) (2017-04-07) 344 | 345 | 346 | 347 | 348 | ## [1.1.2](https://github.com/dollarshaveclub/stickybits/compare/1.0.2...1.1.2) (2017-04-01) 349 | 350 | 351 | 352 | 353 | ## [1.0.2](https://github.com/dollarshaveclub/stickybits/compare/1.0.1...1.0.2) (2017-03-28) 354 | 355 | 356 | 357 | 358 | ## [1.0.1](https://github.com/dollarshaveclub/stickybits/compare/1.0.0...1.0.1) (2017-03-24) 359 | 360 | 361 | 362 | 363 | # [1.0.0](https://github.com/dollarshaveclub/stickybits/compare/0.0.4...1.0.0) (2017-03-23) 364 | 365 | 366 | 367 | 368 | ## [0.0.4](https://github.com/dollarshaveclub/stickybits/compare/0.0.3...0.0.4) (2017-02-27) 369 | 370 | 371 | 372 | 373 | ## [0.0.3](https://github.com/dollarshaveclub/stickybits/compare/0.0.2...0.0.3) (2017-02-22) 374 | 375 | 376 | 377 | 378 | ## 0.0.2 (2017-02-22) 379 | 380 | 381 | 382 | 383 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Stickybits owners 2 | # ---- 3 | * @yowainwright 4 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, available at [Contributor Covenant, Code of Conduct](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html) 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Dollar Shave Club, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️

2 |

3 | 4 | This software is maintained under a new repository located at yowainwright/stickybits 5 | 6 |

7 |

⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️

8 | 9 | *** 10 | 11 |

12 | StickyBits banner 13 |

14 |

Make things get sticky …in a good way

15 |
16 |

17 | 18 | CircleCI 19 | 20 | 21 | npm version 22 | 23 | 24 | unpkg 25 | 26 | 27 | Greenkeeper 28 | 29 | 30 | codecov 31 | 32 | 33 | Share on Twitter 34 | 35 |

36 |
37 |

StickyBits 🍬

38 | 39 | > Stickybits is a lightweight alternative to `position: sticky` polyfills. It works perfectly for things like sticky headers. 40 | 41 | #### Stickybits is awesome because 42 | 43 | - it can add a CSS Sticky Class (`.js-is-sticky`) when [position: sticky](http://caniuse.com/#search=sticky) elements become active and a CSS Stuck Class (`.js-is-stuck`) when they become stuck. See [useStickyClasses](#feature). 44 | - it loosely mimics [position: sticky](http://caniuse.com/#search=sticky) to consistently stick elements vertically across multiple platforms 45 | - it does not have the _jumpiness_ that plugins that are built around `position: fixed` have because it tries to support `position: sticky` first. 46 | - in its simplest use case, a `scroll` event listener will not be used if `position: sticky` is supported. 47 | - it is super simple & lightweight 48 | - it provides a [wiki](https://github.com/dollarshaveclub/stickybits/wiki) that digs deeply into fundementals of `position: sticky` and `position: fixed` and it works with them. 49 | 50 | ---- 51 | 52 |

53 | Installation   54 | Setup   55 | Usage   56 | Feature   57 | Options   58 | Examples   59 | Debugging   60 | Notes   61 | Contributing   62 | Wiki 63 |

64 | 65 | ---- 66 | 67 | ## Installing from a package manager 68 | 69 | yarn 70 | 71 | ```sh 72 | 73 | yarn add stickybits 74 | 75 | ``` 76 | 77 | npm 78 | 79 | ```sh 80 | 81 | npm i stickybits 82 | 83 | ``` 84 | 85 | ## Setup 86 | 87 | Add **dist/stickybits.min.js** 88 | 89 | Or as a module with `import stickybits from 'stickybits'` 90 | 91 |

Basic Usage

92 | 93 | ```javascript 94 | 95 | stickybits('selector'); 96 | 97 | ``` 98 | 99 | ### By default, a selected stickybits element will 100 | 101 | - Stick elements to the top of the viewport when scrolled to vertically. 102 | - Stick elements at the bottom of their parent element when scrolled past. 103 | 104 | ---- 105 | 106 | **Key Note:** Stickybits expects and works best when the element that will become sticky is wrapped within a parent element that defines when the element starts being sticky and stops being sticky. See below for visual reference. 107 | 108 | ```html 109 | 110 |
111 | 112 |
113 | 114 | ``` 115 | 116 | ---- 117 | 118 |

useStickyClasses Feature

119 | 120 | > Stickybits allows customers to add CSS to elements when they become sticky and when they become stuck at the bottom of their parent element. 121 | 122 | By default, if `position: sticky` is supported, StickyBits will exit allowing the browser to manage stickiness and avoid adding a `scroll` event listener. 123 | 124 | If the `useStickyClasses` argument is set to `true` then even if a browser supports `position: sticky`, StickyBits will still add a `scroll` event listener to [add and remove sticky CSS Classes](#notes). This option is available so that CSS styles can use when StickyBits elements become sticky or stuck at the bottom of their parent. 125 | 126 | To provide more feature richness to the Stickybits experience, a `.js-is-sticky--change` CSS class is added after the Stickybit element is sticky for a certain duration of scroll. By default this duration of scrolling is the height of the Stickybit element. The scroll duration for when `.js-is-sticky--change` is added can be modified by providing a number for `customStickyChangeNumber` option. 127 | 128 | To use `useStickyClasses`: 129 | 130 | ```javascript 131 | 132 | stickybits('selector', {useStickyClasses: true}); 133 | 134 | ``` 135 | 136 | Then, in css you can do: 137 | 138 | ```css 139 | 140 | .some-sticky-element.js-is-sticky { 141 | background-color: red; 142 | } 143 | .some-sticky-element.js-is-sticky--change { 144 | height: 50px; 145 | } 146 | .some-sticky-element.js-is-stuck { 147 | background-color: green; 148 | } 149 | 150 | ``` 151 | 152 | View [add css classes](#notes) for more information on StickyBits CSS Classes. 153 | 154 | ## Options 155 | 156 | ### Vertical Layout Position 157 | 158 | By default, a StickyBits element will stick to the top of the viewport when vertically scrolled to. 159 | 160 | Stickybits loosely works for `bottom` positioning as well. 161 | 162 | To have a StickyBits element stick to the `bottom`: 163 | 164 | ```javascript 165 | 166 | stickybits('selector', {verticalPosition: 'bottom'}); 167 | 168 | ``` 169 | 170 | ### Custom Scroll Element 171 | 172 | By default, if Stickybits uses `window` scrolling to define Sticky Elements. An element besides `window` can be used if `window` is `undefined` by selecting the desired scrolling element with the `scrollEl` option. For more custom sticky featuring, the `scrollEl` option can be used. However, those implementations require the implementing developers support. 173 | 174 | To have Stickybit use an selector besides `window`: 175 | 176 | ```javascript 177 | 178 | stickybits('selector', {scrollEl: 'an-id'}); 179 | 180 | ``` 181 | 182 | ### StickyBit Sticky Offset 183 | 184 | By default, a StickyBits element will have a `0px` sticky layout top offset. This means that the element will stick flush to the top of the viewport. 185 | 186 | To have a StickyBits element stick with a `20px` offset to its vertical layout position: 187 | 188 | ```javascript 189 | 190 | stickybits('selector', {stickyBitStickyOffset: 20}); 191 | 192 | ``` 193 | 194 | ### StickyBits Cleanup 195 | 196 | To _cleanup_ an instance of Stickybits: 197 | 198 | ```javascript 199 | 200 | const stickybitsInstancetoBeCleanedup = stickybits('selector'); 201 | stickybitsInstancetoBeCleanedup.cleanup(); 202 | 203 | ``` 204 | 205 | ### StickyBits Update 206 | 207 | To _update_ the calculations of an instance of Stickybits: 208 | 209 | ```javascript 210 | 211 | const stickybitsInstancetoBeUpdated = stickybits('selector'); 212 | stickybitsInstancetoBeUpdated.update(); 213 | 214 | ``` 215 | 216 | Re-calculates each Stickybits instance's offsets (stickyStart, stickyStop). 217 | If the Stickybits implementer would like re-calculate offsets when the DOM window is resized or when the url changes. `.update()` can be invoked within an event listener. 218 | 219 | ```javascript 220 | const stickybitsInstancetoBeUpdated = stickybits('selector'); 221 | stickybitsInstancetoBeUpdated.update({ stickyBitStickyOffset: 20 }); 222 | 223 | ``` 224 | 225 | #### More Stickybits Update Examples 226 | 227 | ```javascript 228 | 229 | // when the window is resized 230 | const stickybitsInstancetoBeUpdated = stickybits('selector'); 231 | window.addEventListener('resize', () => { 232 | stickybitsInstancetoBeUpdated.update(); 233 | }); 234 | // when the url hash changes 235 | window.addEventListener('hashchange', () => { 236 | stickybitsInstancetoBeUpdated.update(); 237 | }); 238 | 239 | ``` 240 | 241 | **Note:** `.update` does not re-initialize classnames or pre-set calculations. Perhaps the update value can help you with that (see the paragraph below). 242 | 243 | #### StickBits Update Props 244 | 245 | Props can be updated to each instance by passing then into the `.update` function as an object. 246 | 247 | ```javascript 248 | 249 | // .update({ someProp: somePropValue }) 250 | const stickybitsInstancetoBeUpdated = stickybits('selector'); 251 | stickybitsInstancetoBeUpdated.update({ stickyBitStickyOffset: 20 }); 252 | 253 | ``` 254 | 255 | ### StickyBits NoStyles 256 | 257 | To use StickyBits without inline styles except for `position: sticky` or `position: fixed`: 258 | 259 | ```javascript 260 | 261 | stickybits('selector', {noStyles: true}); 262 | 263 | ``` 264 | 265 | ### StickyBits Custom CSS Classes 266 | 267 | To use custom CSS classes for Stickybits, add the appropriate properties and values. 268 | 269 | parentClass: 270 | 271 | ```javascript 272 | 273 | stickybits('selector', {parentClass: 'new-parent-classname'}); 274 | 275 | ``` 276 | 277 | stickyClass: 278 | 279 | ```javascript 280 | 281 | stickybits('selector', {stickyClass: 'new-sticky-classname'}); 282 | 283 | ``` 284 | 285 | stuckClass: 286 | 287 | ```javascript 288 | 289 | stickybits('selector', {stuckClass: 'new-stuck-classname'}); 290 | 291 | ``` 292 | 293 | ### StickyBits useFixed 294 | 295 | To not use `position: sticky` **ever**, add the following key value to a stickybit initalization. 296 | 297 | parentClass: 298 | 299 | ```javascript 300 | 301 | stickybits('selector', {useFixed: true}); 302 | 303 | ``` 304 | 305 | To change all of the CSS classes 306 | 307 | ```javascript 308 | 309 | stickybits('selector', { 310 | parentClass: 'new-parent-classname', 311 | stickyClass: 'new-sticky-classname', 312 | stuckClass: 'new-stuck-classname', 313 | stickyChangeClass: 'new-sticky-change-classname' 314 | }); 315 | 316 | ``` 317 | 318 | ### StickyBits useGetBoundingClientRect 319 | 320 | To not use `offsetTop` provide the optional boolean `useGetBoundingClientRect`. 321 | This feature is optimal when dealing with things like CSS calc which can throw off `offsetTop` calculations. Read more about this functionality [here](https://stanko.github.io/javascript-get-element-offset/). 322 | 323 | ```javascript 324 | 325 | stickybits('selector', {useGetBoundingClientRect: true}); 326 | 327 | ``` 328 | 329 | \* For jQuery and Zepto support, read the jQuery notes [below](#jquery). 330 | 331 | ### StickyBits applyStyle 332 | 333 | If you want to take control of how styles and classes are applied to elements 334 | provide a function `applyStyle`. This is useful for example if you want to 335 | integrate with a framework or view library and want to delegate DOM 336 | manipulations to it. 337 | 338 | ``` javascript 339 | stickybits('selector', { 340 | applyStyle: ({ classes, styles }, instance) => { 341 | // Apply styles and classes to your element 342 | } 343 | }); 344 | ``` 345 | 346 | ## Examples 347 | 348 | - [Basic Usage](https://codepen.io/yowainwright/pen/QdedaO) 349 | - [Basic usage but with multiple instances of the same selector](https://codepen.io/yowainwright/pen/VPogaX) 350 | - [Custom vertical top offset](https://codepen.io/yowainwright/pen/YQZPqR) ie: `stickybits('selector', {stickyBitStickyOffset: 20})` 351 | - [UseStickyClasses](http://codepen.io/yowainwright/pen/NpzPGR) ie: `stickybits('selector', {useStickyClasses: true})` 352 | - [Clean Stickybits](https://codepen.io/yowainwright/pen/gRgdep) ie: `const stickything = stickybits('selector'); stickything.cleanup();` 353 | - [Update](https://codepen.io/yowainwright/pen/JZOajV/) ie: `const stickything = stickybits('selector') stickything.update()` 354 | - [Update props](https://codepen.io/yowainwright/pen/EGvjYg) ie: `const stickything = stickybits('selector') stickything.update({ stickyBitStickyOffset: 20 })` 355 | - [Use Fixed](https://codepen.io/yowainwright/pen/mKMzNb/) ie: `const stickything = stickybits('selector', {useFixed: true})` 356 | - [Use GetBoundingClientRect](https://codepen.io/yowainwright/pen/PdZGMQ) ie: `const stickything = stickybits('selector', {useGetBoundingClientRect: true})` 357 | - [As a jQuery or Zepto Plugin](http://codepen.io/yowainwright/pen/57b852e88a644e9d919f843dc7b3b5f1) ie: `$('selector').stickybits()` 358 | 359 | ---- 360 | 361 | ### Extended Examples 362 | 363 | - [Custom vertical position (at bottom of parent element)](http://codepen.io/yowainwright/pen/e32cc7b82907ed9715a0a482ffa57596) 364 | - [NoStyles Stickybits](https://codepen.io/yowainwright/pen/YrQpQj) ie: `stickybits('selector', {noStyles: true});` 365 | - [With Custom Classes](https://codepen.io/yowainwright/pen/rGwWyW/) ie: `stickybits('selector', {parentClass: 'js-parent-test'})` 366 | - [ScrollEl](https://codepen.io/yowainwright/pen/EXzJeb) ie: `stickybits('selector', {scrollEl: 'a-custom-scroll-el'})` or `stickybits('selector', {scrollEl: element})` 367 | - If you have Stickybits examples, please submit an [issue](https://github.com/dollarshaveclub/stickybits/issues) with a link to it. 🙏 368 | 369 | ---- 370 | 371 | Have another example or question? Feel free to [comment](https://github.com/dollarshaveclub/stickybits/issues). 🙌 372 | 373 | ## Notes 374 | 375 | ### CSS Class Usage 376 | 377 | 3 CSS classes will be added and removed by Stickybits if `position: sticky` is not supported or if the `useStickyClasses: true` option is added to the plugin call. These Classes can be modified as desired. See the _With Custom Classes_ example above. 378 | 379 | - `js-is-sticky` if the selected element is sticky. 380 | - `js-is-stuck` if the selected element is stopped at the bottom of its parent. 381 | - `js-stickybit-parent` so that styles can easily be added to the parent of a Stickybits element 382 | 383 | ### Not a Polyfill 384 | 385 | Stickybits is not a Shim or Polyfill for `position: sticky` because full support would require more code. This plugin makes elements vertically sticky very similarly to `position: fixed` but in a `sticky` sort of way. Read more about position sticky [here](https://developer.mozilla.org/en-US/docs/Web/CSS/position) or follow its browser implementation [here](http://caniuse.com/#search=sticky). 386 | 387 | Stickybits is a no dependency JavaScript plugin. It provides the smallest API possible in both features and kb size to deliver working sticky elements. This means that opinionated featuring is left out as much as possible and that it works with minimal effort in Frameworks. 388 | 389 | ### CSS when `position: sticky` is not supported 390 | 391 | **Sticky Start and Sticky Stop:** Because Stickybits is minimal, when `position: sticky` is not supported Stickybits will use `position: fixed` which is relative to the browser window. If the StickyBits parent element has a height recognized by the browser, Stickybits will take care of the sticky top and sticky bottom invocation. If the parent's height is not recognized by the browser there will be issues. 392 | 393 | **Left and Right Positioning:** With `position: fixed` the Stickybit element will work relative to the browser window by default. To work with this issue, there are several options. Some are noted [here](https://github.com/dollarshaveclub/stickybits/issues/66). More solutions to come! 394 | 395 | ### jQuery and Zepto Usage 396 | 397 | Basic 398 | 399 | ```javascript 400 | 401 | $('selector').stickybits(); 402 | 403 | ``` 404 | 405 | With `scrollEl` 406 | 407 | ```javascript 408 | 409 | $('selector').stickybits({scrollEl: '#scrollEl'}); 410 | 411 | // or 412 | 413 | const el = document.querySelector('#scrollEl'); 414 | $('selector').stickybits({scrollEl: el}); 415 | 416 | ``` 417 | 418 | With `.update` 419 | 420 | ```javascript 421 | 422 | const instance = $('selector').stickybits(); 423 | instance.update(); 424 | 425 | ``` 426 | 427 | With `useStickyClasses` 428 | 429 | ```javascript 430 | 431 | $('selector').stickybits({useStickyClasses: true}); 432 | 433 | ``` 434 | 435 | With `verticalPosition` 436 | 437 | ```javascript 438 | 439 | $('selector').stickybits({verticalPosition: 'bottom'}); 440 | 441 | ``` 442 | 443 | With `stickyBitStickyOffset` 444 | 445 | ```javascript 446 | 447 | $('selector').stickybits({stickyBitStickyOffset: 20}); 448 | 449 | ``` 450 | 451 | ## Debugging 452 | 453 | Stickybits 2.0 provides the same API but with more debugging feedback. 454 | 455 | To view the Stickybits API in it's simpliest form: 456 | 457 | ```javascript 458 | 459 | const stickybit = stickybits('a selection'); 460 | console.log(stickybit); 461 | 462 | ``` 463 | 464 | For more debugging and managing Stickybits, view the [wiki](https://github.com/dollarshaveclub/stickybits/wiki). 465 | 466 | ---- 467 | 468 | ### Utility properties 469 | 470 | Stickybits provides both `version` and `userAgent` properties which were added to offer insight into the browser and Stickybits. 471 | 472 | These utility properties can be accessed as direct child properties of the instantiated Stickybits item. 473 | 474 | ```javascript 475 | 476 | const stickybit = stickybits('a selection') 477 | stickybit.version // will show the version of stickybits being used 478 | stickybit.userAgent // will show which userAgent stickybits is detecting 479 | 480 | ``` 481 | 482 | ## Browser Compatibility 483 | 484 | Stickybits works in all modern browsers including Internet Explorer 9 and above. Please file and [issue](https://github.com/dollarshaveclub/stickybits/issues) with browser compatibility quirks. 485 | 486 | ## Contributing 487 | 488 | Please contribute to Stickybits by filing an [issue](https://github.com/dollarshaveclub/stickybits/issues), responding to [issues](https://github.com/dollarshaveclub/stickybits/issues), adding to the [wiki](https://github.com/dollarshaveclub/stickybits/wiki), or reaching out socially—etc. 489 | 490 | Stickybits is a utility. It may often not be needed! With shared understanding of `position: sticky` and `position: fixed` along with product awareness, Stickybits can improve as can a shared understanding of the "sticky element issue". Is this paragraph over-reaching? Yes! Help improve it. 491 | 492 | ## Thanks 493 | 494 | This plugin was heavily influenced by [Filament Group](https://www.filamentgroup.com/)'s awesome [Fixed-sticky](https://github.com/filamentgroup/fixed-sticky) jQuery plugin. Thanks to them for getting my mind going on this a while back. Thanks to [Peloton Cycle](https://github.com/pelotoncycle/)'s [Frame Throttle](https://github.com/pelotoncycle/frame-throttle) for an insightful solve for optimizing `frame throttling`. 495 | 496 | Architecture discussions and Pull Request help has been provided by [Jacob Kelley](https://github.com/jakiestfu), [Brian Gonzalez](https://github.com/briangonzalez/), and [Matt Young](https://github.com/someguynamedmatt). It is much appreciated! 497 | 498 | ---- 499 | 500 | [Created](https://github.com/yowainwright/sticky-bits) and maintained by [Jeff Wainwright](https://github.com/yowainwright) with [Dollar Shave Club Engineering](https://github.com/dollarshaveclub). 501 | 502 | ### More great contributors 503 | 504 | - [Frank Merema](https://github.com/FrankMerema) 505 | - [Daniel Ruf](https://github.com/DanielRuf) 506 | - [Nestor Vera](https://github.com/hacknug) 507 | - [K. Vanberendonck](https://github.com/donkeybonks) 508 | - [Alexey Ukolov](https://github.com/alexey-m-ukolov) 509 | - [Martin Barksten](https://github.com/mbark) 510 | -------------------------------------------------------------------------------- /configs/config.js: -------------------------------------------------------------------------------- 1 | import { 2 | author, 3 | description, 4 | homepage, 5 | license, 6 | name, 7 | version, 8 | } from '../package.json' 9 | 10 | const loose = true 11 | 12 | const babelSetup = { 13 | babelrc: false, 14 | presets: [['@babel/preset-env', { modules: false, loose }]], 15 | plugins: [['@babel/plugin-proposal-class-properties', { loose }]], 16 | exclude: 'node_modules/**', 17 | } 18 | 19 | const uglifyOutput = { 20 | output: { 21 | comments: (node, comment) => { 22 | const text = comment.value 23 | const type = comment.type 24 | if (type === 'comment2') { 25 | // multiline comment 26 | return /@preserve|@license|@cc_on/i.test(text) 27 | } 28 | }, 29 | }, 30 | } 31 | 32 | const banner = `/** 33 | ${name} - ${description} 34 | @version v${version} 35 | @link ${homepage} 36 | @author ${author} 37 | @license ${license} 38 | **/` 39 | 40 | export { 41 | babelSetup, 42 | banner, 43 | name, 44 | uglifyOutput, 45 | version, 46 | } 47 | -------------------------------------------------------------------------------- /configs/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { 2 | babelSetup, 3 | banner, 4 | name, 5 | uglifyOutput, 6 | version, 7 | } from '../configs/config' 8 | import babel from 'rollup-plugin-babel' 9 | import replace from 'rollup-plugin-replace' 10 | import { uglify } from 'rollup-plugin-uglify' 11 | import pkg from '../package.json' 12 | 13 | const ensureArray = maybeArr => Array.isArray(maybeArr) ? maybeArr : [maybeArr] 14 | 15 | const createConfig = ({ input, output, env } = {}) => { 16 | const plugins = [ 17 | babel(babelSetup), 18 | replace({ 'VERSION': JSON.stringify(version).replace(/"/g, '') }), 19 | ] 20 | 21 | if (env === 'production') plugins.push(uglify(uglifyOutput)) 22 | 23 | return { 24 | input, 25 | plugins, 26 | output: ensureArray(output).map(format => 27 | Object.assign( 28 | {}, 29 | format, 30 | { 31 | banner, 32 | name, 33 | }, 34 | ), 35 | ), 36 | } 37 | } 38 | 39 | export default [ 40 | createConfig({ 41 | input: 'src/stickybits.js', 42 | output: [ 43 | { file: pkg.main, format: 'umd' }, 44 | { file: pkg.module, format: 'es' }, 45 | ], 46 | }), 47 | createConfig({ 48 | input: 'src/stickybits.js', 49 | output: { 50 | file: 'dist/stickybits.min.js', 51 | format: 'umd', 52 | }, 53 | env: 'production', 54 | }), 55 | createConfig({ 56 | input: 'src/jquery.stickybits.js', 57 | output: { 58 | file: 'dist/jquery.stickybits.min.js', 59 | format: 'umd', 60 | }, 61 | env: 'production', 62 | }), 63 | createConfig({ 64 | input: 'src/umbrella.stickybits.js', 65 | output: { 66 | file: 'dist/umbrella.stickybits.js', 67 | format: 'umd', 68 | }, 69 | }), 70 | createConfig({ 71 | input: 'src/jquery.stickybits.js', 72 | output: { 73 | file: 'dist/jquery.stickybits.js', 74 | format: 'umd', 75 | }, 76 | }), 77 | ] 78 | -------------------------------------------------------------------------------- /dist/jquery.stickybits.js: -------------------------------------------------------------------------------- 1 | /** 2 | stickybits - Stickybits is a lightweight alternative to `position: sticky` polyfills 3 | @version v3.7.4 4 | @link https://github.com/dollarshaveclub/stickybits#readme 5 | @author Jeff Wainwright (https://jeffry.in) 6 | @license MIT 7 | **/ 8 | (function (factory) { 9 | typeof define === 'function' && define.amd ? define(factory) : 10 | factory(); 11 | }((function () { 'use strict'; 12 | 13 | function _extends() { 14 | _extends = Object.assign || function (target) { 15 | for (var i = 1; i < arguments.length; i++) { 16 | var source = arguments[i]; 17 | 18 | for (var key in source) { 19 | if (Object.prototype.hasOwnProperty.call(source, key)) { 20 | target[key] = source[key]; 21 | } 22 | } 23 | } 24 | 25 | return target; 26 | }; 27 | 28 | return _extends.apply(this, arguments); 29 | } 30 | 31 | /* 32 | STICKYBITS 💉 33 | -------- 34 | > a lightweight alternative to `position: sticky` polyfills 🍬 35 | -------- 36 | - each method is documented above it our view the readme 37 | - Stickybits does not manage polymorphic functionality (position like properties) 38 | * polymorphic functionality: (in the context of describing Stickybits) 39 | means making things like `position: sticky` be loosely supported with position fixed. 40 | It also means that features like `useStickyClasses` takes on styles like `position: fixed`. 41 | -------- 42 | defaults 🔌 43 | -------- 44 | - version = `package.json` version 45 | - userAgent = viewer browser agent 46 | - target = DOM element selector 47 | - noStyles = boolean 48 | - offset = number 49 | - parentClass = 'string' 50 | - scrollEl = window || DOM element selector || DOM element 51 | - stickyClass = 'string' 52 | - stuckClass = 'string' 53 | - useStickyClasses = boolean 54 | - useFixed = boolean 55 | - useGetBoundingClientRect = boolean 56 | - verticalPosition = 'string' 57 | - applyStyle = function 58 | -------- 59 | props🔌 60 | -------- 61 | - p = props {object} 62 | -------- 63 | instance note 64 | -------- 65 | - stickybits parent methods return this 66 | - stickybits instance methods return an instance item 67 | -------- 68 | nomenclature 69 | -------- 70 | - target => el => e 71 | - props => o || p 72 | - instance => item => it 73 | -------- 74 | methods 75 | -------- 76 | - .definePosition = defines sticky or fixed 77 | - .addInstance = an array of objects for each Stickybits Target 78 | - .getClosestParent = gets the parent for non-window scroll 79 | - .getTopPosition = gets the element top pixel position from the viewport 80 | - .computeScrollOffsets = computes scroll position 81 | - .toggleClasses = older browser toggler 82 | - .manageState = manages sticky state 83 | - .removeInstance = removes an instance 84 | - .cleanup = removes all Stickybits instances and cleans up dom from stickybits 85 | */ 86 | var Stickybits = 87 | /*#__PURE__*/ 88 | function () { 89 | function Stickybits(target, obj) { 90 | var _this = this; 91 | 92 | var o = typeof obj !== 'undefined' ? obj : {}; 93 | this.version = '3.7.4'; 94 | this.userAgent = window.navigator.userAgent || 'no `userAgent` provided by the browser'; 95 | this.props = { 96 | customStickyChangeNumber: o.customStickyChangeNumber || null, 97 | noStyles: o.noStyles || false, 98 | stickyBitStickyOffset: o.stickyBitStickyOffset || 0, 99 | parentClass: o.parentClass || 'js-stickybit-parent', 100 | scrollEl: typeof o.scrollEl === 'string' ? document.querySelector(o.scrollEl) : o.scrollEl || window, 101 | stickyClass: o.stickyClass || 'js-is-sticky', 102 | stuckClass: o.stuckClass || 'js-is-stuck', 103 | stickyChangeClass: o.stickyChangeClass || 'js-is-sticky--change', 104 | useStickyClasses: o.useStickyClasses || false, 105 | useFixed: o.useFixed || false, 106 | useGetBoundingClientRect: o.useGetBoundingClientRect || false, 107 | verticalPosition: o.verticalPosition || 'top', 108 | applyStyle: o.applyStyle || function (item, style) { 109 | return _this.applyStyle(item, style); 110 | } 111 | /* 112 | define positionVal after the setting of props, because definePosition looks at the props.useFixed 113 | ---- 114 | - uses a computed (`.definePosition()`) 115 | - defined the position 116 | */ 117 | 118 | }; 119 | this.props.positionVal = this.definePosition() || 'fixed'; 120 | this.instances = []; 121 | var _this$props = this.props, 122 | positionVal = _this$props.positionVal, 123 | verticalPosition = _this$props.verticalPosition, 124 | noStyles = _this$props.noStyles, 125 | stickyBitStickyOffset = _this$props.stickyBitStickyOffset; 126 | var verticalPositionStyle = verticalPosition === 'top' && !noStyles ? stickyBitStickyOffset + "px" : ''; 127 | var positionStyle = positionVal !== 'fixed' ? positionVal : ''; 128 | this.els = typeof target === 'string' ? document.querySelectorAll(target) : target; 129 | if (!('length' in this.els)) this.els = [this.els]; 130 | 131 | for (var i = 0; i < this.els.length; i++) { 132 | var _styles; 133 | 134 | var el = this.els[i]; 135 | var instance = this.addInstance(el, this.props); // set vertical position 136 | 137 | this.props.applyStyle({ 138 | styles: (_styles = {}, _styles[verticalPosition] = verticalPositionStyle, _styles.position = positionStyle, _styles), 139 | classes: {} 140 | }, instance); 141 | this.manageState(instance); // instances are an array of objects 142 | 143 | this.instances.push(instance); 144 | } 145 | } 146 | /* 147 | setStickyPosition ✔️ 148 | -------- 149 | — most basic thing stickybits does 150 | => checks to see if position sticky is supported 151 | => defined the position to be used 152 | => stickybits works accordingly 153 | */ 154 | 155 | 156 | var _proto = Stickybits.prototype; 157 | 158 | _proto.definePosition = function definePosition() { 159 | var stickyProp; 160 | 161 | if (this.props.useFixed) { 162 | stickyProp = 'fixed'; 163 | } else { 164 | var prefix = ['', '-o-', '-webkit-', '-moz-', '-ms-']; 165 | var test = document.head.style; 166 | 167 | for (var i = 0; i < prefix.length; i += 1) { 168 | test.position = prefix[i] + "sticky"; 169 | } 170 | 171 | stickyProp = test.position ? test.position : 'fixed'; 172 | test.position = ''; 173 | } 174 | 175 | return stickyProp; 176 | } 177 | /* 178 | addInstance ✔️ 179 | -------- 180 | — manages instances of items 181 | - takes in an el and props 182 | - returns an item object 183 | --- 184 | - target = el 185 | - o = {object} = props 186 | - scrollEl = 'string' | object 187 | - verticalPosition = number 188 | - off = boolean 189 | - parentClass = 'string' 190 | - stickyClass = 'string' 191 | - stuckClass = 'string' 192 | --- 193 | - defined later 194 | - parent = dom element 195 | - state = 'string' 196 | - offset = number 197 | - stickyStart = number 198 | - stickyStop = number 199 | - returns an instance object 200 | */ 201 | ; 202 | 203 | _proto.addInstance = function addInstance(el, props) { 204 | var _this2 = this; 205 | 206 | var item = { 207 | el: el, 208 | parent: el.parentNode, 209 | props: props 210 | }; 211 | 212 | if (props.positionVal === 'fixed' || props.useStickyClasses) { 213 | this.isWin = this.props.scrollEl === window; 214 | var se = this.isWin ? window : this.getClosestParent(item.el, item.props.scrollEl); 215 | this.computeScrollOffsets(item); 216 | this.toggleClasses(item.parent, '', props.parentClass); 217 | item.state = 'default'; 218 | item.stateChange = 'default'; 219 | 220 | item.stateContainer = function () { 221 | return _this2.manageState(item); 222 | }; 223 | 224 | se.addEventListener('scroll', item.stateContainer); 225 | } 226 | 227 | return item; 228 | } 229 | /* 230 | -------- 231 | getParent 👨‍ 232 | -------- 233 | - a helper function that gets the target element's parent selected el 234 | - only used for non `window` scroll elements 235 | - supports older browsers 236 | */ 237 | ; 238 | 239 | _proto.getClosestParent = function getClosestParent(el, match) { 240 | // p = parent element 241 | var p = match; 242 | var e = el; 243 | if (e.parentElement === p) return p; // traverse up the dom tree until we get to the parent 244 | 245 | while (e.parentElement !== p) { 246 | e = e.parentElement; 247 | } // return parent element 248 | 249 | 250 | return p; 251 | } 252 | /* 253 | -------- 254 | getTopPosition 255 | -------- 256 | - a helper function that gets the topPosition of a Stickybit element 257 | - from the top level of the DOM 258 | */ 259 | ; 260 | 261 | _proto.getTopPosition = function getTopPosition(el) { 262 | if (this.props.useGetBoundingClientRect) { 263 | return el.getBoundingClientRect().top + (this.props.scrollEl.pageYOffset || document.documentElement.scrollTop); 264 | } 265 | 266 | var topPosition = 0; 267 | 268 | do { 269 | topPosition = el.offsetTop + topPosition; 270 | } while (el = el.offsetParent); 271 | 272 | return topPosition; 273 | } 274 | /* 275 | computeScrollOffsets 📊 276 | --- 277 | computeScrollOffsets for Stickybits 278 | - defines 279 | - offset 280 | - start 281 | - stop 282 | */ 283 | ; 284 | 285 | _proto.computeScrollOffsets = function computeScrollOffsets(item) { 286 | var it = item; 287 | var p = it.props; 288 | var el = it.el; 289 | var parent = it.parent; 290 | var isCustom = !this.isWin && p.positionVal === 'fixed'; 291 | var isTop = p.verticalPosition !== 'bottom'; 292 | var scrollElOffset = isCustom ? this.getTopPosition(p.scrollEl) : 0; 293 | var stickyStart = isCustom ? this.getTopPosition(parent) - scrollElOffset : this.getTopPosition(parent); 294 | var stickyChangeOffset = p.customStickyChangeNumber !== null ? p.customStickyChangeNumber : el.offsetHeight; 295 | var parentBottom = stickyStart + parent.offsetHeight; 296 | it.offset = !isCustom ? scrollElOffset + p.stickyBitStickyOffset : 0; 297 | it.stickyStart = isTop ? stickyStart - it.offset : 0; 298 | it.stickyChange = it.stickyStart + stickyChangeOffset; 299 | it.stickyStop = isTop ? parentBottom - (el.offsetHeight + it.offset) : parentBottom - window.innerHeight; 300 | } 301 | /* 302 | toggleClasses ⚖️ 303 | --- 304 | toggles classes (for older browser support) 305 | r = removed class 306 | a = added class 307 | */ 308 | ; 309 | 310 | _proto.toggleClasses = function toggleClasses(el, r, a) { 311 | var e = el; 312 | var cArray = e.className.split(' '); 313 | if (a && cArray.indexOf(a) === -1) cArray.push(a); 314 | var rItem = cArray.indexOf(r); 315 | if (rItem !== -1) cArray.splice(rItem, 1); 316 | e.className = cArray.join(' '); 317 | } 318 | /* 319 | manageState 📝 320 | --- 321 | - defines the state 322 | - normal 323 | - sticky 324 | - stuck 325 | */ 326 | ; 327 | 328 | _proto.manageState = function manageState(item) { 329 | var _this3 = this; 330 | 331 | // cache object 332 | var it = item; 333 | var p = it.props; 334 | var state = it.state; 335 | var stateChange = it.stateChange; 336 | var start = it.stickyStart; 337 | var change = it.stickyChange; 338 | var stop = it.stickyStop; // cache props 339 | 340 | var pv = p.positionVal; 341 | var se = p.scrollEl; 342 | var sticky = p.stickyClass; 343 | var stickyChange = p.stickyChangeClass; 344 | var stuck = p.stuckClass; 345 | var vp = p.verticalPosition; 346 | var isTop = vp !== 'bottom'; 347 | var aS = p.applyStyle; 348 | var ns = p.noStyles; 349 | /* 350 | requestAnimationFrame 351 | --- 352 | - use rAF 353 | - or stub rAF 354 | */ 355 | 356 | var rAFStub = function rAFDummy(f) { 357 | f(); 358 | }; 359 | 360 | var rAF = !this.isWin ? rAFStub : window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || rAFStub; 361 | /* 362 | define scroll vars 363 | --- 364 | - scroll 365 | - notSticky 366 | - isSticky 367 | - isStuck 368 | */ 369 | 370 | var scroll = this.isWin ? window.scrollY || window.pageYOffset : se.scrollTop; 371 | var notSticky = scroll > start && scroll < stop && (state === 'default' || state === 'stuck'); 372 | var isSticky = isTop && scroll <= start && (state === 'sticky' || state === 'stuck'); 373 | var isStuck = scroll >= stop && state === 'sticky'; 374 | /* 375 | Unnamed arrow functions within this block 376 | --- 377 | - help wanted or discussion 378 | - view test.stickybits.js 379 | - `stickybits .manageState `position: fixed` interface` for more awareness 👀 380 | */ 381 | 382 | if (notSticky) { 383 | it.state = 'sticky'; 384 | } else if (isSticky) { 385 | it.state = 'default'; 386 | } else if (isStuck) { 387 | it.state = 'stuck'; 388 | } 389 | 390 | var isStickyChange = scroll >= change && scroll <= stop; 391 | var isNotStickyChange = scroll < change / 2 || scroll > stop; 392 | 393 | if (isNotStickyChange) { 394 | it.stateChange = 'default'; 395 | } else if (isStickyChange) { 396 | it.stateChange = 'sticky'; 397 | } // Only apply new styles if the state has changed 398 | 399 | 400 | if (state === it.state && stateChange === it.stateChange) return; 401 | rAF(function () { 402 | var _styles2, _classes, _styles3, _extends2, _classes2, _style$classes; 403 | 404 | var stateStyles = { 405 | sticky: { 406 | styles: (_styles2 = { 407 | position: pv, 408 | top: '', 409 | bottom: '' 410 | }, _styles2[vp] = p.stickyBitStickyOffset + "px", _styles2), 411 | classes: (_classes = {}, _classes[sticky] = true, _classes) 412 | }, 413 | default: { 414 | styles: (_styles3 = {}, _styles3[vp] = '', _styles3), 415 | classes: {} 416 | }, 417 | stuck: { 418 | styles: _extends((_extends2 = {}, _extends2[vp] = '', _extends2), pv === 'fixed' && !ns || !_this3.isWin ? { 419 | position: 'absolute', 420 | top: '', 421 | bottom: '0' 422 | } : {}), 423 | classes: (_classes2 = {}, _classes2[stuck] = true, _classes2) 424 | } 425 | }; 426 | 427 | if (pv === 'fixed') { 428 | stateStyles.default.styles.position = ''; 429 | } 430 | 431 | var style = stateStyles[it.state]; 432 | style.classes = (_style$classes = {}, _style$classes[stuck] = !!style.classes[stuck], _style$classes[sticky] = !!style.classes[sticky], _style$classes[stickyChange] = isStickyChange, _style$classes); 433 | aS(style, item); 434 | }); 435 | } 436 | /* 437 | applyStyle 438 | --- 439 | - apply the given styles and classes to the element 440 | */ 441 | ; 442 | 443 | _proto.applyStyle = function applyStyle(_ref, item) { 444 | var styles = _ref.styles, 445 | classes = _ref.classes; 446 | // cache object 447 | var it = item; 448 | var e = it.el; 449 | var p = it.props; 450 | var stl = e.style; // cache props 451 | 452 | var ns = p.noStyles; 453 | var cArray = e.className.split(' '); // Disable due to bug with old versions of eslint-scope and for ... in 454 | // https://github.com/eslint/eslint/issues/12117 455 | // eslint-disable-next-line no-unused-vars 456 | 457 | for (var cls in classes) { 458 | var addClass = classes[cls]; 459 | 460 | if (addClass) { 461 | if (cArray.indexOf(cls) === -1) cArray.push(cls); 462 | } else { 463 | var idx = cArray.indexOf(cls); 464 | if (idx !== -1) cArray.splice(idx, 1); 465 | } 466 | } 467 | 468 | e.className = cArray.join(' '); 469 | 470 | if (styles['position']) { 471 | stl['position'] = styles['position']; 472 | } 473 | 474 | if (ns) return; // eslint-disable-next-line no-unused-vars 475 | 476 | for (var key in styles) { 477 | stl[key] = styles[key]; 478 | } 479 | }; 480 | 481 | _proto.update = function update(updatedProps) { 482 | var _this4 = this; 483 | 484 | if (updatedProps === void 0) { 485 | updatedProps = null; 486 | } 487 | 488 | this.instances.forEach(function (instance) { 489 | _this4.computeScrollOffsets(instance); 490 | 491 | if (updatedProps) { 492 | // eslint-disable-next-line no-unused-vars 493 | for (var updatedProp in updatedProps) { 494 | instance.props[updatedProp] = updatedProps[updatedProp]; 495 | } 496 | } 497 | }); 498 | return this; 499 | } 500 | /* 501 | removes an instance 👋 502 | -------- 503 | - cleanup instance 504 | */ 505 | ; 506 | 507 | _proto.removeInstance = function removeInstance(instance) { 508 | var _styles4, _classes3; 509 | 510 | var e = instance.el; 511 | var p = instance.props; 512 | this.applyStyle({ 513 | styles: (_styles4 = { 514 | position: '' 515 | }, _styles4[p.verticalPosition] = '', _styles4), 516 | classes: (_classes3 = {}, _classes3[p.stickyClass] = '', _classes3[p.stuckClass] = '', _classes3) 517 | }, instance); 518 | this.toggleClasses(e.parentNode, p.parentClass); 519 | } 520 | /* 521 | cleanup 🛁 522 | -------- 523 | - cleans up each instance 524 | - clears instance 525 | */ 526 | ; 527 | 528 | _proto.cleanup = function cleanup() { 529 | for (var i = 0; i < this.instances.length; i += 1) { 530 | var instance = this.instances[i]; 531 | 532 | if (instance.stateContainer) { 533 | instance.props.scrollEl.removeEventListener('scroll', instance.stateContainer); 534 | } 535 | 536 | this.removeInstance(instance); 537 | } 538 | 539 | this.manageState = false; 540 | this.instances = []; 541 | }; 542 | 543 | return Stickybits; 544 | }(); 545 | /* 546 | export 547 | -------- 548 | exports StickBits to be used 🏁 549 | */ 550 | 551 | 552 | function stickybits(target, o) { 553 | return new Stickybits(target, o); 554 | } 555 | 556 | if (typeof window !== 'undefined') { 557 | var plugin = window.$ || window.jQuery || window.Zepto; 558 | 559 | if (plugin) { 560 | plugin.fn.stickybits = function stickybitsPlugin(opts) { 561 | return stickybits(this, opts); 562 | }; 563 | } 564 | } 565 | 566 | }))); 567 | -------------------------------------------------------------------------------- /dist/jquery.stickybits.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | stickybits - Stickybits is a lightweight alternative to `position: sticky` polyfills 3 | @version v3.7.4 4 | @link https://github.com/dollarshaveclub/stickybits#readme 5 | @author Jeff Wainwright (https://jeffry.in) 6 | @license MIT 7 | **/ 8 | !function(t){"function"==typeof define&&define.amd?define(t):t()}(function(){"use strict";function x(){return(x=Object.assign||function(t){for(var s=1;s (https://jeffry.in) 6 | @license MIT 7 | **/ 8 | function _extends() { 9 | _extends = Object.assign || function (target) { 10 | for (var i = 1; i < arguments.length; i++) { 11 | var source = arguments[i]; 12 | 13 | for (var key in source) { 14 | if (Object.prototype.hasOwnProperty.call(source, key)) { 15 | target[key] = source[key]; 16 | } 17 | } 18 | } 19 | 20 | return target; 21 | }; 22 | 23 | return _extends.apply(this, arguments); 24 | } 25 | 26 | /* 27 | STICKYBITS 💉 28 | -------- 29 | > a lightweight alternative to `position: sticky` polyfills 🍬 30 | -------- 31 | - each method is documented above it our view the readme 32 | - Stickybits does not manage polymorphic functionality (position like properties) 33 | * polymorphic functionality: (in the context of describing Stickybits) 34 | means making things like `position: sticky` be loosely supported with position fixed. 35 | It also means that features like `useStickyClasses` takes on styles like `position: fixed`. 36 | -------- 37 | defaults 🔌 38 | -------- 39 | - version = `package.json` version 40 | - userAgent = viewer browser agent 41 | - target = DOM element selector 42 | - noStyles = boolean 43 | - offset = number 44 | - parentClass = 'string' 45 | - scrollEl = window || DOM element selector || DOM element 46 | - stickyClass = 'string' 47 | - stuckClass = 'string' 48 | - useStickyClasses = boolean 49 | - useFixed = boolean 50 | - useGetBoundingClientRect = boolean 51 | - verticalPosition = 'string' 52 | - applyStyle = function 53 | -------- 54 | props🔌 55 | -------- 56 | - p = props {object} 57 | -------- 58 | instance note 59 | -------- 60 | - stickybits parent methods return this 61 | - stickybits instance methods return an instance item 62 | -------- 63 | nomenclature 64 | -------- 65 | - target => el => e 66 | - props => o || p 67 | - instance => item => it 68 | -------- 69 | methods 70 | -------- 71 | - .definePosition = defines sticky or fixed 72 | - .addInstance = an array of objects for each Stickybits Target 73 | - .getClosestParent = gets the parent for non-window scroll 74 | - .getTopPosition = gets the element top pixel position from the viewport 75 | - .computeScrollOffsets = computes scroll position 76 | - .toggleClasses = older browser toggler 77 | - .manageState = manages sticky state 78 | - .removeInstance = removes an instance 79 | - .cleanup = removes all Stickybits instances and cleans up dom from stickybits 80 | */ 81 | var Stickybits = 82 | /*#__PURE__*/ 83 | function () { 84 | function Stickybits(target, obj) { 85 | var _this = this; 86 | 87 | var o = typeof obj !== 'undefined' ? obj : {}; 88 | this.version = '3.7.4'; 89 | this.userAgent = window.navigator.userAgent || 'no `userAgent` provided by the browser'; 90 | this.props = { 91 | customStickyChangeNumber: o.customStickyChangeNumber || null, 92 | noStyles: o.noStyles || false, 93 | stickyBitStickyOffset: o.stickyBitStickyOffset || 0, 94 | parentClass: o.parentClass || 'js-stickybit-parent', 95 | scrollEl: typeof o.scrollEl === 'string' ? document.querySelector(o.scrollEl) : o.scrollEl || window, 96 | stickyClass: o.stickyClass || 'js-is-sticky', 97 | stuckClass: o.stuckClass || 'js-is-stuck', 98 | stickyChangeClass: o.stickyChangeClass || 'js-is-sticky--change', 99 | useStickyClasses: o.useStickyClasses || false, 100 | useFixed: o.useFixed || false, 101 | useGetBoundingClientRect: o.useGetBoundingClientRect || false, 102 | verticalPosition: o.verticalPosition || 'top', 103 | applyStyle: o.applyStyle || function (item, style) { 104 | return _this.applyStyle(item, style); 105 | } 106 | /* 107 | define positionVal after the setting of props, because definePosition looks at the props.useFixed 108 | ---- 109 | - uses a computed (`.definePosition()`) 110 | - defined the position 111 | */ 112 | 113 | }; 114 | this.props.positionVal = this.definePosition() || 'fixed'; 115 | this.instances = []; 116 | var _this$props = this.props, 117 | positionVal = _this$props.positionVal, 118 | verticalPosition = _this$props.verticalPosition, 119 | noStyles = _this$props.noStyles, 120 | stickyBitStickyOffset = _this$props.stickyBitStickyOffset; 121 | var verticalPositionStyle = verticalPosition === 'top' && !noStyles ? stickyBitStickyOffset + "px" : ''; 122 | var positionStyle = positionVal !== 'fixed' ? positionVal : ''; 123 | this.els = typeof target === 'string' ? document.querySelectorAll(target) : target; 124 | if (!('length' in this.els)) this.els = [this.els]; 125 | 126 | for (var i = 0; i < this.els.length; i++) { 127 | var _styles; 128 | 129 | var el = this.els[i]; 130 | var instance = this.addInstance(el, this.props); // set vertical position 131 | 132 | this.props.applyStyle({ 133 | styles: (_styles = {}, _styles[verticalPosition] = verticalPositionStyle, _styles.position = positionStyle, _styles), 134 | classes: {} 135 | }, instance); 136 | this.manageState(instance); // instances are an array of objects 137 | 138 | this.instances.push(instance); 139 | } 140 | } 141 | /* 142 | setStickyPosition ✔️ 143 | -------- 144 | — most basic thing stickybits does 145 | => checks to see if position sticky is supported 146 | => defined the position to be used 147 | => stickybits works accordingly 148 | */ 149 | 150 | 151 | var _proto = Stickybits.prototype; 152 | 153 | _proto.definePosition = function definePosition() { 154 | var stickyProp; 155 | 156 | if (this.props.useFixed) { 157 | stickyProp = 'fixed'; 158 | } else { 159 | var prefix = ['', '-o-', '-webkit-', '-moz-', '-ms-']; 160 | var test = document.head.style; 161 | 162 | for (var i = 0; i < prefix.length; i += 1) { 163 | test.position = prefix[i] + "sticky"; 164 | } 165 | 166 | stickyProp = test.position ? test.position : 'fixed'; 167 | test.position = ''; 168 | } 169 | 170 | return stickyProp; 171 | } 172 | /* 173 | addInstance ✔️ 174 | -------- 175 | — manages instances of items 176 | - takes in an el and props 177 | - returns an item object 178 | --- 179 | - target = el 180 | - o = {object} = props 181 | - scrollEl = 'string' | object 182 | - verticalPosition = number 183 | - off = boolean 184 | - parentClass = 'string' 185 | - stickyClass = 'string' 186 | - stuckClass = 'string' 187 | --- 188 | - defined later 189 | - parent = dom element 190 | - state = 'string' 191 | - offset = number 192 | - stickyStart = number 193 | - stickyStop = number 194 | - returns an instance object 195 | */ 196 | ; 197 | 198 | _proto.addInstance = function addInstance(el, props) { 199 | var _this2 = this; 200 | 201 | var item = { 202 | el: el, 203 | parent: el.parentNode, 204 | props: props 205 | }; 206 | 207 | if (props.positionVal === 'fixed' || props.useStickyClasses) { 208 | this.isWin = this.props.scrollEl === window; 209 | var se = this.isWin ? window : this.getClosestParent(item.el, item.props.scrollEl); 210 | this.computeScrollOffsets(item); 211 | this.toggleClasses(item.parent, '', props.parentClass); 212 | item.state = 'default'; 213 | item.stateChange = 'default'; 214 | 215 | item.stateContainer = function () { 216 | return _this2.manageState(item); 217 | }; 218 | 219 | se.addEventListener('scroll', item.stateContainer); 220 | } 221 | 222 | return item; 223 | } 224 | /* 225 | -------- 226 | getParent 👨‍ 227 | -------- 228 | - a helper function that gets the target element's parent selected el 229 | - only used for non `window` scroll elements 230 | - supports older browsers 231 | */ 232 | ; 233 | 234 | _proto.getClosestParent = function getClosestParent(el, match) { 235 | // p = parent element 236 | var p = match; 237 | var e = el; 238 | if (e.parentElement === p) return p; // traverse up the dom tree until we get to the parent 239 | 240 | while (e.parentElement !== p) { 241 | e = e.parentElement; 242 | } // return parent element 243 | 244 | 245 | return p; 246 | } 247 | /* 248 | -------- 249 | getTopPosition 250 | -------- 251 | - a helper function that gets the topPosition of a Stickybit element 252 | - from the top level of the DOM 253 | */ 254 | ; 255 | 256 | _proto.getTopPosition = function getTopPosition(el) { 257 | if (this.props.useGetBoundingClientRect) { 258 | return el.getBoundingClientRect().top + (this.props.scrollEl.pageYOffset || document.documentElement.scrollTop); 259 | } 260 | 261 | var topPosition = 0; 262 | 263 | do { 264 | topPosition = el.offsetTop + topPosition; 265 | } while (el = el.offsetParent); 266 | 267 | return topPosition; 268 | } 269 | /* 270 | computeScrollOffsets 📊 271 | --- 272 | computeScrollOffsets for Stickybits 273 | - defines 274 | - offset 275 | - start 276 | - stop 277 | */ 278 | ; 279 | 280 | _proto.computeScrollOffsets = function computeScrollOffsets(item) { 281 | var it = item; 282 | var p = it.props; 283 | var el = it.el; 284 | var parent = it.parent; 285 | var isCustom = !this.isWin && p.positionVal === 'fixed'; 286 | var isTop = p.verticalPosition !== 'bottom'; 287 | var scrollElOffset = isCustom ? this.getTopPosition(p.scrollEl) : 0; 288 | var stickyStart = isCustom ? this.getTopPosition(parent) - scrollElOffset : this.getTopPosition(parent); 289 | var stickyChangeOffset = p.customStickyChangeNumber !== null ? p.customStickyChangeNumber : el.offsetHeight; 290 | var parentBottom = stickyStart + parent.offsetHeight; 291 | it.offset = !isCustom ? scrollElOffset + p.stickyBitStickyOffset : 0; 292 | it.stickyStart = isTop ? stickyStart - it.offset : 0; 293 | it.stickyChange = it.stickyStart + stickyChangeOffset; 294 | it.stickyStop = isTop ? parentBottom - (el.offsetHeight + it.offset) : parentBottom - window.innerHeight; 295 | } 296 | /* 297 | toggleClasses ⚖️ 298 | --- 299 | toggles classes (for older browser support) 300 | r = removed class 301 | a = added class 302 | */ 303 | ; 304 | 305 | _proto.toggleClasses = function toggleClasses(el, r, a) { 306 | var e = el; 307 | var cArray = e.className.split(' '); 308 | if (a && cArray.indexOf(a) === -1) cArray.push(a); 309 | var rItem = cArray.indexOf(r); 310 | if (rItem !== -1) cArray.splice(rItem, 1); 311 | e.className = cArray.join(' '); 312 | } 313 | /* 314 | manageState 📝 315 | --- 316 | - defines the state 317 | - normal 318 | - sticky 319 | - stuck 320 | */ 321 | ; 322 | 323 | _proto.manageState = function manageState(item) { 324 | var _this3 = this; 325 | 326 | // cache object 327 | var it = item; 328 | var p = it.props; 329 | var state = it.state; 330 | var stateChange = it.stateChange; 331 | var start = it.stickyStart; 332 | var change = it.stickyChange; 333 | var stop = it.stickyStop; // cache props 334 | 335 | var pv = p.positionVal; 336 | var se = p.scrollEl; 337 | var sticky = p.stickyClass; 338 | var stickyChange = p.stickyChangeClass; 339 | var stuck = p.stuckClass; 340 | var vp = p.verticalPosition; 341 | var isTop = vp !== 'bottom'; 342 | var aS = p.applyStyle; 343 | var ns = p.noStyles; 344 | /* 345 | requestAnimationFrame 346 | --- 347 | - use rAF 348 | - or stub rAF 349 | */ 350 | 351 | var rAFStub = function rAFDummy(f) { 352 | f(); 353 | }; 354 | 355 | var rAF = !this.isWin ? rAFStub : window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || rAFStub; 356 | /* 357 | define scroll vars 358 | --- 359 | - scroll 360 | - notSticky 361 | - isSticky 362 | - isStuck 363 | */ 364 | 365 | var scroll = this.isWin ? window.scrollY || window.pageYOffset : se.scrollTop; 366 | var notSticky = scroll > start && scroll < stop && (state === 'default' || state === 'stuck'); 367 | var isSticky = isTop && scroll <= start && (state === 'sticky' || state === 'stuck'); 368 | var isStuck = scroll >= stop && state === 'sticky'; 369 | /* 370 | Unnamed arrow functions within this block 371 | --- 372 | - help wanted or discussion 373 | - view test.stickybits.js 374 | - `stickybits .manageState `position: fixed` interface` for more awareness 👀 375 | */ 376 | 377 | if (notSticky) { 378 | it.state = 'sticky'; 379 | } else if (isSticky) { 380 | it.state = 'default'; 381 | } else if (isStuck) { 382 | it.state = 'stuck'; 383 | } 384 | 385 | var isStickyChange = scroll >= change && scroll <= stop; 386 | var isNotStickyChange = scroll < change / 2 || scroll > stop; 387 | 388 | if (isNotStickyChange) { 389 | it.stateChange = 'default'; 390 | } else if (isStickyChange) { 391 | it.stateChange = 'sticky'; 392 | } // Only apply new styles if the state has changed 393 | 394 | 395 | if (state === it.state && stateChange === it.stateChange) return; 396 | rAF(function () { 397 | var _styles2, _classes, _styles3, _extends2, _classes2, _style$classes; 398 | 399 | var stateStyles = { 400 | sticky: { 401 | styles: (_styles2 = { 402 | position: pv, 403 | top: '', 404 | bottom: '' 405 | }, _styles2[vp] = p.stickyBitStickyOffset + "px", _styles2), 406 | classes: (_classes = {}, _classes[sticky] = true, _classes) 407 | }, 408 | default: { 409 | styles: (_styles3 = {}, _styles3[vp] = '', _styles3), 410 | classes: {} 411 | }, 412 | stuck: { 413 | styles: _extends((_extends2 = {}, _extends2[vp] = '', _extends2), pv === 'fixed' && !ns || !_this3.isWin ? { 414 | position: 'absolute', 415 | top: '', 416 | bottom: '0' 417 | } : {}), 418 | classes: (_classes2 = {}, _classes2[stuck] = true, _classes2) 419 | } 420 | }; 421 | 422 | if (pv === 'fixed') { 423 | stateStyles.default.styles.position = ''; 424 | } 425 | 426 | var style = stateStyles[it.state]; 427 | style.classes = (_style$classes = {}, _style$classes[stuck] = !!style.classes[stuck], _style$classes[sticky] = !!style.classes[sticky], _style$classes[stickyChange] = isStickyChange, _style$classes); 428 | aS(style, item); 429 | }); 430 | } 431 | /* 432 | applyStyle 433 | --- 434 | - apply the given styles and classes to the element 435 | */ 436 | ; 437 | 438 | _proto.applyStyle = function applyStyle(_ref, item) { 439 | var styles = _ref.styles, 440 | classes = _ref.classes; 441 | // cache object 442 | var it = item; 443 | var e = it.el; 444 | var p = it.props; 445 | var stl = e.style; // cache props 446 | 447 | var ns = p.noStyles; 448 | var cArray = e.className.split(' '); // Disable due to bug with old versions of eslint-scope and for ... in 449 | // https://github.com/eslint/eslint/issues/12117 450 | // eslint-disable-next-line no-unused-vars 451 | 452 | for (var cls in classes) { 453 | var addClass = classes[cls]; 454 | 455 | if (addClass) { 456 | if (cArray.indexOf(cls) === -1) cArray.push(cls); 457 | } else { 458 | var idx = cArray.indexOf(cls); 459 | if (idx !== -1) cArray.splice(idx, 1); 460 | } 461 | } 462 | 463 | e.className = cArray.join(' '); 464 | 465 | if (styles['position']) { 466 | stl['position'] = styles['position']; 467 | } 468 | 469 | if (ns) return; // eslint-disable-next-line no-unused-vars 470 | 471 | for (var key in styles) { 472 | stl[key] = styles[key]; 473 | } 474 | }; 475 | 476 | _proto.update = function update(updatedProps) { 477 | var _this4 = this; 478 | 479 | if (updatedProps === void 0) { 480 | updatedProps = null; 481 | } 482 | 483 | this.instances.forEach(function (instance) { 484 | _this4.computeScrollOffsets(instance); 485 | 486 | if (updatedProps) { 487 | // eslint-disable-next-line no-unused-vars 488 | for (var updatedProp in updatedProps) { 489 | instance.props[updatedProp] = updatedProps[updatedProp]; 490 | } 491 | } 492 | }); 493 | return this; 494 | } 495 | /* 496 | removes an instance 👋 497 | -------- 498 | - cleanup instance 499 | */ 500 | ; 501 | 502 | _proto.removeInstance = function removeInstance(instance) { 503 | var _styles4, _classes3; 504 | 505 | var e = instance.el; 506 | var p = instance.props; 507 | this.applyStyle({ 508 | styles: (_styles4 = { 509 | position: '' 510 | }, _styles4[p.verticalPosition] = '', _styles4), 511 | classes: (_classes3 = {}, _classes3[p.stickyClass] = '', _classes3[p.stuckClass] = '', _classes3) 512 | }, instance); 513 | this.toggleClasses(e.parentNode, p.parentClass); 514 | } 515 | /* 516 | cleanup 🛁 517 | -------- 518 | - cleans up each instance 519 | - clears instance 520 | */ 521 | ; 522 | 523 | _proto.cleanup = function cleanup() { 524 | for (var i = 0; i < this.instances.length; i += 1) { 525 | var instance = this.instances[i]; 526 | 527 | if (instance.stateContainer) { 528 | instance.props.scrollEl.removeEventListener('scroll', instance.stateContainer); 529 | } 530 | 531 | this.removeInstance(instance); 532 | } 533 | 534 | this.manageState = false; 535 | this.instances = []; 536 | }; 537 | 538 | return Stickybits; 539 | }(); 540 | /* 541 | export 542 | -------- 543 | exports StickBits to be used 🏁 544 | */ 545 | 546 | 547 | function stickybits(target, o) { 548 | return new Stickybits(target, o); 549 | } 550 | 551 | export default stickybits; 552 | -------------------------------------------------------------------------------- /dist/stickybits.js: -------------------------------------------------------------------------------- 1 | /** 2 | stickybits - Stickybits is a lightweight alternative to `position: sticky` polyfills 3 | @version v3.7.4 4 | @link https://github.com/dollarshaveclub/stickybits#readme 5 | @author Jeff Wainwright (https://jeffry.in) 6 | @license MIT 7 | **/ 8 | (function (global, factory) { 9 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 10 | typeof define === 'function' && define.amd ? define(factory) : 11 | (global = global || self, global.stickybits = factory()); 12 | }(this, (function () { 'use strict'; 13 | 14 | function _extends() { 15 | _extends = Object.assign || function (target) { 16 | for (var i = 1; i < arguments.length; i++) { 17 | var source = arguments[i]; 18 | 19 | for (var key in source) { 20 | if (Object.prototype.hasOwnProperty.call(source, key)) { 21 | target[key] = source[key]; 22 | } 23 | } 24 | } 25 | 26 | return target; 27 | }; 28 | 29 | return _extends.apply(this, arguments); 30 | } 31 | 32 | /* 33 | STICKYBITS 💉 34 | -------- 35 | > a lightweight alternative to `position: sticky` polyfills 🍬 36 | -------- 37 | - each method is documented above it our view the readme 38 | - Stickybits does not manage polymorphic functionality (position like properties) 39 | * polymorphic functionality: (in the context of describing Stickybits) 40 | means making things like `position: sticky` be loosely supported with position fixed. 41 | It also means that features like `useStickyClasses` takes on styles like `position: fixed`. 42 | -------- 43 | defaults 🔌 44 | -------- 45 | - version = `package.json` version 46 | - userAgent = viewer browser agent 47 | - target = DOM element selector 48 | - noStyles = boolean 49 | - offset = number 50 | - parentClass = 'string' 51 | - scrollEl = window || DOM element selector || DOM element 52 | - stickyClass = 'string' 53 | - stuckClass = 'string' 54 | - useStickyClasses = boolean 55 | - useFixed = boolean 56 | - useGetBoundingClientRect = boolean 57 | - verticalPosition = 'string' 58 | - applyStyle = function 59 | -------- 60 | props🔌 61 | -------- 62 | - p = props {object} 63 | -------- 64 | instance note 65 | -------- 66 | - stickybits parent methods return this 67 | - stickybits instance methods return an instance item 68 | -------- 69 | nomenclature 70 | -------- 71 | - target => el => e 72 | - props => o || p 73 | - instance => item => it 74 | -------- 75 | methods 76 | -------- 77 | - .definePosition = defines sticky or fixed 78 | - .addInstance = an array of objects for each Stickybits Target 79 | - .getClosestParent = gets the parent for non-window scroll 80 | - .getTopPosition = gets the element top pixel position from the viewport 81 | - .computeScrollOffsets = computes scroll position 82 | - .toggleClasses = older browser toggler 83 | - .manageState = manages sticky state 84 | - .removeInstance = removes an instance 85 | - .cleanup = removes all Stickybits instances and cleans up dom from stickybits 86 | */ 87 | var Stickybits = 88 | /*#__PURE__*/ 89 | function () { 90 | function Stickybits(target, obj) { 91 | var _this = this; 92 | 93 | var o = typeof obj !== 'undefined' ? obj : {}; 94 | this.version = '3.7.4'; 95 | this.userAgent = window.navigator.userAgent || 'no `userAgent` provided by the browser'; 96 | this.props = { 97 | customStickyChangeNumber: o.customStickyChangeNumber || null, 98 | noStyles: o.noStyles || false, 99 | stickyBitStickyOffset: o.stickyBitStickyOffset || 0, 100 | parentClass: o.parentClass || 'js-stickybit-parent', 101 | scrollEl: typeof o.scrollEl === 'string' ? document.querySelector(o.scrollEl) : o.scrollEl || window, 102 | stickyClass: o.stickyClass || 'js-is-sticky', 103 | stuckClass: o.stuckClass || 'js-is-stuck', 104 | stickyChangeClass: o.stickyChangeClass || 'js-is-sticky--change', 105 | useStickyClasses: o.useStickyClasses || false, 106 | useFixed: o.useFixed || false, 107 | useGetBoundingClientRect: o.useGetBoundingClientRect || false, 108 | verticalPosition: o.verticalPosition || 'top', 109 | applyStyle: o.applyStyle || function (item, style) { 110 | return _this.applyStyle(item, style); 111 | } 112 | /* 113 | define positionVal after the setting of props, because definePosition looks at the props.useFixed 114 | ---- 115 | - uses a computed (`.definePosition()`) 116 | - defined the position 117 | */ 118 | 119 | }; 120 | this.props.positionVal = this.definePosition() || 'fixed'; 121 | this.instances = []; 122 | var _this$props = this.props, 123 | positionVal = _this$props.positionVal, 124 | verticalPosition = _this$props.verticalPosition, 125 | noStyles = _this$props.noStyles, 126 | stickyBitStickyOffset = _this$props.stickyBitStickyOffset; 127 | var verticalPositionStyle = verticalPosition === 'top' && !noStyles ? stickyBitStickyOffset + "px" : ''; 128 | var positionStyle = positionVal !== 'fixed' ? positionVal : ''; 129 | this.els = typeof target === 'string' ? document.querySelectorAll(target) : target; 130 | if (!('length' in this.els)) this.els = [this.els]; 131 | 132 | for (var i = 0; i < this.els.length; i++) { 133 | var _styles; 134 | 135 | var el = this.els[i]; 136 | var instance = this.addInstance(el, this.props); // set vertical position 137 | 138 | this.props.applyStyle({ 139 | styles: (_styles = {}, _styles[verticalPosition] = verticalPositionStyle, _styles.position = positionStyle, _styles), 140 | classes: {} 141 | }, instance); 142 | this.manageState(instance); // instances are an array of objects 143 | 144 | this.instances.push(instance); 145 | } 146 | } 147 | /* 148 | setStickyPosition ✔️ 149 | -------- 150 | — most basic thing stickybits does 151 | => checks to see if position sticky is supported 152 | => defined the position to be used 153 | => stickybits works accordingly 154 | */ 155 | 156 | 157 | var _proto = Stickybits.prototype; 158 | 159 | _proto.definePosition = function definePosition() { 160 | var stickyProp; 161 | 162 | if (this.props.useFixed) { 163 | stickyProp = 'fixed'; 164 | } else { 165 | var prefix = ['', '-o-', '-webkit-', '-moz-', '-ms-']; 166 | var test = document.head.style; 167 | 168 | for (var i = 0; i < prefix.length; i += 1) { 169 | test.position = prefix[i] + "sticky"; 170 | } 171 | 172 | stickyProp = test.position ? test.position : 'fixed'; 173 | test.position = ''; 174 | } 175 | 176 | return stickyProp; 177 | } 178 | /* 179 | addInstance ✔️ 180 | -------- 181 | — manages instances of items 182 | - takes in an el and props 183 | - returns an item object 184 | --- 185 | - target = el 186 | - o = {object} = props 187 | - scrollEl = 'string' | object 188 | - verticalPosition = number 189 | - off = boolean 190 | - parentClass = 'string' 191 | - stickyClass = 'string' 192 | - stuckClass = 'string' 193 | --- 194 | - defined later 195 | - parent = dom element 196 | - state = 'string' 197 | - offset = number 198 | - stickyStart = number 199 | - stickyStop = number 200 | - returns an instance object 201 | */ 202 | ; 203 | 204 | _proto.addInstance = function addInstance(el, props) { 205 | var _this2 = this; 206 | 207 | var item = { 208 | el: el, 209 | parent: el.parentNode, 210 | props: props 211 | }; 212 | 213 | if (props.positionVal === 'fixed' || props.useStickyClasses) { 214 | this.isWin = this.props.scrollEl === window; 215 | var se = this.isWin ? window : this.getClosestParent(item.el, item.props.scrollEl); 216 | this.computeScrollOffsets(item); 217 | this.toggleClasses(item.parent, '', props.parentClass); 218 | item.state = 'default'; 219 | item.stateChange = 'default'; 220 | 221 | item.stateContainer = function () { 222 | return _this2.manageState(item); 223 | }; 224 | 225 | se.addEventListener('scroll', item.stateContainer); 226 | } 227 | 228 | return item; 229 | } 230 | /* 231 | -------- 232 | getParent 👨‍ 233 | -------- 234 | - a helper function that gets the target element's parent selected el 235 | - only used for non `window` scroll elements 236 | - supports older browsers 237 | */ 238 | ; 239 | 240 | _proto.getClosestParent = function getClosestParent(el, match) { 241 | // p = parent element 242 | var p = match; 243 | var e = el; 244 | if (e.parentElement === p) return p; // traverse up the dom tree until we get to the parent 245 | 246 | while (e.parentElement !== p) { 247 | e = e.parentElement; 248 | } // return parent element 249 | 250 | 251 | return p; 252 | } 253 | /* 254 | -------- 255 | getTopPosition 256 | -------- 257 | - a helper function that gets the topPosition of a Stickybit element 258 | - from the top level of the DOM 259 | */ 260 | ; 261 | 262 | _proto.getTopPosition = function getTopPosition(el) { 263 | if (this.props.useGetBoundingClientRect) { 264 | return el.getBoundingClientRect().top + (this.props.scrollEl.pageYOffset || document.documentElement.scrollTop); 265 | } 266 | 267 | var topPosition = 0; 268 | 269 | do { 270 | topPosition = el.offsetTop + topPosition; 271 | } while (el = el.offsetParent); 272 | 273 | return topPosition; 274 | } 275 | /* 276 | computeScrollOffsets 📊 277 | --- 278 | computeScrollOffsets for Stickybits 279 | - defines 280 | - offset 281 | - start 282 | - stop 283 | */ 284 | ; 285 | 286 | _proto.computeScrollOffsets = function computeScrollOffsets(item) { 287 | var it = item; 288 | var p = it.props; 289 | var el = it.el; 290 | var parent = it.parent; 291 | var isCustom = !this.isWin && p.positionVal === 'fixed'; 292 | var isTop = p.verticalPosition !== 'bottom'; 293 | var scrollElOffset = isCustom ? this.getTopPosition(p.scrollEl) : 0; 294 | var stickyStart = isCustom ? this.getTopPosition(parent) - scrollElOffset : this.getTopPosition(parent); 295 | var stickyChangeOffset = p.customStickyChangeNumber !== null ? p.customStickyChangeNumber : el.offsetHeight; 296 | var parentBottom = stickyStart + parent.offsetHeight; 297 | it.offset = !isCustom ? scrollElOffset + p.stickyBitStickyOffset : 0; 298 | it.stickyStart = isTop ? stickyStart - it.offset : 0; 299 | it.stickyChange = it.stickyStart + stickyChangeOffset; 300 | it.stickyStop = isTop ? parentBottom - (el.offsetHeight + it.offset) : parentBottom - window.innerHeight; 301 | } 302 | /* 303 | toggleClasses ⚖️ 304 | --- 305 | toggles classes (for older browser support) 306 | r = removed class 307 | a = added class 308 | */ 309 | ; 310 | 311 | _proto.toggleClasses = function toggleClasses(el, r, a) { 312 | var e = el; 313 | var cArray = e.className.split(' '); 314 | if (a && cArray.indexOf(a) === -1) cArray.push(a); 315 | var rItem = cArray.indexOf(r); 316 | if (rItem !== -1) cArray.splice(rItem, 1); 317 | e.className = cArray.join(' '); 318 | } 319 | /* 320 | manageState 📝 321 | --- 322 | - defines the state 323 | - normal 324 | - sticky 325 | - stuck 326 | */ 327 | ; 328 | 329 | _proto.manageState = function manageState(item) { 330 | var _this3 = this; 331 | 332 | // cache object 333 | var it = item; 334 | var p = it.props; 335 | var state = it.state; 336 | var stateChange = it.stateChange; 337 | var start = it.stickyStart; 338 | var change = it.stickyChange; 339 | var stop = it.stickyStop; // cache props 340 | 341 | var pv = p.positionVal; 342 | var se = p.scrollEl; 343 | var sticky = p.stickyClass; 344 | var stickyChange = p.stickyChangeClass; 345 | var stuck = p.stuckClass; 346 | var vp = p.verticalPosition; 347 | var isTop = vp !== 'bottom'; 348 | var aS = p.applyStyle; 349 | var ns = p.noStyles; 350 | /* 351 | requestAnimationFrame 352 | --- 353 | - use rAF 354 | - or stub rAF 355 | */ 356 | 357 | var rAFStub = function rAFDummy(f) { 358 | f(); 359 | }; 360 | 361 | var rAF = !this.isWin ? rAFStub : window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || rAFStub; 362 | /* 363 | define scroll vars 364 | --- 365 | - scroll 366 | - notSticky 367 | - isSticky 368 | - isStuck 369 | */ 370 | 371 | var scroll = this.isWin ? window.scrollY || window.pageYOffset : se.scrollTop; 372 | var notSticky = scroll > start && scroll < stop && (state === 'default' || state === 'stuck'); 373 | var isSticky = isTop && scroll <= start && (state === 'sticky' || state === 'stuck'); 374 | var isStuck = scroll >= stop && state === 'sticky'; 375 | /* 376 | Unnamed arrow functions within this block 377 | --- 378 | - help wanted or discussion 379 | - view test.stickybits.js 380 | - `stickybits .manageState `position: fixed` interface` for more awareness 👀 381 | */ 382 | 383 | if (notSticky) { 384 | it.state = 'sticky'; 385 | } else if (isSticky) { 386 | it.state = 'default'; 387 | } else if (isStuck) { 388 | it.state = 'stuck'; 389 | } 390 | 391 | var isStickyChange = scroll >= change && scroll <= stop; 392 | var isNotStickyChange = scroll < change / 2 || scroll > stop; 393 | 394 | if (isNotStickyChange) { 395 | it.stateChange = 'default'; 396 | } else if (isStickyChange) { 397 | it.stateChange = 'sticky'; 398 | } // Only apply new styles if the state has changed 399 | 400 | 401 | if (state === it.state && stateChange === it.stateChange) return; 402 | rAF(function () { 403 | var _styles2, _classes, _styles3, _extends2, _classes2, _style$classes; 404 | 405 | var stateStyles = { 406 | sticky: { 407 | styles: (_styles2 = { 408 | position: pv, 409 | top: '', 410 | bottom: '' 411 | }, _styles2[vp] = p.stickyBitStickyOffset + "px", _styles2), 412 | classes: (_classes = {}, _classes[sticky] = true, _classes) 413 | }, 414 | default: { 415 | styles: (_styles3 = {}, _styles3[vp] = '', _styles3), 416 | classes: {} 417 | }, 418 | stuck: { 419 | styles: _extends((_extends2 = {}, _extends2[vp] = '', _extends2), pv === 'fixed' && !ns || !_this3.isWin ? { 420 | position: 'absolute', 421 | top: '', 422 | bottom: '0' 423 | } : {}), 424 | classes: (_classes2 = {}, _classes2[stuck] = true, _classes2) 425 | } 426 | }; 427 | 428 | if (pv === 'fixed') { 429 | stateStyles.default.styles.position = ''; 430 | } 431 | 432 | var style = stateStyles[it.state]; 433 | style.classes = (_style$classes = {}, _style$classes[stuck] = !!style.classes[stuck], _style$classes[sticky] = !!style.classes[sticky], _style$classes[stickyChange] = isStickyChange, _style$classes); 434 | aS(style, item); 435 | }); 436 | } 437 | /* 438 | applyStyle 439 | --- 440 | - apply the given styles and classes to the element 441 | */ 442 | ; 443 | 444 | _proto.applyStyle = function applyStyle(_ref, item) { 445 | var styles = _ref.styles, 446 | classes = _ref.classes; 447 | // cache object 448 | var it = item; 449 | var e = it.el; 450 | var p = it.props; 451 | var stl = e.style; // cache props 452 | 453 | var ns = p.noStyles; 454 | var cArray = e.className.split(' '); // Disable due to bug with old versions of eslint-scope and for ... in 455 | // https://github.com/eslint/eslint/issues/12117 456 | // eslint-disable-next-line no-unused-vars 457 | 458 | for (var cls in classes) { 459 | var addClass = classes[cls]; 460 | 461 | if (addClass) { 462 | if (cArray.indexOf(cls) === -1) cArray.push(cls); 463 | } else { 464 | var idx = cArray.indexOf(cls); 465 | if (idx !== -1) cArray.splice(idx, 1); 466 | } 467 | } 468 | 469 | e.className = cArray.join(' '); 470 | 471 | if (styles['position']) { 472 | stl['position'] = styles['position']; 473 | } 474 | 475 | if (ns) return; // eslint-disable-next-line no-unused-vars 476 | 477 | for (var key in styles) { 478 | stl[key] = styles[key]; 479 | } 480 | }; 481 | 482 | _proto.update = function update(updatedProps) { 483 | var _this4 = this; 484 | 485 | if (updatedProps === void 0) { 486 | updatedProps = null; 487 | } 488 | 489 | this.instances.forEach(function (instance) { 490 | _this4.computeScrollOffsets(instance); 491 | 492 | if (updatedProps) { 493 | // eslint-disable-next-line no-unused-vars 494 | for (var updatedProp in updatedProps) { 495 | instance.props[updatedProp] = updatedProps[updatedProp]; 496 | } 497 | } 498 | }); 499 | return this; 500 | } 501 | /* 502 | removes an instance 👋 503 | -------- 504 | - cleanup instance 505 | */ 506 | ; 507 | 508 | _proto.removeInstance = function removeInstance(instance) { 509 | var _styles4, _classes3; 510 | 511 | var e = instance.el; 512 | var p = instance.props; 513 | this.applyStyle({ 514 | styles: (_styles4 = { 515 | position: '' 516 | }, _styles4[p.verticalPosition] = '', _styles4), 517 | classes: (_classes3 = {}, _classes3[p.stickyClass] = '', _classes3[p.stuckClass] = '', _classes3) 518 | }, instance); 519 | this.toggleClasses(e.parentNode, p.parentClass); 520 | } 521 | /* 522 | cleanup 🛁 523 | -------- 524 | - cleans up each instance 525 | - clears instance 526 | */ 527 | ; 528 | 529 | _proto.cleanup = function cleanup() { 530 | for (var i = 0; i < this.instances.length; i += 1) { 531 | var instance = this.instances[i]; 532 | 533 | if (instance.stateContainer) { 534 | instance.props.scrollEl.removeEventListener('scroll', instance.stateContainer); 535 | } 536 | 537 | this.removeInstance(instance); 538 | } 539 | 540 | this.manageState = false; 541 | this.instances = []; 542 | }; 543 | 544 | return Stickybits; 545 | }(); 546 | /* 547 | export 548 | -------- 549 | exports StickBits to be used 🏁 550 | */ 551 | 552 | 553 | function stickybits(target, o) { 554 | return new Stickybits(target, o); 555 | } 556 | 557 | return stickybits; 558 | 559 | }))); 560 | -------------------------------------------------------------------------------- /dist/stickybits.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | stickybits - Stickybits is a lightweight alternative to `position: sticky` polyfills 3 | @version v3.7.4 4 | @link https://github.com/dollarshaveclub/stickybits#readme 5 | @author Jeff Wainwright (https://jeffry.in) 6 | @license MIT 7 | **/ 8 | !function(t,s){"object"==typeof exports&&"undefined"!=typeof module?module.exports=s():"function"==typeof define&&define.amd?define(s):(t=t||self).stickybits=s()}(this,function(){"use strict";function b(){return(b=Object.assign||function(t){for(var s=1;s (https://jeffry.in) 6 | @license MIT 7 | **/ 8 | (function (factory) { 9 | typeof define === 'function' && define.amd ? define(factory) : 10 | factory(); 11 | }((function () { 'use strict'; 12 | 13 | function _extends() { 14 | _extends = Object.assign || function (target) { 15 | for (var i = 1; i < arguments.length; i++) { 16 | var source = arguments[i]; 17 | 18 | for (var key in source) { 19 | if (Object.prototype.hasOwnProperty.call(source, key)) { 20 | target[key] = source[key]; 21 | } 22 | } 23 | } 24 | 25 | return target; 26 | }; 27 | 28 | return _extends.apply(this, arguments); 29 | } 30 | 31 | /* 32 | STICKYBITS 💉 33 | -------- 34 | > a lightweight alternative to `position: sticky` polyfills 🍬 35 | -------- 36 | - each method is documented above it our view the readme 37 | - Stickybits does not manage polymorphic functionality (position like properties) 38 | * polymorphic functionality: (in the context of describing Stickybits) 39 | means making things like `position: sticky` be loosely supported with position fixed. 40 | It also means that features like `useStickyClasses` takes on styles like `position: fixed`. 41 | -------- 42 | defaults 🔌 43 | -------- 44 | - version = `package.json` version 45 | - userAgent = viewer browser agent 46 | - target = DOM element selector 47 | - noStyles = boolean 48 | - offset = number 49 | - parentClass = 'string' 50 | - scrollEl = window || DOM element selector || DOM element 51 | - stickyClass = 'string' 52 | - stuckClass = 'string' 53 | - useStickyClasses = boolean 54 | - useFixed = boolean 55 | - useGetBoundingClientRect = boolean 56 | - verticalPosition = 'string' 57 | - applyStyle = function 58 | -------- 59 | props🔌 60 | -------- 61 | - p = props {object} 62 | -------- 63 | instance note 64 | -------- 65 | - stickybits parent methods return this 66 | - stickybits instance methods return an instance item 67 | -------- 68 | nomenclature 69 | -------- 70 | - target => el => e 71 | - props => o || p 72 | - instance => item => it 73 | -------- 74 | methods 75 | -------- 76 | - .definePosition = defines sticky or fixed 77 | - .addInstance = an array of objects for each Stickybits Target 78 | - .getClosestParent = gets the parent for non-window scroll 79 | - .getTopPosition = gets the element top pixel position from the viewport 80 | - .computeScrollOffsets = computes scroll position 81 | - .toggleClasses = older browser toggler 82 | - .manageState = manages sticky state 83 | - .removeInstance = removes an instance 84 | - .cleanup = removes all Stickybits instances and cleans up dom from stickybits 85 | */ 86 | var Stickybits = 87 | /*#__PURE__*/ 88 | function () { 89 | function Stickybits(target, obj) { 90 | var _this = this; 91 | 92 | var o = typeof obj !== 'undefined' ? obj : {}; 93 | this.version = '3.7.4'; 94 | this.userAgent = window.navigator.userAgent || 'no `userAgent` provided by the browser'; 95 | this.props = { 96 | customStickyChangeNumber: o.customStickyChangeNumber || null, 97 | noStyles: o.noStyles || false, 98 | stickyBitStickyOffset: o.stickyBitStickyOffset || 0, 99 | parentClass: o.parentClass || 'js-stickybit-parent', 100 | scrollEl: typeof o.scrollEl === 'string' ? document.querySelector(o.scrollEl) : o.scrollEl || window, 101 | stickyClass: o.stickyClass || 'js-is-sticky', 102 | stuckClass: o.stuckClass || 'js-is-stuck', 103 | stickyChangeClass: o.stickyChangeClass || 'js-is-sticky--change', 104 | useStickyClasses: o.useStickyClasses || false, 105 | useFixed: o.useFixed || false, 106 | useGetBoundingClientRect: o.useGetBoundingClientRect || false, 107 | verticalPosition: o.verticalPosition || 'top', 108 | applyStyle: o.applyStyle || function (item, style) { 109 | return _this.applyStyle(item, style); 110 | } 111 | /* 112 | define positionVal after the setting of props, because definePosition looks at the props.useFixed 113 | ---- 114 | - uses a computed (`.definePosition()`) 115 | - defined the position 116 | */ 117 | 118 | }; 119 | this.props.positionVal = this.definePosition() || 'fixed'; 120 | this.instances = []; 121 | var _this$props = this.props, 122 | positionVal = _this$props.positionVal, 123 | verticalPosition = _this$props.verticalPosition, 124 | noStyles = _this$props.noStyles, 125 | stickyBitStickyOffset = _this$props.stickyBitStickyOffset; 126 | var verticalPositionStyle = verticalPosition === 'top' && !noStyles ? stickyBitStickyOffset + "px" : ''; 127 | var positionStyle = positionVal !== 'fixed' ? positionVal : ''; 128 | this.els = typeof target === 'string' ? document.querySelectorAll(target) : target; 129 | if (!('length' in this.els)) this.els = [this.els]; 130 | 131 | for (var i = 0; i < this.els.length; i++) { 132 | var _styles; 133 | 134 | var el = this.els[i]; 135 | var instance = this.addInstance(el, this.props); // set vertical position 136 | 137 | this.props.applyStyle({ 138 | styles: (_styles = {}, _styles[verticalPosition] = verticalPositionStyle, _styles.position = positionStyle, _styles), 139 | classes: {} 140 | }, instance); 141 | this.manageState(instance); // instances are an array of objects 142 | 143 | this.instances.push(instance); 144 | } 145 | } 146 | /* 147 | setStickyPosition ✔️ 148 | -------- 149 | — most basic thing stickybits does 150 | => checks to see if position sticky is supported 151 | => defined the position to be used 152 | => stickybits works accordingly 153 | */ 154 | 155 | 156 | var _proto = Stickybits.prototype; 157 | 158 | _proto.definePosition = function definePosition() { 159 | var stickyProp; 160 | 161 | if (this.props.useFixed) { 162 | stickyProp = 'fixed'; 163 | } else { 164 | var prefix = ['', '-o-', '-webkit-', '-moz-', '-ms-']; 165 | var test = document.head.style; 166 | 167 | for (var i = 0; i < prefix.length; i += 1) { 168 | test.position = prefix[i] + "sticky"; 169 | } 170 | 171 | stickyProp = test.position ? test.position : 'fixed'; 172 | test.position = ''; 173 | } 174 | 175 | return stickyProp; 176 | } 177 | /* 178 | addInstance ✔️ 179 | -------- 180 | — manages instances of items 181 | - takes in an el and props 182 | - returns an item object 183 | --- 184 | - target = el 185 | - o = {object} = props 186 | - scrollEl = 'string' | object 187 | - verticalPosition = number 188 | - off = boolean 189 | - parentClass = 'string' 190 | - stickyClass = 'string' 191 | - stuckClass = 'string' 192 | --- 193 | - defined later 194 | - parent = dom element 195 | - state = 'string' 196 | - offset = number 197 | - stickyStart = number 198 | - stickyStop = number 199 | - returns an instance object 200 | */ 201 | ; 202 | 203 | _proto.addInstance = function addInstance(el, props) { 204 | var _this2 = this; 205 | 206 | var item = { 207 | el: el, 208 | parent: el.parentNode, 209 | props: props 210 | }; 211 | 212 | if (props.positionVal === 'fixed' || props.useStickyClasses) { 213 | this.isWin = this.props.scrollEl === window; 214 | var se = this.isWin ? window : this.getClosestParent(item.el, item.props.scrollEl); 215 | this.computeScrollOffsets(item); 216 | this.toggleClasses(item.parent, '', props.parentClass); 217 | item.state = 'default'; 218 | item.stateChange = 'default'; 219 | 220 | item.stateContainer = function () { 221 | return _this2.manageState(item); 222 | }; 223 | 224 | se.addEventListener('scroll', item.stateContainer); 225 | } 226 | 227 | return item; 228 | } 229 | /* 230 | -------- 231 | getParent 👨‍ 232 | -------- 233 | - a helper function that gets the target element's parent selected el 234 | - only used for non `window` scroll elements 235 | - supports older browsers 236 | */ 237 | ; 238 | 239 | _proto.getClosestParent = function getClosestParent(el, match) { 240 | // p = parent element 241 | var p = match; 242 | var e = el; 243 | if (e.parentElement === p) return p; // traverse up the dom tree until we get to the parent 244 | 245 | while (e.parentElement !== p) { 246 | e = e.parentElement; 247 | } // return parent element 248 | 249 | 250 | return p; 251 | } 252 | /* 253 | -------- 254 | getTopPosition 255 | -------- 256 | - a helper function that gets the topPosition of a Stickybit element 257 | - from the top level of the DOM 258 | */ 259 | ; 260 | 261 | _proto.getTopPosition = function getTopPosition(el) { 262 | if (this.props.useGetBoundingClientRect) { 263 | return el.getBoundingClientRect().top + (this.props.scrollEl.pageYOffset || document.documentElement.scrollTop); 264 | } 265 | 266 | var topPosition = 0; 267 | 268 | do { 269 | topPosition = el.offsetTop + topPosition; 270 | } while (el = el.offsetParent); 271 | 272 | return topPosition; 273 | } 274 | /* 275 | computeScrollOffsets 📊 276 | --- 277 | computeScrollOffsets for Stickybits 278 | - defines 279 | - offset 280 | - start 281 | - stop 282 | */ 283 | ; 284 | 285 | _proto.computeScrollOffsets = function computeScrollOffsets(item) { 286 | var it = item; 287 | var p = it.props; 288 | var el = it.el; 289 | var parent = it.parent; 290 | var isCustom = !this.isWin && p.positionVal === 'fixed'; 291 | var isTop = p.verticalPosition !== 'bottom'; 292 | var scrollElOffset = isCustom ? this.getTopPosition(p.scrollEl) : 0; 293 | var stickyStart = isCustom ? this.getTopPosition(parent) - scrollElOffset : this.getTopPosition(parent); 294 | var stickyChangeOffset = p.customStickyChangeNumber !== null ? p.customStickyChangeNumber : el.offsetHeight; 295 | var parentBottom = stickyStart + parent.offsetHeight; 296 | it.offset = !isCustom ? scrollElOffset + p.stickyBitStickyOffset : 0; 297 | it.stickyStart = isTop ? stickyStart - it.offset : 0; 298 | it.stickyChange = it.stickyStart + stickyChangeOffset; 299 | it.stickyStop = isTop ? parentBottom - (el.offsetHeight + it.offset) : parentBottom - window.innerHeight; 300 | } 301 | /* 302 | toggleClasses ⚖️ 303 | --- 304 | toggles classes (for older browser support) 305 | r = removed class 306 | a = added class 307 | */ 308 | ; 309 | 310 | _proto.toggleClasses = function toggleClasses(el, r, a) { 311 | var e = el; 312 | var cArray = e.className.split(' '); 313 | if (a && cArray.indexOf(a) === -1) cArray.push(a); 314 | var rItem = cArray.indexOf(r); 315 | if (rItem !== -1) cArray.splice(rItem, 1); 316 | e.className = cArray.join(' '); 317 | } 318 | /* 319 | manageState 📝 320 | --- 321 | - defines the state 322 | - normal 323 | - sticky 324 | - stuck 325 | */ 326 | ; 327 | 328 | _proto.manageState = function manageState(item) { 329 | var _this3 = this; 330 | 331 | // cache object 332 | var it = item; 333 | var p = it.props; 334 | var state = it.state; 335 | var stateChange = it.stateChange; 336 | var start = it.stickyStart; 337 | var change = it.stickyChange; 338 | var stop = it.stickyStop; // cache props 339 | 340 | var pv = p.positionVal; 341 | var se = p.scrollEl; 342 | var sticky = p.stickyClass; 343 | var stickyChange = p.stickyChangeClass; 344 | var stuck = p.stuckClass; 345 | var vp = p.verticalPosition; 346 | var isTop = vp !== 'bottom'; 347 | var aS = p.applyStyle; 348 | var ns = p.noStyles; 349 | /* 350 | requestAnimationFrame 351 | --- 352 | - use rAF 353 | - or stub rAF 354 | */ 355 | 356 | var rAFStub = function rAFDummy(f) { 357 | f(); 358 | }; 359 | 360 | var rAF = !this.isWin ? rAFStub : window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame || rAFStub; 361 | /* 362 | define scroll vars 363 | --- 364 | - scroll 365 | - notSticky 366 | - isSticky 367 | - isStuck 368 | */ 369 | 370 | var scroll = this.isWin ? window.scrollY || window.pageYOffset : se.scrollTop; 371 | var notSticky = scroll > start && scroll < stop && (state === 'default' || state === 'stuck'); 372 | var isSticky = isTop && scroll <= start && (state === 'sticky' || state === 'stuck'); 373 | var isStuck = scroll >= stop && state === 'sticky'; 374 | /* 375 | Unnamed arrow functions within this block 376 | --- 377 | - help wanted or discussion 378 | - view test.stickybits.js 379 | - `stickybits .manageState `position: fixed` interface` for more awareness 👀 380 | */ 381 | 382 | if (notSticky) { 383 | it.state = 'sticky'; 384 | } else if (isSticky) { 385 | it.state = 'default'; 386 | } else if (isStuck) { 387 | it.state = 'stuck'; 388 | } 389 | 390 | var isStickyChange = scroll >= change && scroll <= stop; 391 | var isNotStickyChange = scroll < change / 2 || scroll > stop; 392 | 393 | if (isNotStickyChange) { 394 | it.stateChange = 'default'; 395 | } else if (isStickyChange) { 396 | it.stateChange = 'sticky'; 397 | } // Only apply new styles if the state has changed 398 | 399 | 400 | if (state === it.state && stateChange === it.stateChange) return; 401 | rAF(function () { 402 | var _styles2, _classes, _styles3, _extends2, _classes2, _style$classes; 403 | 404 | var stateStyles = { 405 | sticky: { 406 | styles: (_styles2 = { 407 | position: pv, 408 | top: '', 409 | bottom: '' 410 | }, _styles2[vp] = p.stickyBitStickyOffset + "px", _styles2), 411 | classes: (_classes = {}, _classes[sticky] = true, _classes) 412 | }, 413 | default: { 414 | styles: (_styles3 = {}, _styles3[vp] = '', _styles3), 415 | classes: {} 416 | }, 417 | stuck: { 418 | styles: _extends((_extends2 = {}, _extends2[vp] = '', _extends2), pv === 'fixed' && !ns || !_this3.isWin ? { 419 | position: 'absolute', 420 | top: '', 421 | bottom: '0' 422 | } : {}), 423 | classes: (_classes2 = {}, _classes2[stuck] = true, _classes2) 424 | } 425 | }; 426 | 427 | if (pv === 'fixed') { 428 | stateStyles.default.styles.position = ''; 429 | } 430 | 431 | var style = stateStyles[it.state]; 432 | style.classes = (_style$classes = {}, _style$classes[stuck] = !!style.classes[stuck], _style$classes[sticky] = !!style.classes[sticky], _style$classes[stickyChange] = isStickyChange, _style$classes); 433 | aS(style, item); 434 | }); 435 | } 436 | /* 437 | applyStyle 438 | --- 439 | - apply the given styles and classes to the element 440 | */ 441 | ; 442 | 443 | _proto.applyStyle = function applyStyle(_ref, item) { 444 | var styles = _ref.styles, 445 | classes = _ref.classes; 446 | // cache object 447 | var it = item; 448 | var e = it.el; 449 | var p = it.props; 450 | var stl = e.style; // cache props 451 | 452 | var ns = p.noStyles; 453 | var cArray = e.className.split(' '); // Disable due to bug with old versions of eslint-scope and for ... in 454 | // https://github.com/eslint/eslint/issues/12117 455 | // eslint-disable-next-line no-unused-vars 456 | 457 | for (var cls in classes) { 458 | var addClass = classes[cls]; 459 | 460 | if (addClass) { 461 | if (cArray.indexOf(cls) === -1) cArray.push(cls); 462 | } else { 463 | var idx = cArray.indexOf(cls); 464 | if (idx !== -1) cArray.splice(idx, 1); 465 | } 466 | } 467 | 468 | e.className = cArray.join(' '); 469 | 470 | if (styles['position']) { 471 | stl['position'] = styles['position']; 472 | } 473 | 474 | if (ns) return; // eslint-disable-next-line no-unused-vars 475 | 476 | for (var key in styles) { 477 | stl[key] = styles[key]; 478 | } 479 | }; 480 | 481 | _proto.update = function update(updatedProps) { 482 | var _this4 = this; 483 | 484 | if (updatedProps === void 0) { 485 | updatedProps = null; 486 | } 487 | 488 | this.instances.forEach(function (instance) { 489 | _this4.computeScrollOffsets(instance); 490 | 491 | if (updatedProps) { 492 | // eslint-disable-next-line no-unused-vars 493 | for (var updatedProp in updatedProps) { 494 | instance.props[updatedProp] = updatedProps[updatedProp]; 495 | } 496 | } 497 | }); 498 | return this; 499 | } 500 | /* 501 | removes an instance 👋 502 | -------- 503 | - cleanup instance 504 | */ 505 | ; 506 | 507 | _proto.removeInstance = function removeInstance(instance) { 508 | var _styles4, _classes3; 509 | 510 | var e = instance.el; 511 | var p = instance.props; 512 | this.applyStyle({ 513 | styles: (_styles4 = { 514 | position: '' 515 | }, _styles4[p.verticalPosition] = '', _styles4), 516 | classes: (_classes3 = {}, _classes3[p.stickyClass] = '', _classes3[p.stuckClass] = '', _classes3) 517 | }, instance); 518 | this.toggleClasses(e.parentNode, p.parentClass); 519 | } 520 | /* 521 | cleanup 🛁 522 | -------- 523 | - cleans up each instance 524 | - clears instance 525 | */ 526 | ; 527 | 528 | _proto.cleanup = function cleanup() { 529 | for (var i = 0; i < this.instances.length; i += 1) { 530 | var instance = this.instances[i]; 531 | 532 | if (instance.stateContainer) { 533 | instance.props.scrollEl.removeEventListener('scroll', instance.stateContainer); 534 | } 535 | 536 | this.removeInstance(instance); 537 | } 538 | 539 | this.manageState = false; 540 | this.instances = []; 541 | }; 542 | 543 | return Stickybits; 544 | }(); 545 | /* 546 | export 547 | -------- 548 | exports StickBits to be used 🏁 549 | */ 550 | 551 | 552 | function stickybits(target, o) { 553 | return new Stickybits(target, o); 554 | } 555 | 556 | if (typeof window !== 'undefined') { 557 | var plugin = window.u; 558 | 559 | if (plugin) { 560 | plugin.prototype.stickybits = function stickybitsPlugin(opts) { 561 | return stickybits(this, opts); 562 | }; 563 | } 564 | } 565 | 566 | }))); 567 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | {"compilerOptions":{"target":"es6","experimentalDecorators":true},"exclude":["node_modules","bower_components","tmp","vendor",".git","dist"]} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stickybits", 3 | "version": "3.7.4", 4 | "description": "Stickybits is a lightweight alternative to `position: sticky` polyfills", 5 | "main": "dist/stickybits.js", 6 | "module": "dist/stickybits.es.js", 7 | "unpkg": "dist/stickybits.min.js", 8 | "types": "types/index.d.ts", 9 | "scripts": { 10 | "build": "npm run test:unit && npm run build:rollup", 11 | "build:rollup": "rollup --config configs/rollup.config.js", 12 | "lint": "eslint . --fix", 13 | "lint:ci": "eslint .", 14 | "chore:delete-changelog-branch": "if git show-ref --quiet refs/heads/chore-changelog; then git branch -D chore-changelog; fi", 15 | "chore:branch": "git checkout -b chore-changelog", 16 | "chore:changelog": "conventional-changelog -p eslint -i CHANGELOG.md -s -r 0", 17 | "chore:setup-next-work": "git checkout master && npm run chore:delete-changelog-branch", 18 | "chore:pr": "git add . && git commit -m '[chore] updates changelog' --no-verify && git push origin chore-changelog -f", 19 | "chore:setup-changelog": "git checkout master && git pull", 20 | "chore": "npm run chore:delete-changelog-branch && npm run chore:branch && npm run chore:changelog && npm run chore:pr && npm run chore:setup-next-work", 21 | "grammar": "write-good *.md --no-passive", 22 | "markdownlint": "markdownlint *.md", 23 | "prepush": "npm run build && npm test", 24 | "pre-commit-msg": "Echo 'Running pre-commit checks...' && exit 0", 25 | "postpublish": "git tag $npm_package_version && git push origin --tags && npm run chore", 26 | "release": "npm run lint && npm run build && npm test && npm run report:coverage", 27 | "report:coverage": "nyc report --reporter=lcov > coverage.lcov && codecov", 28 | "spelling": "mdspell '**/*.md' '!**/node_modules/**/*.md' --ignore-numbers", 29 | "spelling:ci": "mdspell '**/*.md' '!**/node_modules/**/*.md' --ignore-numbers --report", 30 | "start": "npm i", 31 | "test:es-check": "es-check es5 dist/stickybits.min.js dist/stickybits.js dist/jquery.stickybits.js dist/jquery.stickybits.min.js dist/umbrella.stickybits.js dist/umbrella.stickybits.min.js", 32 | "test:unit": "jest --coverage", 33 | "test:acceptance": "node ./scripts/acceptance.js --coverage", 34 | "test": "npm run markdownlint && npm run test:unit && npm run test:acceptance" 35 | }, 36 | "jest": { 37 | "testRegex": "./tests/unit/.*.js$", 38 | "testURL": "http://localhost/" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/dollarshaveclub/stickybits/issues" 42 | }, 43 | "homepage": "https://github.com/dollarshaveclub/stickybits#readme", 44 | "repository": { 45 | "url": "https://github.com/dollarshaveclub/stickybits.git", 46 | "type": "git" 47 | }, 48 | "author": "Jeff Wainwright (https://jeffry.in)", 49 | "license": "MIT", 50 | "files": [ 51 | "dist", 52 | "types", 53 | "src" 54 | ], 55 | "devDependencies": { 56 | "@babel/core": "^7.0.0-beta.44", 57 | "@babel/plugin-proposal-class-properties": "^7.0.0-beta.44", 58 | "@babel/preset-env": "^7.0.0-beta.40", 59 | "babel-core": "^7.0.0-bridge.0", 60 | "babel-jest": "^23.0.0", 61 | "codecov": "^3.0.2", 62 | "conventional-changelog-cli": "^2.0.11", 63 | "es-check": "^5.0.0", 64 | "eslint": "^6.6.0", 65 | "eslint-config-dollarshaveclub": "^3.1.0", 66 | "eslint-plugin-import": "^2.19.1", 67 | "eslint-plugin-node": "^11.0.0", 68 | "eslint-plugin-standard": "^4.0.0", 69 | "husky": "^4.0.10", 70 | "jest": "^22.0.0", 71 | "jquery": "^3.2.1", 72 | "markdown-spellcheck": "^1.3.1", 73 | "markdownlint-cli": "^0.22.0", 74 | "node-qunit-phantomjs": "^2.0.0", 75 | "nyc": "^15.0.0", 76 | "qunit": "^2.6.1", 77 | "rollup": "1.31.1", 78 | "rollup-plugin-babel": "^4.1.0", 79 | "rollup-plugin-replace": "^2.0.0", 80 | "rollup-plugin-uglify": "^6.0.0", 81 | "write-good": "^1.0.0" 82 | }, 83 | "keywords": [ 84 | "stick", 85 | "fixed", 86 | "sticky", 87 | "position", 88 | "navigation", 89 | "nav", 90 | "dom", 91 | "simple", 92 | "javascript", 93 | "stuck", 94 | "waypoint", 95 | "scroll", 96 | "stickyheader", 97 | "stickynavigation", 98 | "fixedheader" 99 | ], 100 | "browserslist": [ 101 | "defaults", 102 | "ie >= 9" 103 | ] 104 | } 105 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /scripts/acceptance.js: -------------------------------------------------------------------------------- 1 | const qunit = require('node-qunit-phantomjs') 2 | 3 | qunit('tests/acceptance/cleanup/index.html') 4 | qunit('tests/acceptance/monitoring/index.html') 5 | qunit('tests/acceptance/multiple/index.html') 6 | qunit('tests/acceptance/multiple-sticky-classes/index.html') 7 | qunit('tests/acceptance/offset/index.html') 8 | qunit('tests/acceptance/scrollTo/index.html') 9 | qunit('tests/acceptance/update/index.html') 10 | qunit('tests/acceptance/use-fixed/index.html') 11 | -------------------------------------------------------------------------------- /src/jquery.stickybits.js: -------------------------------------------------------------------------------- 1 | import stickybits from './stickybits' 2 | 3 | if (typeof window !== 'undefined') { 4 | const plugin = window.$ || window.jQuery || window.Zepto 5 | if (plugin) { 6 | plugin.fn.stickybits = function stickybitsPlugin (opts) { 7 | return stickybits(this, opts) 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/stickybits.js: -------------------------------------------------------------------------------- 1 | /* 2 | STICKYBITS 💉 3 | -------- 4 | > a lightweight alternative to `position: sticky` polyfills 🍬 5 | -------- 6 | - each method is documented above it our view the readme 7 | - Stickybits does not manage polymorphic functionality (position like properties) 8 | * polymorphic functionality: (in the context of describing Stickybits) 9 | means making things like `position: sticky` be loosely supported with position fixed. 10 | It also means that features like `useStickyClasses` takes on styles like `position: fixed`. 11 | -------- 12 | defaults 🔌 13 | -------- 14 | - version = `package.json` version 15 | - userAgent = viewer browser agent 16 | - target = DOM element selector 17 | - noStyles = boolean 18 | - offset = number 19 | - parentClass = 'string' 20 | - scrollEl = window || DOM element selector || DOM element 21 | - stickyClass = 'string' 22 | - stuckClass = 'string' 23 | - useStickyClasses = boolean 24 | - useFixed = boolean 25 | - useGetBoundingClientRect = boolean 26 | - verticalPosition = 'string' 27 | - applyStyle = function 28 | -------- 29 | props🔌 30 | -------- 31 | - p = props {object} 32 | -------- 33 | instance note 34 | -------- 35 | - stickybits parent methods return this 36 | - stickybits instance methods return an instance item 37 | -------- 38 | nomenclature 39 | -------- 40 | - target => el => e 41 | - props => o || p 42 | - instance => item => it 43 | -------- 44 | methods 45 | -------- 46 | - .definePosition = defines sticky or fixed 47 | - .addInstance = an array of objects for each Stickybits Target 48 | - .getClosestParent = gets the parent for non-window scroll 49 | - .getTopPosition = gets the element top pixel position from the viewport 50 | - .computeScrollOffsets = computes scroll position 51 | - .toggleClasses = older browser toggler 52 | - .manageState = manages sticky state 53 | - .removeInstance = removes an instance 54 | - .cleanup = removes all Stickybits instances and cleans up dom from stickybits 55 | */ 56 | class Stickybits { 57 | constructor (target, obj) { 58 | const o = typeof obj !== 'undefined' ? obj : {} 59 | this.version = 'VERSION' 60 | this.userAgent = window.navigator.userAgent || 'no `userAgent` provided by the browser' 61 | this.props = { 62 | customStickyChangeNumber: o.customStickyChangeNumber || null, 63 | noStyles: o.noStyles || false, 64 | stickyBitStickyOffset: o.stickyBitStickyOffset || 0, 65 | parentClass: o.parentClass || 'js-stickybit-parent', 66 | scrollEl: typeof o.scrollEl === 'string' ? document.querySelector(o.scrollEl) : o.scrollEl || window, 67 | stickyClass: o.stickyClass || 'js-is-sticky', 68 | stuckClass: o.stuckClass || 'js-is-stuck', 69 | stickyChangeClass: o.stickyChangeClass || 'js-is-sticky--change', 70 | useStickyClasses: o.useStickyClasses || false, 71 | useFixed: o.useFixed || false, 72 | useGetBoundingClientRect: o.useGetBoundingClientRect || false, 73 | verticalPosition: o.verticalPosition || 'top', 74 | applyStyle: o.applyStyle || ((item, style) => this.applyStyle(item, style)), 75 | } 76 | /* 77 | define positionVal after the setting of props, because definePosition looks at the props.useFixed 78 | ---- 79 | - uses a computed (`.definePosition()`) 80 | - defined the position 81 | */ 82 | this.props.positionVal = this.definePosition() || 'fixed' 83 | 84 | this.instances = [] 85 | 86 | const { 87 | positionVal, 88 | verticalPosition, 89 | noStyles, 90 | stickyBitStickyOffset, 91 | } = this.props 92 | const verticalPositionStyle = verticalPosition === 'top' && !noStyles ? `${stickyBitStickyOffset}px` : '' 93 | const positionStyle = positionVal !== 'fixed' ? positionVal : '' 94 | 95 | this.els = typeof target === 'string' ? document.querySelectorAll(target) : target 96 | 97 | if (!('length' in this.els)) this.els = [this.els] 98 | 99 | for (let i = 0; i < this.els.length; i++) { 100 | const el = this.els[i] 101 | 102 | var instance = this.addInstance(el, this.props) 103 | // set vertical position 104 | this.props.applyStyle( 105 | { 106 | styles: { 107 | [verticalPosition]: verticalPositionStyle, 108 | position: positionStyle, 109 | }, 110 | classes: {}, 111 | }, 112 | instance, 113 | ) 114 | this.manageState(instance) 115 | 116 | // instances are an array of objects 117 | this.instances.push(instance) 118 | } 119 | } 120 | 121 | /* 122 | setStickyPosition ✔️ 123 | -------- 124 | — most basic thing stickybits does 125 | => checks to see if position sticky is supported 126 | => defined the position to be used 127 | => stickybits works accordingly 128 | */ 129 | definePosition () { 130 | let stickyProp 131 | if (this.props.useFixed) { 132 | stickyProp = 'fixed' 133 | } else { 134 | const prefix = ['', '-o-', '-webkit-', '-moz-', '-ms-'] 135 | const test = document.head.style 136 | for (let i = 0; i < prefix.length; i += 1) { 137 | test.position = `${prefix[i]}sticky` 138 | } 139 | stickyProp = test.position ? test.position : 'fixed' 140 | test.position = '' 141 | } 142 | return stickyProp 143 | } 144 | 145 | /* 146 | addInstance ✔️ 147 | -------- 148 | — manages instances of items 149 | - takes in an el and props 150 | - returns an item object 151 | --- 152 | - target = el 153 | - o = {object} = props 154 | - scrollEl = 'string' | object 155 | - verticalPosition = number 156 | - off = boolean 157 | - parentClass = 'string' 158 | - stickyClass = 'string' 159 | - stuckClass = 'string' 160 | --- 161 | - defined later 162 | - parent = dom element 163 | - state = 'string' 164 | - offset = number 165 | - stickyStart = number 166 | - stickyStop = number 167 | - returns an instance object 168 | */ 169 | addInstance (el, props) { 170 | const item = { 171 | el, 172 | parent: el.parentNode, 173 | props, 174 | } 175 | if (props.positionVal === 'fixed' || props.useStickyClasses) { 176 | this.isWin = this.props.scrollEl === window 177 | const se = this.isWin ? window : this.getClosestParent(item.el, item.props.scrollEl) 178 | this.computeScrollOffsets(item) 179 | this.toggleClasses(item.parent, '', props.parentClass) 180 | item.state = 'default' 181 | item.stateChange = 'default' 182 | item.stateContainer = () => this.manageState(item) 183 | se.addEventListener('scroll', item.stateContainer) 184 | } 185 | return item 186 | } 187 | 188 | /* 189 | -------- 190 | getParent 👨‍ 191 | -------- 192 | - a helper function that gets the target element's parent selected el 193 | - only used for non `window` scroll elements 194 | - supports older browsers 195 | */ 196 | getClosestParent (el, match) { 197 | // p = parent element 198 | const p = match 199 | let e = el 200 | if (e.parentElement === p) return p 201 | // traverse up the dom tree until we get to the parent 202 | while (e.parentElement !== p) e = e.parentElement 203 | // return parent element 204 | return p 205 | } 206 | 207 | /* 208 | -------- 209 | getTopPosition 210 | -------- 211 | - a helper function that gets the topPosition of a Stickybit element 212 | - from the top level of the DOM 213 | */ 214 | getTopPosition (el) { 215 | if (this.props.useGetBoundingClientRect) { 216 | return el.getBoundingClientRect().top + (this.props.scrollEl.pageYOffset || document.documentElement.scrollTop) 217 | } 218 | let topPosition = 0 219 | do { 220 | topPosition = el.offsetTop + topPosition 221 | } while ((el = el.offsetParent)) 222 | return topPosition 223 | } 224 | 225 | /* 226 | computeScrollOffsets 📊 227 | --- 228 | computeScrollOffsets for Stickybits 229 | - defines 230 | - offset 231 | - start 232 | - stop 233 | */ 234 | computeScrollOffsets (item) { 235 | const it = item 236 | const p = it.props 237 | const el = it.el 238 | const parent = it.parent 239 | const isCustom = !this.isWin && p.positionVal === 'fixed' 240 | const isTop = p.verticalPosition !== 'bottom' 241 | const scrollElOffset = isCustom ? this.getTopPosition(p.scrollEl) : 0 242 | const stickyStart = isCustom 243 | ? this.getTopPosition(parent) - scrollElOffset 244 | : this.getTopPosition(parent) 245 | const stickyChangeOffset = p.customStickyChangeNumber !== null 246 | ? p.customStickyChangeNumber 247 | : el.offsetHeight 248 | const parentBottom = stickyStart + parent.offsetHeight 249 | it.offset = !isCustom ? scrollElOffset + p.stickyBitStickyOffset : 0 250 | it.stickyStart = isTop ? stickyStart - it.offset : 0 251 | it.stickyChange = it.stickyStart + stickyChangeOffset 252 | it.stickyStop = isTop 253 | ? parentBottom - (el.offsetHeight + it.offset) 254 | : parentBottom - window.innerHeight 255 | } 256 | 257 | /* 258 | toggleClasses ⚖️ 259 | --- 260 | toggles classes (for older browser support) 261 | r = removed class 262 | a = added class 263 | */ 264 | toggleClasses (el, r, a) { 265 | const e = el 266 | const cArray = e.className.split(' ') 267 | if (a && cArray.indexOf(a) === -1) cArray.push(a) 268 | const rItem = cArray.indexOf(r) 269 | if (rItem !== -1) cArray.splice(rItem, 1) 270 | e.className = cArray.join(' ') 271 | } 272 | 273 | /* 274 | manageState 📝 275 | --- 276 | - defines the state 277 | - normal 278 | - sticky 279 | - stuck 280 | */ 281 | manageState (item) { 282 | // cache object 283 | const it = item 284 | const p = it.props 285 | const state = it.state 286 | const stateChange = it.stateChange 287 | const start = it.stickyStart 288 | const change = it.stickyChange 289 | const stop = it.stickyStop 290 | // cache props 291 | const pv = p.positionVal 292 | const se = p.scrollEl 293 | const sticky = p.stickyClass 294 | const stickyChange = p.stickyChangeClass 295 | const stuck = p.stuckClass 296 | const vp = p.verticalPosition 297 | const isTop = vp !== 'bottom' 298 | const aS = p.applyStyle 299 | const ns = p.noStyles 300 | /* 301 | requestAnimationFrame 302 | --- 303 | - use rAF 304 | - or stub rAF 305 | */ 306 | const rAFStub = function rAFDummy (f) { f() } 307 | const rAF = !this.isWin 308 | ? rAFStub 309 | : window.requestAnimationFrame || 310 | window.mozRequestAnimationFrame || 311 | window.webkitRequestAnimationFrame || 312 | window.msRequestAnimationFrame || 313 | rAFStub 314 | 315 | /* 316 | define scroll vars 317 | --- 318 | - scroll 319 | - notSticky 320 | - isSticky 321 | - isStuck 322 | */ 323 | const scroll = this.isWin ? (window.scrollY || window.pageYOffset) : se.scrollTop 324 | const notSticky = scroll > start && scroll < stop && (state === 'default' || state === 'stuck') 325 | const isSticky = isTop && scroll <= start && (state === 'sticky' || state === 'stuck') 326 | const isStuck = scroll >= stop && state === 'sticky' 327 | /* 328 | Unnamed arrow functions within this block 329 | --- 330 | - help wanted or discussion 331 | - view test.stickybits.js 332 | - `stickybits .manageState `position: fixed` interface` for more awareness 👀 333 | */ 334 | if (notSticky) { 335 | it.state = 'sticky' 336 | } else if (isSticky) { 337 | it.state = 'default' 338 | } else if (isStuck) { 339 | it.state = 'stuck' 340 | } 341 | 342 | const isStickyChange = scroll >= change && scroll <= stop 343 | const isNotStickyChange = scroll < change / 2 || scroll > stop 344 | if (isNotStickyChange) { 345 | it.stateChange = 'default' 346 | } else if (isStickyChange) { 347 | it.stateChange = 'sticky' 348 | } 349 | 350 | // Only apply new styles if the state has changed 351 | if (state === it.state && stateChange === it.stateChange) return 352 | rAF(() => { 353 | const stateStyles = { 354 | sticky: { 355 | styles: { 356 | position: pv, 357 | top: '', 358 | bottom: '', 359 | [vp]: `${p.stickyBitStickyOffset}px`, 360 | }, 361 | classes: { [sticky]: true }, 362 | }, 363 | default: { 364 | styles: { 365 | [vp]: '', 366 | }, 367 | classes: {}, 368 | }, 369 | stuck: { 370 | styles: { 371 | [vp]: '', 372 | /** 373 | * leave !this.isWin 374 | * @example https://codepen.io/yowainwright/pen/EXzJeb 375 | */ 376 | ...((pv === 'fixed' && !ns) || !this.isWin ? { 377 | position: 'absolute', 378 | top: '', 379 | bottom: '0', 380 | } : {}), 381 | }, 382 | classes: { [stuck]: true }, 383 | }, 384 | } 385 | 386 | if (pv === 'fixed') { 387 | stateStyles.default.styles.position = '' 388 | } 389 | 390 | const style = stateStyles[it.state] 391 | style.classes = { 392 | [stuck]: !!style.classes[stuck], 393 | [sticky]: !!style.classes[sticky], 394 | [stickyChange]: isStickyChange, 395 | } 396 | 397 | aS(style, item) 398 | }) 399 | } 400 | 401 | /* 402 | applyStyle 403 | --- 404 | - apply the given styles and classes to the element 405 | */ 406 | applyStyle ({ styles, classes }, item) { 407 | // cache object 408 | const it = item 409 | const e = it.el 410 | const p = it.props 411 | const stl = e.style 412 | // cache props 413 | const ns = p.noStyles 414 | 415 | const cArray = e.className.split(' ') 416 | // Disable due to bug with old versions of eslint-scope and for ... in 417 | // https://github.com/eslint/eslint/issues/12117 418 | // eslint-disable-next-line no-unused-vars 419 | for (const cls in classes) { 420 | const addClass = classes[cls] 421 | if (addClass) { 422 | if (cArray.indexOf(cls) === -1) cArray.push(cls) 423 | } else { 424 | const idx = cArray.indexOf(cls) 425 | if (idx !== -1) cArray.splice(idx, 1) 426 | } 427 | } 428 | 429 | e.className = cArray.join(' ') 430 | 431 | if (styles['position']) { 432 | stl['position'] = styles['position'] 433 | } 434 | 435 | if (ns) return 436 | 437 | // eslint-disable-next-line no-unused-vars 438 | for (const key in styles) { 439 | stl[key] = styles[key] 440 | } 441 | } 442 | 443 | update (updatedProps = null) { 444 | this.instances.forEach((instance) => { 445 | this.computeScrollOffsets(instance) 446 | if (updatedProps) { 447 | // eslint-disable-next-line no-unused-vars 448 | for (const updatedProp in updatedProps) { 449 | instance.props[updatedProp] = updatedProps[updatedProp] 450 | } 451 | } 452 | }) 453 | 454 | return this 455 | } 456 | 457 | /* 458 | removes an instance 👋 459 | -------- 460 | - cleanup instance 461 | */ 462 | removeInstance (instance) { 463 | const e = instance.el 464 | const p = instance.props 465 | 466 | this.applyStyle( 467 | { 468 | styles: { position: '', [p.verticalPosition]: '' }, 469 | classes: { [p.stickyClass]: '', [p.stuckClass]: '' }, 470 | }, 471 | instance, 472 | ) 473 | 474 | this.toggleClasses(e.parentNode, p.parentClass) 475 | } 476 | 477 | /* 478 | cleanup 🛁 479 | -------- 480 | - cleans up each instance 481 | - clears instance 482 | */ 483 | cleanup () { 484 | for (let i = 0; i < this.instances.length; i += 1) { 485 | const instance = this.instances[i] 486 | if (instance.stateContainer) { 487 | instance.props.scrollEl.removeEventListener('scroll', instance.stateContainer) 488 | } 489 | this.removeInstance(instance) 490 | } 491 | this.manageState = false 492 | this.instances = [] 493 | } 494 | } 495 | 496 | /* 497 | export 498 | -------- 499 | exports StickBits to be used 🏁 500 | */ 501 | export default function stickybits (target, o) { 502 | return new Stickybits(target, o) 503 | } 504 | -------------------------------------------------------------------------------- /src/umbrella.stickybits.js: -------------------------------------------------------------------------------- 1 | import stickybits from './stickybits' 2 | 3 | if (typeof window !== 'undefined') { 4 | const plugin = window.u 5 | if (plugin) { 6 | plugin.prototype.stickybits = function stickybitsPlugin (opts) { 7 | return stickybits(this, opts) 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tests/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "prefer-arrow-callback": 0, 4 | "func-names": 0, 5 | "no-undef": 0 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tests/acceptance/bottom/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/acceptance/bottom/test.js: -------------------------------------------------------------------------------- 1 | /* 2 | QUnit Tests 3 | ---- 4 | - Acceptance test oriented 5 | 6 | */ 7 | 8 | // generateContentBlock 9 | var num = 1; 10 | var content; 11 | var main = document.getElementById('main'); 12 | var generateTestContent = function(num) { 13 | content = '

Child '+ num +'

'; 14 | main.innerHTML = content; 15 | }; 16 | 17 | window.addEventListener('load', function() { 18 | // tests StickyBits test 19 | // ensures StickyBits offset is working 20 | var selector = document.querySelector('.child-1'); 21 | stickybits(selector, {useStickyClasses: true}); 22 | $('html, body').animate({scrollTop: '+=1000px'}, 300); 23 | setTimeout(function() { 24 | QUnit.test('stickbits adds `js-is-stuck`', function(assert) { 25 | assert.equal(selector.classList.contains('js-is-stuck'), true, 'The stickybit should have a stucky class'); 26 | }) 27 | }, 300); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/acceptance/cleanup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/acceptance/cleanup/test.js: -------------------------------------------------------------------------------- 1 | /* 2 | QUnit Tests 3 | ---- 4 | - Acceptance test oriented 5 | 6 | */ 7 | 8 | // generateContentBlock 9 | var num = 1; 10 | var content; 11 | var main = document.getElementById('main'); 12 | var generateTestContent = function(num) { 13 | content = '

Child '+ num +'

'; 14 | main.innerHTML = content; 15 | }; 16 | 17 | window.addEventListener('load', function() { 18 | // tests StickyBits test 19 | // ensures StickyBits offset is working 20 | 21 | QUnit.test('Checks cleanup method', function(assert) { 22 | generateTestContent(num) 23 | const selector = document.querySelector('.child-1'); 24 | var stickybit = stickybits('.child-1', {useStickyClasses: true}); 25 | stickybit.cleanup(); 26 | assert.equal(selector.parentNode.classList.contains('js-stickybit-parent'), false, 'This should work like fixed'); 27 | stickybit = stickybits('.child-1', {useStickyClasses: true}); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /tests/acceptance/monitoring/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/acceptance/monitoring/test.js: -------------------------------------------------------------------------------- 1 | /* 2 | QUnit Tests 3 | ---- 4 | - monitors `useStickyClasses` 5 | 6 | */ 7 | 8 | // ensure QUnit is working 9 | QUnit.test('hello test', function(assert) { 10 | assert.ok(1 == '1', 'Passed!'); 11 | }); 12 | 13 | var main = document.getElementById('main'); 14 | 15 | // generateContentBlock 16 | var num = 1; 17 | var content; 18 | var generateTestContent = function(num) { 19 | content = '

Child '+ num +'

'; 20 | return main.innerHTML = content; 21 | }; 22 | 23 | 24 | window.addEventListener('load', function() { 25 | generateTestContent(num); 26 | var selector = document.querySelector('.child-1'); 27 | stickybits(selector, {useStickyClasses: true}); 28 | $('html, body').animate({scrollTop: '+=500px'}, 300); 29 | setTimeout(function() { 30 | QUnit.test('stickbits adds `js-is-sticky`', function(assert) { 31 | assert.equal(selector.classList.contains('js-is-sticky'), true, 'The stickybit should have a sticky class'); 32 | }) 33 | $('html, body').animate({scrollTop: '0px'}, 0); 34 | setTimeout(function() { 35 | QUnit.test('stickbits removes `js-is-sticky`', function(assert) { 36 | assert.equal(selector.classList.contains('js-is-sticky'), false, 'The stickybit should not have a sticky class'); 37 | }) 38 | $('html, body').animate({scrollTop: '+=1000px'}, 300); 39 | setTimeout(function() { 40 | QUnit.test('stickbits adds `js-is-stuck`', function(assert) { 41 | assert.equal(selector.classList.contains('js-is-stuck'), true, 'The stickybit should have a stuck class'); 42 | }) 43 | $('html, body').animate({scrollTop: '0px'}, 0); 44 | }, 300); 45 | }, 100); 46 | }, 300); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/acceptance/multiple-sticky-classes/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/acceptance/multiple-sticky-classes/test.js: -------------------------------------------------------------------------------- 1 | /* 2 | QUnit Tests 3 | ---- 4 | - monitors multiple stickybits 5 | 6 | */ 7 | 8 | // ensure QUnit is working 9 | QUnit.test('hello test', function(assert) { 10 | assert.ok(1 == '1', 'Passed!'); 11 | }); 12 | var main = document.getElementById('main'); 13 | 14 | // generateContentBlock 15 | var content; 16 | var num; 17 | var generateTestContent = function(num) { 18 | content = '

Child '+ num +'

'; 19 | return content; 20 | }; 21 | 22 | window.addEventListener('load', function() { 23 | // default StickyBits test 24 | // ensures StickyBits is working 25 | QUnit.test('Test multiple stickbits', function(assert) { 26 | var numbers = ['1', '2', '3']; 27 | var content = []; 28 | for (var i = 0; numbers.length > i; i += 1) { 29 | num = numbers[i]; 30 | var el = generateTestContent(num); 31 | content.push(el); 32 | } 33 | main.innerHTML = content.join(''); 34 | var stickies = stickybits('.child', {useStickyClasses: true}); 35 | var stickyItems = document.querySelectorAll('[style*="position"]'); 36 | assert.equal(stickyItems.length, 3, 'There are 3 sticky items'); 37 | }); 38 | }) 39 | -------------------------------------------------------------------------------- /tests/acceptance/multiple/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/acceptance/multiple/test.js: -------------------------------------------------------------------------------- 1 | /* 2 | QUnit Tests 3 | ---- 4 | - tests multiples 5 | */ 6 | 7 | // ensure QUnit is working 8 | QUnit.test('hello test', function(assert) { 9 | assert.ok(1 == '1', 'Passed!'); 10 | }); 11 | 12 | var main = document.getElementById('main'); 13 | 14 | // generateContentBlock 15 | var content; 16 | var num; 17 | var generateTestContent = function(num) { 18 | content = '

Child '+ num +'

'; 19 | return content; 20 | }; 21 | 22 | 23 | window.addEventListener('load', function() { 24 | // default StickyBits test 25 | // ensures StickyBits is working 26 | QUnit.test('Test multiple stickbits', function(assert) { 27 | var numbers = ['1', '2', '3']; 28 | var content = []; 29 | for (var i = 0; numbers.length > i; i += 1) { 30 | num = numbers[i]; 31 | var el = generateTestContent(num); 32 | content.push(el); 33 | } 34 | main.innerHTML = content.join(''); 35 | 36 | var stickies = stickybits('.child'); 37 | var stickyItems = document.querySelectorAll('[style*="position"]'); 38 | assert.equal(stickyItems.length, 3, 'There are 3 stick items'); 39 | }); 40 | }) 41 | -------------------------------------------------------------------------------- /tests/acceptance/offset/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/acceptance/offset/test.js: -------------------------------------------------------------------------------- 1 | /* 2 | QUnit Tests 3 | ---- 4 | - tests the `offset` 5 | 6 | */ 7 | 8 | // ensure QUnit is working 9 | QUnit.test('hello test', function(assert) { 10 | assert.ok(1 == '1', 'Passed!'); 11 | }); 12 | 13 | var main = document.getElementById('main'); 14 | 15 | // generateContentBlock 16 | var num = 1; 17 | var content; 18 | var generateTestContent = function(num) { 19 | content = '

Child '+ num +'

'; 20 | return main.innerHTML = content; 21 | }; 22 | 23 | window.addEventListener('load', function() { 24 | // tests StickyBits test 25 | // ensures StickyBits offset is working 26 | QUnit.test('different stickyOffset test', function(assert) { 27 | generateTestContent(1) 28 | var selector = document.querySelector('.child-1') 29 | stickybits('.child-1', {stickyBitStickyOffset: 10}) 30 | assert.equal(selector.style.top, '10px', 'top should be set to 10px') 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /tests/acceptance/scrollTo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/acceptance/scrollTo/test.js: -------------------------------------------------------------------------------- 1 | /* 2 | QUnit Tests 3 | ---- 4 | - monitors multiple stickybits 5 | 6 | */ 7 | 8 | // ensure QUnit is working 9 | QUnit.test('hello test', function(assert) { 10 | assert.ok(1 == '1', 'Passed!'); 11 | }); 12 | 13 | var main = document.getElementById('main'); 14 | 15 | // generateContentBlock 16 | var content; 17 | var num; 18 | var generateTestContent = function(num) { 19 | content = '

Child '+ num +'

'; 20 | return content; 21 | }; 22 | 23 | 24 | window.addEventListener('load', function() { 25 | // default StickyBits test 26 | // ensures StickyBits is working 27 | QUnit.test('Test multiple stickbits', function(assert) { 28 | var numbers = ['1', '2', '3']; 29 | var content = []; 30 | for (var i = 0; numbers.length > i; i += 1) { 31 | num = numbers[i]; 32 | var el = generateTestContent(num); 33 | content.push(el); 34 | } 35 | main.innerHTML = content.join(''); 36 | var stickies = stickybits('.child', {useStickyClasses: true}); 37 | var stickyItems = document.querySelectorAll('[style*="position"]'); 38 | assert.equal(stickyItems.length, 3, 'There are 3 sticky items'); 39 | }); 40 | }) 41 | -------------------------------------------------------------------------------- /tests/acceptance/stacked/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/acceptance/stacked/test.js: -------------------------------------------------------------------------------- 1 | /* 2 | QUnit Tests 3 | ---- 4 | - monitors multiple stickybits 5 | 6 | */ 7 | 8 | // ensure QUnit is working 9 | QUnit.test('hello test', function(assert) { 10 | assert.ok(1 == '1', 'Passed!'); 11 | }); 12 | 13 | var main = document.getElementById('main'); 14 | 15 | // generateContentBlock 16 | var content; 17 | var num; 18 | var generateTestContent = function(num) { 19 | content = '

Child '+ num +'

'; 20 | return content; 21 | }; 22 | 23 | window.addEventListener('load', function() { 24 | // default StickyBits test 25 | // ensures StickyBits is working 26 | QUnit.test('Test multiple stickybits', function(assert) { 27 | var numbers = ['1', '2', '3']; 28 | var content = []; 29 | for (var i = 0; numbers.length > i; i += 1) { 30 | num = numbers[i]; 31 | var el = generateTestContent(num); 32 | content.push(el); 33 | } 34 | main.innerHTML = content.join(''); 35 | var stickies = stickybits('.child', {useStickyClasses: true}); 36 | var stickyItems = document.querySelectorAll('[style*="position"]'); 37 | assert.equal(stickyItems.length, 3, 'There are 3 sticky items'); 38 | }); 39 | }) 40 | -------------------------------------------------------------------------------- /tests/acceptance/test.css: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | color: white; 4 | font-family: sans-serif; 5 | margin: 0; 6 | padding: 0; 7 | } 8 | main { 9 | counter-reset: div; 10 | display: flex; 11 | height: 100%; 12 | justify-content: space-between; 13 | max-width: 100vw; 14 | min-height: 2000px; 15 | position: absolute; 16 | top: 400px; 17 | width: 100%; 18 | } 19 | .child { 20 | padding: 1rem 0; 21 | text-indent: 1rem; 22 | width: 100%; 23 | } 24 | .parent { 25 | height: 300px; 26 | position: relative; 27 | width: 100%; 28 | } 29 | .parent:nth-child(odd) { 30 | background: tan; 31 | } 32 | .parent:nth-child(odd) .child { 33 | background-color: red; 34 | } 35 | .parent:nth-child(even) { 36 | background: aqua; 37 | } 38 | .parent:nth-child(even) .child { 39 | background-color: green; 40 | } 41 | .parent:before { 42 | counter-increment: div; 43 | content: 'Parent 'counter(div); 44 | left: 1rem; 45 | position: absolute; 46 | top: 1rem; 47 | z-index: 2; 48 | } 49 | .child.js-is-sticky { 50 | top: 0; 51 | } 52 | .child.js-is-stuck { 53 | bottom: 0; 54 | } 55 | -------------------------------------------------------------------------------- /tests/acceptance/update/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 |
17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/acceptance/update/test.js: -------------------------------------------------------------------------------- 1 | /* 2 | QUnit Tests 3 | ---- 4 | - monitors multiple stickybits 5 | 6 | */ 7 | 8 | var main = document.getElementById('main'); 9 | 10 | // generateContentBlock 11 | var content; 12 | var num; 13 | var generateTestContent = function (num) { 14 | content = '

Child ' + num + '

'; 15 | return content; 16 | }; 17 | 18 | window.addEventListener('load', function () { 19 | // default StickyBits test 20 | // ensures StickyBits is working 21 | QUnit.test('Test multiple stickybits', function (assert) { 22 | var numbers = ['1']; 23 | var content = []; 24 | for (var i = 0; numbers.length > i; i += 1) { 25 | num = numbers[i]; 26 | var el = generateTestContent(num); 27 | content.push(el); 28 | } 29 | main.innerHTML = content.join(''); 30 | var stickies = stickybits('.child', { useStickyClasses: true }); 31 | var stickyItems = document.querySelectorAll('[style*="position"]'); 32 | var instance = stickies.instances[0]; 33 | var stickyStart = instance.stickyStart 34 | assert.equal(stickyItems.length, 1, 'There are 3 sticky items'); 35 | assert.equal(stickyStart, 400, 'The stickyStart is 400'); 36 | main.style.top = 500; 37 | stickies.update(); 38 | assert.equal(stickyStart, 400, 'The stickyStart is 500'); 39 | }); 40 | }) 41 | -------------------------------------------------------------------------------- /tests/acceptance/use-fixed/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dollarshaveclub/stickybits/327fcb656e6966763f195807971b3ac00b31bd3c/tests/acceptance/use-fixed/index.html -------------------------------------------------------------------------------- /tests/acceptance/use-fixed/test.js: -------------------------------------------------------------------------------- 1 | /* 2 | QUnit Tests 3 | ---- 4 | - monitors useFixed 5 | 6 | */ 7 | 8 | // ensure QUnit is working 9 | QUnit.test('hello test', function (assert) { 10 | assert.ok(1 == '1', 'Passed!'); 11 | }); 12 | 13 | var main = document.getElementById('main'); 14 | 15 | // generateContentBlock 16 | var content; 17 | var num; 18 | var generateTestContent = function (num) { 19 | content = '

Child ' + num + '

'; 20 | return content; 21 | }; 22 | 23 | window.addEventListener('load', function () { 24 | // default StickyBits test 25 | // ensures StickyBits is working 26 | QUnit.test('Test update', function (assert) { 27 | var numbers = ['1']; 28 | var content = []; 29 | for (var i = 0; numbers.length > i; i += 1) { 30 | num = numbers[i]; 31 | var el = generateTestContent(num); 32 | content.push(el); 33 | } 34 | main.innerHTML = content.join(''); 35 | var stickies = stickybits('.child', { useFixed: true }); 36 | var stickyItems = document.querySelectorAll('[style*="position"]'); 37 | var instance = stickies.instances[0]; 38 | var stickyStart = instance.stickyStart 39 | var main = document.getElementById('main') 40 | assert.equal(stickyItems.length, 1, 'There is 1 sticky item'); 41 | assert.equal(stickyItems[0].style.position, 'fixed', 'The stickybit position is fixed'); 42 | }); 43 | }) 44 | -------------------------------------------------------------------------------- /tests/unit/test.stickybits.js: -------------------------------------------------------------------------------- 1 | import stickybits from '../../src/stickybits.js' 2 | 3 | test('Jest paths are pointed correctly, dom is ready to go', () => { 4 | document.body.innerHTML = '
' 5 | const div = document.getElementById('stickybit') 6 | const stickybit = stickybits 7 | expect(div.id).toBe('stickybit') 8 | expect(typeof stickybit).toBe('function') 9 | }) 10 | 11 | test('basic stickybits setup', () => { 12 | document.body.innerHTML = '
' 13 | const stickybit = stickybits('#stickybit') 14 | expect(typeof stickybit).toBe('object') 15 | }) 16 | 17 | test('basic stickybits interface', () => { 18 | document.body.innerHTML = '
' 19 | const stickybit = stickybits('#stickybit') 20 | expect(stickybit.props.scrollEl).toBe(window) 21 | expect(stickybit.props.stickyBitStickyOffset).toBe(0) 22 | expect(stickybit.props.verticalPosition).toBe('top') 23 | expect(stickybit.props.useStickyClasses).toBe(false) 24 | expect(stickybit.props.noStyles).toBe(false) 25 | expect(stickybit.props.stickyClass).toBe('js-is-sticky') 26 | expect(stickybit.props.stuckClass).toBe('js-is-stuck') 27 | expect(stickybit.props.parentClass).toBe('js-stickybit-parent') 28 | expect(stickybit.props.positionVal).toBe('-ms-sticky') 29 | }) 30 | 31 | test('basic stickybits interface with positionVal equalling sticky', () => { 32 | document.body.innerHTML = '
' 33 | const stickybit = stickybits('#stickybit') 34 | 35 | // api allows users to define the interface 36 | stickybit.props.positionVal = 'sticky' 37 | // stickybit should be sticky 38 | expect(stickybit.props.positionVal).toBe('sticky') 39 | stickybit.props.positionVal = stickybit.definePosition() 40 | // stickybit should be `-ms-sticky` 41 | expect(stickybit.props.positionVal).toBe('-ms-sticky') 42 | }) 43 | 44 | test('stickybits interface with an updated object properties', () => { 45 | document.body.innerHTML = '
' 46 | const stickybit = stickybits('#stickybit', { 47 | stickyBitStickyOffset: 10, 48 | verticalPosition: 'bottom', 49 | useStickyClasses: true, 50 | noStyles: true, 51 | stickyClass: 'sticky', 52 | stuckClass: 'stuck', 53 | parentClass: 'parent' 54 | }) 55 | // interface results with custom object properties 56 | expect(stickybit.props.stickyBitStickyOffset).toBe(10) 57 | expect(stickybit.props.verticalPosition).toBe('bottom') 58 | expect(stickybit.props.useStickyClasses).toBe(true) 59 | expect(stickybit.props.noStyles).toBe(true) 60 | expect(stickybit.props.stickyClass).toBe('sticky') 61 | expect(stickybit.props.stuckClass).toBe('stuck') 62 | expect(stickybit.props.parentClass).toBe('parent') 63 | expect(stickybit.props.positionVal).toBe('-ms-sticky') 64 | }) 65 | 66 | test('stickybits only appends parent class once', () => { 67 | document.body.innerHTML = '
' 68 | const stickybit = stickybits('.child', { 69 | useStickyClasses: true 70 | }); 71 | 72 | const parent = document.querySelector('#parent'); 73 | expect(parent.className).toBe(stickybit.props.parentClass); 74 | }) 75 | 76 | test('stickybits interface with custom scrollEl selector', () => { 77 | document.body.innerHTML = '
' 78 | const stickybit = stickybits('#stickybit', { 79 | scrollEl: '#parent' 80 | }) 81 | // interface results for custom scrollEl 82 | expect(stickybit.props.scrollEl).toBe(document.querySelector('#parent')) 83 | }) 84 | 85 | test('stickybits interface with custom scrollEl element', () => { 86 | document.body.innerHTML = '
' 87 | const stickybit = stickybits('#stickybit', { 88 | scrollEl: document.querySelector('#parent') 89 | }) 90 | 91 | // interface results for custom scrollEl 92 | expect(stickybit.props.scrollEl).toBe(document.querySelector('#parent')) 93 | }) 94 | 95 | test('stickybits interface with custom applyStyle function', () => { 96 | const applyStyle = jest.fn(() => {}) 97 | document.body.innerHTML = '
' 98 | const stickybit = stickybits("#stickybit", { applyStyle }) 99 | 100 | const item = stickybit.instances[0]; 101 | item.state = 'sticky'; 102 | stickybit.isWin = false; 103 | item.props.scrollEl = { scrollTop: 200 }; 104 | item.stickyStart = 0; 105 | item.stickyStop = 200; 106 | stickybit.manageState(item) 107 | 108 | expect(applyStyle).toHaveBeenCalled() 109 | }) 110 | 111 | test("stickybits doesn't apply styles if noStyles is true", () => { 112 | document.body.innerHTML = '
' 113 | const stickybit = stickybits('#parent', { 114 | noStyles: true, 115 | }); 116 | 117 | const item = stickybit.instances[0]; 118 | stickybit.applyStyle({ styles: { color: 'red' }, classes: [] }, item) 119 | 120 | const parent = document.querySelector('#parent'); 121 | expect(parent.style['color']).toBe(''); 122 | }) 123 | 124 | test("stickybits sets position even with noStyles true", () => { 125 | document.body.innerHTML = '
' 126 | const stickybit = stickybits('#parent', { 127 | noStyles: true, 128 | }); 129 | 130 | const item = stickybit.instances[0]; 131 | stickybit.applyStyle({ styles: { position: 'absolute' }, classes: [] }, item) 132 | 133 | const parent = document.querySelector('#parent'); 134 | expect(parent.style['position']).toBe('absolute'); 135 | }) 136 | 137 | test("stickybits sets position absolute if the element is fixed", () => { 138 | document.body.innerHTML = '
' 139 | const stickybit = stickybits('#parent', { useFixed: true }); 140 | 141 | const item = stickybit.instances[0]; 142 | item.state = 'sticky'; 143 | stickybit.isWin = false; 144 | item.props.scrollEl = { scrollTop: 200 }; 145 | item.stickyStart = 0; 146 | item.stickyStop = 200; 147 | stickybit.manageState(item) 148 | 149 | const parent = document.querySelector('#parent'); 150 | expect(parent.style['position']).toBe('absolute'); 151 | }) 152 | 153 | test("stickybits doesn't change position style if the element isn't fixed", () => { 154 | document.body.innerHTML = '
' 155 | const stickybit = stickybits('#parent'); 156 | const parent = document.querySelector('#parent'); 157 | const positionStyle = parent.style['position']; 158 | 159 | const item = stickybit.instances[0]; 160 | item.state = 'sticky'; 161 | stickybit.isWin = true; 162 | item.props.scrollEl = { scrollTop: 200 }; 163 | item.stickyStart = 0; 164 | item.stickyStop = 200; 165 | stickybit.manageState(item) 166 | 167 | expect(parent.style['position']).toBe(positionStyle); 168 | }) 169 | 170 | test("stickybits doesn't change position style if noStyles is true", () => { 171 | document.body.innerHTML = '
' 172 | const stickybit = stickybits('#parent'); 173 | const parent = document.querySelector('#parent'); 174 | const positionStyle = parent.style['position']; 175 | 176 | const item = stickybit.instances[0]; 177 | item.state = 'sticky'; 178 | stickybit.isWin = true; 179 | item.props.scrollEl = { scrollTop: 200 }; 180 | item.stickyStart = 0; 181 | item.stickyStop = 200; 182 | stickybit.manageState(item) 183 | 184 | expect(parent.style['position']).toBe(positionStyle); 185 | }) 186 | 187 | test('stickybits .addInstance interface', () => { 188 | document.body.innerHTML = '
' 189 | const e = document.getElementById('manage-sticky') 190 | const stickybit = stickybits('#manage-sticky', { useStickyClasses: true }) 191 | const instance = stickybit.instances[0] 192 | const p = instance.props 193 | // test instances 194 | expect(typeof instance).toBe('object') 195 | expect(typeof p).toBe('object') 196 | // test new props 197 | expect(p.stickyClass).toBe('js-is-sticky') 198 | expect(p.stuckClass).toBe('js-is-stuck') 199 | expect(p.useStickyClasses).toBe(true) 200 | expect(p.verticalPosition).toBe('top') 201 | expect(p.positionVal).toBe('-ms-sticky') 202 | }) 203 | 204 | test('stickybits .getClosestParent interface', () => { 205 | document.body.innerHTML = '
' 206 | const child = document.getElementById('child') 207 | const parentEl = document.getElementById('parent') 208 | const stickybit = stickybits('#manage-sticky') 209 | const parent = stickybit.getClosestParent(child, parentEl) 210 | expect(parent.id).toBe('parent') 211 | }) 212 | 213 | test('stickybits .getTopPosition interface', () => { 214 | document.body.innerHTML = '
' 215 | const child = document.getElementById('child') 216 | const parentEl = document.getElementById('parent') 217 | const stickybit = stickybits('#manage-sticky') 218 | const parentOffsetTop = stickybit.getTopPosition(parentEl) 219 | expect(parentOffsetTop).toBe(0) 220 | }) 221 | 222 | test('stickybits .computeScrollOffsets interface', () => { 223 | document.body.innerHTML = '
' 224 | const stickybit = stickybits('#manage-sticky', { useStickyClasses: true }) 225 | const instance = stickybit.instances[0] 226 | const p = instance.props 227 | // test instance setup 228 | expect(typeof instance).toBe('object') 229 | expect(typeof p).toBe('object') 230 | stickybit.computeScrollOffsets(instance) 231 | // test .computeScrollOffsets interface 232 | expect(instance.offset).toBe(0) 233 | expect(instance.stickyStart).toBe(0) 234 | expect(instance.stickyStop).toBe(0) 235 | expect(instance.state).toBe('default') 236 | }) 237 | 238 | test('stickybits .toggleClasses interface', () => { 239 | document.body.innerHTML = '
' 240 | const stickybit = stickybits('#manage-sticky', { useStickyClasses: true }) 241 | const el = document.getElementById('parent') 242 | el.classList.add('test') 243 | stickybit.toggleClasses(el, test, 'other-test') 244 | expect(el.classList.contains('other-test')).toBe(true) 245 | }) 246 | 247 | test('stickybits .manageState `notSticky` interface', () => { 248 | document.body.innerHTML = '
' 249 | const stickybit = stickybits('#manage-sticky', { useStickyClasses: true }) 250 | // test instance setup 251 | const instance = stickybit.instances[0] 252 | 253 | // test notSticky 254 | instance.state = 'default' 255 | instance.stickyStart = 0; 256 | stickybit.manageState(instance) 257 | // test instance setup 258 | expect(typeof instance).toBe('object') 259 | // test results 260 | expect(instance.el.style.position).toBe('-ms-sticky') 261 | expect(instance.state).toBe('default') 262 | expect(instance.stickyStart).toBe(0) 263 | }) 264 | 265 | test('stickybits .manageState `isSticky` interface from default', () => { 266 | document.body.innerHTML = '
' 267 | const stickybit = stickybits('#manage-sticky', { useStickyClasses: true }) 268 | // test instance setup 269 | const instance = stickybit.instances[0] 270 | 271 | // test notSticky 272 | stickybit.isWin = false 273 | instance.props.scrollEl = { scrollTop: 1 } 274 | instance.state = 'default' 275 | instance.stickyStart = 0 276 | instance.stickyStop = 200 277 | stickybit.manageState(instance); 278 | // test instance setup 279 | expect(typeof instance).toBe('object') 280 | // test results 281 | expect(instance.el.style.position).toBe('-ms-sticky') 282 | expect(instance.state).toBe('sticky') 283 | expect(instance.stickyStart).toBe(0) 284 | expect(instance.stickyStop).toBe(200) 285 | expect(instance.props.scrollEl.scrollTop).toBe(1) 286 | }) 287 | 288 | test('stickybits .manageState `isSticky` interface from stuck', () => { 289 | document.body.innerHTML = '
' 290 | const stickybit = stickybits('#manage-sticky', { useStickyClasses: true }) 291 | // test instance setup 292 | const instance = stickybit.instances[0] 293 | 294 | // test notSticky 295 | instance.state = 'stuck' 296 | instance.el.classList.add('js-is-stuck') 297 | instance.stickyStart = 1 298 | stickybit.manageState(instance); 299 | // test instance setup 300 | expect(typeof instance).toBe('object') 301 | // test results 302 | expect(instance.el.style.position).toBe('-ms-sticky') 303 | expect(instance.state).toBe('default') 304 | expect(instance.stickyStart).toBe(1) 305 | }) 306 | 307 | test('stickybits .manageState `isStickyChange` interface', () => { 308 | document.body.innerHTML = '
' 309 | const stickybit = stickybits('#manage-sticky', { useStickyClasses: true, customStickyChangeNumber: 10 }) 310 | // test instance setup 311 | const instance = stickybit.instances[0] 312 | 313 | // test notSticky 314 | const stickyChangeTest = instance.stickyChange 315 | stickybit.manageState(instance) 316 | // test instance setup 317 | expect(typeof instance).toBe('object') 318 | // test results 319 | expect(instance.el.style.position).toBe('-ms-sticky') 320 | expect(instance.props.customStickyChangeNumber).toBe(10) 321 | expect(instance.stickyChange).toBe(10) 322 | }) 323 | 324 | test('stickybits .manageState `isStuck` interface', () => { 325 | document.body.innerHTML = '
' 326 | const stickybit = stickybits('#manage-sticky', { useStickyClasses: true }) 327 | // test instance setup 328 | const instance = stickybit.instances[0] 329 | // test notSticky 330 | stickybit.isWin = false 331 | instance.props.scrollEl = { scrollTop: 500 } 332 | instance.state = 'stuck' 333 | instance.el.classList.add('js-is-stuck') 334 | instance.stickyStart = 0 335 | instance.stickyStop = 200 336 | stickybit.manageState(instance); 337 | // test instance setup 338 | expect(typeof instance).toBe('object') 339 | // test results 340 | expect(instance.el.style.position).toBe('absolute') 341 | expect(instance.state).toBe('stuck') 342 | expect(instance.props.scrollEl.scrollTop).toBe(500) 343 | expect(instance.stickyStart).toBe(0) 344 | expect(instance.stickyStop).toBe(200) 345 | }) 346 | 347 | /* 348 | Fixed Position Testing 349 | -------- 350 | This is brittle 351 | - will probably abstract out arrow functions within rAF 352 | - help/discussion wanted 353 | */ 354 | test('stickybits .manageState `position: fixed` interface', () => { 355 | document.body.innerHTML = '
' 356 | const stickybit = stickybits('#manage-sticky', { useStickyClasses: true }) 357 | // test instance setup 358 | const instance = stickybit.instances[0] 359 | // test notSticky 360 | instance.props.positionVal = 'fixed' 361 | instance.state = 'default' 362 | instance.stickyStart = 0 363 | stickybit.manageState(instance); 364 | // test instance setup 365 | expect(typeof instance).toBe('object') 366 | // test results 367 | expect(instance.props.positionVal).toBe('fixed') 368 | expect(instance.state).toBe('default') 369 | expect(instance.stickyStart).toBe(0) 370 | }) 371 | 372 | test('stickybits .removeClass interface', () => { 373 | document.body.innerHTML = '
' 374 | const stickybit = stickybits('#manage-sticky', { useStickyClasses: true }) 375 | const el = document.getElementById('parent') 376 | el.classList.add('test') 377 | stickybit.toggleClasses(el, 'test') 378 | expect(el.classList.contains('test')).toBe(false) 379 | }) 380 | 381 | test('stickybits .removeInstance interface', () => { 382 | document.body.innerHTML = '
' 383 | const stickybit = stickybits('#manage-sticky', { useStickyClasses: true }) 384 | const instance = stickybit.instances[0] 385 | const el = instance.el 386 | const parent = el.parentElement 387 | // setup 388 | el.classList.add('test') 389 | el.style.position = 'fixed' 390 | el.style.top = '0px' 391 | el.classList.add('js-is-sticky') 392 | parent.classList.add('js-stickybit-parent') 393 | // invoke removeInstance 394 | stickybit.removeInstance(instance) 395 | expect(instance.el.position).toBe(undefined) 396 | expect(instance.el.top).toBe(undefined) 397 | expect(instance.el.classList.contains('js-is-sticky')).toBe(false) 398 | expect(instance.el.parentElement.classList.contains('js-stickybit-parent')).toBe(false) 399 | }) 400 | 401 | test('stickybits .cleanup interface', () => { 402 | document.body.innerHTML = '
' 403 | const stickybit = stickybits('#manage-sticky', { useStickyClasses: true }) 404 | // invoke .cleanup 405 | stickybit.cleanup() 406 | expect(stickybit.manageState).toBe(false) 407 | expect(stickybit.instances).toEqual([]) 408 | }) 409 | 410 | test('stickybits .update interface', () => { 411 | document.body.innerHTML = '
' 412 | const stickybit = stickybits('#manage-sticky', { useStickyClasses: true }) 413 | const instance = stickybit.instances[0] 414 | instance.stickyStart = 200 415 | expect(stickybit.instances[0].stickyStart).toBe(200) 416 | stickybit.update() 417 | expect(stickybit.instances[0].stickyStart).toBe(0) 418 | }) 419 | 420 | test('stickybits .update interface', () => { 421 | document.body.innerHTML = '
' 422 | const stickybit = stickybits('#manage-sticky', { useStickyClasses: true, stickyBitStickyOffset: 20 }) 423 | const instance = stickybit.instances[0] 424 | expect(stickybit.instances[0].props.stickyBitStickyOffset).toBe(20) 425 | stickybit.update({ stickyBitStickyOffset: 30 }) 426 | expect(stickybit.instances[0].props.stickyBitStickyOffset).toBe(30) 427 | }) 428 | 429 | test('stickybits .useFixed interface', () => { 430 | document.body.innerHTML = '
' 431 | const stickybit = stickybits('#manage-sticky', { useFixed: true }) 432 | const instance = stickybit.instances[0] 433 | expect(instance.props.useFixed).toBe(true) 434 | }) 435 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export default function stickybits( 2 | target: string | Element | Element[], 3 | options?: StickyBits.Options, 4 | ): StickyBits 5 | 6 | export interface StickyBits { 7 | els: Element[] 8 | instances: StickyBits.Instance[] 9 | 10 | props: StickyBits.Options 11 | userAgent: string 12 | version: string 13 | 14 | cleanup: () => void 15 | update: (props?: StickyBits.Options) => void 16 | } 17 | 18 | export namespace StickyBits { 19 | export interface Options { 20 | customStickyChangeNumber?: number | null 21 | noStyles?: boolean 22 | stickyBitStickyOffset?: number 23 | parentClass?: string 24 | scrollEl?: Element | string 25 | stickyClass?: string 26 | stuckClass?: string 27 | stickyChangeClass?: string 28 | useStickyClasses?: boolean 29 | useFixed?: boolean 30 | useGetBoundingClientRect?: boolean 31 | verticalPosition?: 'top' | 'bottom' 32 | } 33 | 34 | export interface Instance { 35 | el: Element 36 | parent: Element 37 | props: StickyBits.Options 38 | } 39 | } 40 | --------------------------------------------------------------------------------