]`.
71 | .u-pull-#{$numerator}#{$fractions-delimiter}#{$denominator}#{$breakpoint} {
72 | position: relative $important;
73 | right: ($numerator / $denominator) * 100% $important;
74 | left: auto $important;
75 | }
76 | }
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/docs/src/styles/utilities/_align.scss:
--------------------------------------------------------------------------------
1 | // ==========================================================================
2 | // Utilities / Alignment
3 | // ==========================================================================
4 |
5 | // Floats
6 | // ==========================================================================
7 | .u-float-left {
8 | float: left !important;
9 | }
10 |
11 | .u-float-right {
12 | float: right !important;
13 | }
14 |
15 | // Horizontal Text
16 | // ==========================================================================
17 | .u-text-center {
18 | text-align: center !important;
19 | }
20 |
21 | .u-text-left {
22 | text-align: left !important;
23 | }
24 |
25 | .u-text-right {
26 | text-align: right !important;
27 | }
28 |
29 | // Vertical Text
30 | // ==========================================================================
31 | .u-align-baseline {
32 | vertical-align: baseline !important;
33 | }
34 |
35 | .u-align-bottom {
36 | vertical-align: bottom !important;
37 | }
38 |
39 | .u-align-middle {
40 | vertical-align: middle !important;
41 | }
42 |
43 | .u-align-top {
44 | vertical-align: top !important;
45 | }
46 |
47 | .u-vertical-center {
48 | @include o-vertical-center;
49 | }
50 |
--------------------------------------------------------------------------------
/docs/src/styles/utilities/_headings.scss:
--------------------------------------------------------------------------------
1 | // ==========================================================================
2 | // Utilities / Headings
3 | // ==========================================================================
4 |
5 | /**
6 | * Redefine all of our basic heading styles against utility classes so as to
7 | * provide larger (or smaller) generic font sizes. Anything more opinionated
8 | * than simple font-size changes should likely be applied via "o-" classes
9 | *
10 | * @example
11 | *
12 | *
13 | * @requires base/headings
14 | * @link http://csswizardry.com/2016/02/managing-typography-on-large-apps/
15 | * @link https://github.com/inuitcss/inuitcss/blob/develop/utilities/_utilities.headings.scss
16 | */
17 |
18 | .u-h1 {
19 | font-size: rem($font-size-h1) !important;
20 | }
21 |
22 | .u-h2 {
23 | font-size: rem($font-size-h2) !important;
24 | }
25 |
26 | .u-h3 {
27 | font-size: rem($font-size-h3) !important;
28 | }
29 |
30 | .u-h4 {
31 | font-size: rem($font-size-h4) !important;
32 | }
33 |
34 | .u-h5 {
35 | font-size: rem($font-size-h5) !important;
36 | }
37 |
38 | .u-h6 {
39 | font-size: rem($font-size-h6) !important;
40 | }
41 |
--------------------------------------------------------------------------------
/docs/src/styles/utilities/_helpers.scss:
--------------------------------------------------------------------------------
1 | // ==========================================================================
2 | // Utilities / Helpers
3 | // ==========================================================================
4 |
5 | // Colors
6 | .u-white {
7 | color: $white;
8 | }
9 |
10 | // Text / font
11 | .u-label {
12 | @include u-label;
13 | }
14 |
15 | .u-icon {
16 | font-family: $font-lucida;
17 | font-size: rem(18px);
18 | }
19 |
20 | .u-text {
21 | font-size: rem(14px);
22 | }
23 |
24 | // Layout
25 | // ==========================================================================
26 | .u-clearfix {
27 | @include u-clearfix;
28 | }
29 |
30 | // Decorative
31 | // =============================================================================
32 | .u-truncate {
33 | @include u-truncate;
34 | }
35 |
36 | // Visibility / Display
37 | // ==========================================================================
38 | [hidden][aria-hidden="false"] {
39 | position: absolute;
40 | display: inherit;
41 | clip: rect(0, 0, 0, 0);
42 | }
43 |
44 | [hidden][aria-hidden="false"]:focus {
45 | clip: auto;
46 | }
47 |
48 | // .u-block {
49 | // display: block;
50 | // }
51 |
52 | // /**
53 | // * 1. Fix for Firefox bug: an image styled `max-width:100%` within an
54 | // * inline-block will display at its default size, and not limit its width to
55 | // * 100% of an ancestral container.
56 | // */
57 | // .u-inline-block {
58 | // display: inline-block !important;
59 | // max-width: 100%; /* 1 */
60 | // }
61 |
62 | // .u-inline {
63 | // display: inline !important;
64 | // }
65 |
66 | // .u-table {
67 | // display: table !important;
68 | // }
69 |
70 | // .u-tableCell {
71 | // display: table-cell !important;
72 | // }
73 |
74 | // .u-tableRow {
75 | // display: table-row !important;
76 | // }
77 |
78 | /**
79 | * Completely remove from the flow but leave available to screen readers.
80 | */
81 | .u-screen-reader-text {
82 | @include u-accessibly-hidden;
83 | }
84 |
85 | @media not print {
86 | .u-screen-reader-text\@screen {
87 | @include u-accessibly-hidden;
88 | }
89 | }
90 |
91 | /*
92 | * Extends the `.screen-reader-text` class to allow the element
93 | * to be focusable when navigated to via the keyboard.
94 | *
95 | * @link https://www.drupal.org/node/897638
96 | * @todo Define styles when focused.
97 | */
98 | .u-screen-reader-text.-focusable {
99 | @include u-accessibly-focusable;
100 | }
101 |
--------------------------------------------------------------------------------
/docs/src/styles/utilities/_print.scss:
--------------------------------------------------------------------------------
1 | // ==========================================================================
2 | // Utilities / Print Mode
3 | // ==========================================================================
4 |
5 | ////
6 | /// Very crude, reset-like styles taken from the HTML5 Boilerplate:
7 | /// - https://github.com/h5bp/html5-boilerplate/blob/5.3.0/dist/doc/css.md#print-styles
8 | /// - https://github.com/h5bp/html5-boilerplate/blob/master/dist/css/main.css#L205-L282
9 | ///
10 | /// @link https://github.com/inuitcss/inuitcss/blob/c27993f/utilities/_utilities.print.scss
11 | ////
12 |
13 | @media print {
14 | /**
15 | * 1. Black prints faster: http://www.sanbeiji.com/archives/953
16 | */
17 | *,
18 | *:before,
19 | *:after,
20 | *:first-letter,
21 | *:first-line {
22 | background: transparent !important;
23 | box-shadow: none !important;
24 | color: #000000 !important; /* [1] */
25 | text-shadow: none !important;
26 | }
27 |
28 | a,
29 | a:visited {
30 | text-decoration: underline;
31 | }
32 |
33 | a[href]:after {
34 | content: " (" attr(href) ")";
35 | }
36 |
37 | abbr[title]:after {
38 | content: " (" attr(title) ")";
39 | }
40 |
41 | /**
42 | * Don't show links that are fragment identifiers, or use the `javascript:`
43 | * pseudo protocol.
44 | */
45 | a[href^="#"]:after,
46 | a[href^="javascript:"]:after {
47 | content: "";
48 | }
49 |
50 | pre,
51 | blockquote {
52 | border: 1px solid #999999;
53 | page-break-inside: avoid;
54 | }
55 |
56 | /**
57 | * Printing Tables: http://css-discuss.incutio.com/wiki/Printing_Tables
58 | */
59 | thead {
60 | display: table-header-group;
61 | }
62 |
63 | tr,
64 | img {
65 | page-break-inside: avoid;
66 | }
67 |
68 |
69 | img {
70 | max-width: 100% !important;
71 | }
72 |
73 | p,
74 | h2,
75 | h3 {
76 | orphans: 3;
77 | widows: 3;
78 | }
79 |
80 | h2,
81 | h3 {
82 | page-break-after: avoid;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/docs/src/styles/utilities/_ratio.scss:
--------------------------------------------------------------------------------
1 | // ==========================================================================
2 | // Utilities / Ratio
3 | // ==========================================================================
4 |
5 | //
6 | // @link https://github.com/inuitcss/inuitcss/blob/19d0c7e/objects/_objects.ratio.scss
7 | //
8 |
9 | // A list of aspect ratios that get generated as modifier classes.
10 |
11 | $aspect-ratios: (
12 | (2:1),
13 | (4:3),
14 | (16:9),
15 | ) !default;
16 |
17 | /* stylelint-disable */
18 |
19 | //
20 | // Generate a series of ratio classes to be used like so:
21 | //
22 | // @example
23 | //
24 | //
25 | //
26 | @each $ratio in $aspect-ratios {
27 | @each $antecedent, $consequent in $ratio {
28 | @if (type-of($antecedent) != number) {
29 | @error "`#{$antecedent}` needs to be a number."
30 | }
31 |
32 | @if (type-of($consequent) != number) {
33 | @error "`#{$consequent}` needs to be a number."
34 | }
35 |
36 | &.u-#{$antecedent}\:#{$consequent}::before {
37 | padding-bottom: ($consequent/$antecedent) * 100%;
38 | }
39 | }
40 | }
41 |
42 | /* stylelint-enable */
43 |
--------------------------------------------------------------------------------
/docs/src/styles/utilities/_spacing.scss:
--------------------------------------------------------------------------------
1 | // ==========================================================================
2 | // Utilities / Spacing
3 | // ==========================================================================
4 |
5 | ////
6 | /// Utility classes to put specific spacing values onto elements. The below loop
7 | /// will generate us a suite of classes like:
8 | ///
9 | /// @example
10 | /// .u-margin-top {}
11 | /// .u-padding-left-large {}
12 | /// .u-margin-right-small {}
13 | /// .u-padding {}
14 | /// .u-padding-right-none {}
15 | /// .u-padding-horizontal {}
16 | /// .u-padding-vertical-small {}
17 | ///
18 | /// @link https://github.com/inuitcss/inuitcss/blob/512977a/utilities/_utilities.spacing.scss
19 | ////
20 |
21 | /* stylelint-disable string-quotes */
22 |
23 | $spacing-directions: (
24 | null: null,
25 | '-top': '-top',
26 | '-right': '-right',
27 | '-bottom': '-bottom',
28 | '-left': '-left',
29 | '-horizontal': '-left' '-right',
30 | '-vertical': '-top' '-bottom',
31 | ) !default;
32 |
33 | $spacing-properties: (
34 | 'padding': 'padding',
35 | 'margin': 'margin',
36 | ) !default;
37 |
38 | $spacing-sizes: (
39 | null: $unit,
40 | '-small': $unit-small,
41 | '-none': 0
42 | ) !default;
43 |
44 | @each $property-namespace, $property in $spacing-properties {
45 | @each $direction-namespace, $direction-rules in $spacing-directions {
46 | @each $size-namespace, $size in $spacing-sizes {
47 | .u-#{$property-namespace}#{$direction-namespace}#{$size-namespace} {
48 | @each $direction in $direction-rules {
49 | #{$property}#{$direction}: $size !important;
50 | }
51 | }
52 | }
53 | }
54 | }
55 |
56 | /* stylelint-enable string-quotes */
57 |
--------------------------------------------------------------------------------
/docs/src/styles/utilities/_states.scss:
--------------------------------------------------------------------------------
1 | // ==========================================================================
2 | // Utilities / States
3 | // ==========================================================================
4 |
5 | /**
6 | * ARIA roles display visual cursor hints
7 | */
8 | [aria-busy="true"] {
9 | cursor: progress;
10 | }
11 |
12 | [aria-controls] {
13 | cursor: pointer;
14 | }
15 |
16 | [aria-disabled] {
17 | cursor: default;
18 | }
19 |
20 | /**
21 | * Control visibility without affecting flow.
22 | */
23 |
24 | .is-visible {
25 | visibility: visible !important;
26 | opacity: 1 !important;
27 | }
28 |
29 | .is-invisible {
30 | visibility: hidden !important;
31 | opacity: 0 !important;
32 | }
33 |
34 | /**
35 | * Completely remove from the flow and screen readers.
36 | */
37 |
38 | .is-hidden {
39 | @include u-hidden;
40 | }
41 |
42 | @media not print {
43 | .is-hidden\@screen {
44 | @include u-hidden;
45 | }
46 | }
47 |
48 | @media print {
49 | .is-hidden\@print {
50 | @include u-hidden;
51 | }
52 | }
53 |
54 | // .is-hidden\@to-large {
55 | // @media (max-width: $to-large) {
56 | // display: none;
57 | // }
58 | // }
59 |
60 | // .is-hidden\@from-large {
61 | // @media (min-width: $from-large) {
62 | // display: none;
63 | // }
64 | // }
65 |
66 | // /**
67 | // * Display a hidden-by-default element.
68 | // */
69 |
70 | // .is-shown {
71 | // @include u-shown;
72 | // }
73 |
74 | // table.is-shown {
75 | // display: table !important;
76 | // }
77 |
78 | // tr.is-shown {
79 | // display: table-row !important;
80 | // }
81 |
82 | // td.is-shown,
83 | // th.is-shown {
84 | // display: table-cell !important;
85 | // }
86 |
--------------------------------------------------------------------------------
/docs/src/styles/utilities/_widths.scss:
--------------------------------------------------------------------------------
1 | // ==========================================================================
2 | // Utilities / Widths
3 | // ==========================================================================
4 |
5 | ////
6 | /// @link https://github.com/inuitcss/inuitcss/blob/6eb574f/utilities/_utilities.widths.scss
7 | ////
8 |
9 | /// Which fractions would you like in your grid system(s)?
10 | /// By default, the boilerplate provides fractions of one whole, halves, thirds,
11 | /// quarters, and fifths, e.g.:
12 | ///
13 | /// @example css
14 | /// .u-1/2
15 | /// .u-2/5
16 | /// .u-3/4
17 | /// .u-2/3
18 | $widths-fractions: 1 2 3 4 5 !default;
19 |
20 | @include widths($widths-fractions);
21 |
22 | .u-1\/2\@from-small {
23 | @media (min-width: $from-small) {
24 | width: span(1/2);
25 | }
26 | }
27 |
28 | .u-1\/3\@from-medium {
29 | @media (min-width: $from-medium) {
30 | width: span(1/3);
31 | }
32 | }
33 |
34 | .u-1\/2\@from-medium {
35 | @media (min-width: $from-medium) {
36 | width: span(1/2);
37 | }
38 | }
39 |
40 | .u-2\/5\@from-medium {
41 | @media (min-width: $from-medium) {
42 | width: span(2/5);
43 | }
44 | }
45 |
46 | .u-3\/5\@from-medium {
47 | @media (min-width: $from-medium) {
48 | width: span(3/5);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/docs/src/styles/vendors/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/locomotivemtl/locomotive-scroll/8ac3990434182eb3e782edeae3d16e1cdf3ac015/docs/src/styles/vendors/.gitkeep
--------------------------------------------------------------------------------
/gulpfile.babel.js:
--------------------------------------------------------------------------------
1 | import gulp from 'gulp';
2 | import styles from './build/styles.js';
3 | import scripts from './build/scripts.js';
4 | import svgs from './build/svgs.js';
5 | import serve from './build/serve.js';
6 | import watch from './build/watch.js';
7 | import copy from './build/copy.js';
8 | import { buildStyles, buildScripts } from './build/build.js';
9 |
10 | const compile = gulp.series(styles, scripts, svgs);
11 | const main = gulp.series(copy, compile, serve, watch);
12 | const build = gulp.series(copy, compile, buildStyles, buildScripts);
13 |
14 | gulp.task('default', main);
15 | gulp.task('compile', compile);
16 | gulp.task('build', build);
17 | gulp.task('copy', copy);
18 |
--------------------------------------------------------------------------------
/mconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "src": "./src/",
3 | "dest": "./dist/",
4 | "docs": {
5 | "src": "./docs/src/",
6 | "dest": "./docs/"
7 | },
8 | "build": "./build/",
9 | "styles": {
10 | "src": "./src/",
11 | "dest": "./dist/",
12 | "main": "locomotive-scroll",
13 | "docs": {
14 | "src": "./docs/src/styles/",
15 | "dest": "./docs/dist/styles/",
16 | "main": "main"
17 | }
18 | },
19 | "scripts": {
20 | "src": "./src/",
21 | "dest": "./dist/",
22 | "main": "locomotive-scroll",
23 | "docs": {
24 | "src": "./docs/src/scripts/",
25 | "dest": "./docs/dist/scripts/",
26 | "main": "main"
27 | }
28 | },
29 | "svgs": {
30 | "src": "./docs/src/images/sprite/",
31 | "dest": "./docs/dist/images/"
32 | },
33 | "views": {
34 | "src": "./docs/"
35 | },
36 | "modules": {
37 | "build": "gulp",
38 | "style": "sass",
39 | "script": "js",
40 | "view": false
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "locomotive-scroll",
3 | "version": "4.1.4",
4 | "description": "Detection of elements in viewport & smooth scrolling with parallax effects.",
5 | "repository": "locomotivemtl/locomotive-scroll",
6 | "author": "Locomotive (https://locomotive.ca)",
7 | "license": "MIT",
8 | "main": "./dist/locomotive-scroll.js",
9 | "module": "./dist/locomotive-scroll.esm.js",
10 | "scripts": {
11 | "start": "gulp",
12 | "format": "prettier --write 'src/**/*.js'",
13 | "build": "npm run format && gulp build"
14 | },
15 | "devDependencies": {
16 | "@modularbp/gulp": "^1.0.5",
17 | "@modularbp/gulp-build": "^1.1.0",
18 | "@modularbp/gulp-error": "^1.0.3",
19 | "@modularbp/gulp-js": "^1.0.8",
20 | "@modularbp/gulp-notify": "^1.0.3",
21 | "@modularbp/gulp-sass": "^1.0.7",
22 | "@modularbp/gulp-serve": "^1.0.5",
23 | "@modularbp/gulp-svg": "^1.0.5",
24 | "@modularbp/gulp-watch": "^1.0.5",
25 | "gulp-header": "^2.0.9",
26 | "mbp": "^1.3.0",
27 | "merge-stream": "^2.0.0",
28 | "node-sass": "^6.0.1",
29 | "normalize.css": "^8.0.1",
30 | "postcss": "^8.3.11",
31 | "prettier": "^2.1.2"
32 | },
33 | "dependencies": {
34 | "bezier-easing": "^2.1.0",
35 | "smoothscroll-polyfill": "^0.4.4",
36 | "virtual-scroll": "^1.5.2"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/locomotive-scroll.js:
--------------------------------------------------------------------------------
1 | export { default } from './scripts/Main';
2 | export { Native } from './scripts/NativeMain';
3 | export { Smooth } from './scripts/Main';
4 |
--------------------------------------------------------------------------------
/src/locomotive-scroll.scss:
--------------------------------------------------------------------------------
1 | @import "styles/base";
2 | @import "styles/scrollbar";
3 |
--------------------------------------------------------------------------------
/src/locomotive-scroll.umd.js:
--------------------------------------------------------------------------------
1 | export { default } from './scripts/Main';
2 |
--------------------------------------------------------------------------------
/src/scripts/Core.js:
--------------------------------------------------------------------------------
1 | import { defaults } from './options';
2 |
3 | export default class {
4 | constructor(options = {}) {
5 | Object.assign(this, defaults, options);
6 | this.smartphone = defaults.smartphone;
7 | if (options.smartphone) Object.assign(this.smartphone, options.smartphone);
8 | this.tablet = defaults.tablet;
9 | if (options.tablet) Object.assign(this.tablet, options.tablet);
10 |
11 | this.namespace = 'locomotive';
12 | this.html = document.documentElement;
13 | this.windowHeight = window.innerHeight;
14 | this.windowWidth = window.innerWidth;
15 | this.windowMiddle = {
16 | x: this.windowWidth / 2,
17 | y: this.windowHeight / 2
18 | };
19 | this.els = {};
20 | this.currentElements = {};
21 | this.listeners = {};
22 |
23 | this.hasScrollTicking = false;
24 | this.hasCallEventSet = false;
25 |
26 | this.checkScroll = this.checkScroll.bind(this);
27 | this.checkResize = this.checkResize.bind(this);
28 | this.checkEvent = this.checkEvent.bind(this);
29 |
30 | this.instance = {
31 | scroll: {
32 | x: 0,
33 | y: 0
34 | },
35 | limit: {
36 | x: this.html.offsetWidth,
37 | y: this.html.offsetHeight
38 | },
39 | currentElements: this.currentElements
40 | };
41 |
42 | if (this.isMobile) {
43 | if (this.isTablet) {
44 | this.context = 'tablet';
45 | } else {
46 | this.context = 'smartphone';
47 | }
48 | } else {
49 | this.context = 'desktop';
50 | }
51 |
52 | if (this.isMobile) this.direction = this[this.context].direction;
53 | if (this.direction === 'horizontal') {
54 | this.directionAxis = 'x';
55 | } else {
56 | this.directionAxis = 'y';
57 | }
58 |
59 | if (this.getDirection) {
60 | this.instance.direction = null;
61 | }
62 |
63 | if (this.getDirection) {
64 | this.instance.speed = 0;
65 | }
66 |
67 | this.html.classList.add(this.initClass);
68 |
69 | window.addEventListener('resize', this.checkResize, false);
70 | }
71 |
72 | init() {
73 | this.initEvents();
74 | }
75 |
76 | checkScroll() {
77 | this.dispatchScroll();
78 | }
79 |
80 | checkResize() {
81 | if (!this.resizeTick) {
82 | this.resizeTick = true;
83 | requestAnimationFrame(() => {
84 | this.resize();
85 | this.resizeTick = false;
86 | });
87 | }
88 | }
89 |
90 | resize() {}
91 |
92 | checkContext() {
93 | if (!this.reloadOnContextChange) return;
94 |
95 | this.isMobile =
96 | /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
97 | (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) ||
98 | this.windowWidth < this.tablet.breakpoint;
99 | this.isTablet = this.isMobile && this.windowWidth >= this.tablet.breakpoint;
100 |
101 | let oldContext = this.context;
102 | if (this.isMobile) {
103 | if (this.isTablet) {
104 | this.context = 'tablet';
105 | } else {
106 | this.context = 'smartphone';
107 | }
108 | } else {
109 | this.context = 'desktop';
110 | }
111 |
112 | if (oldContext != this.context) {
113 | let oldSmooth = oldContext == 'desktop' ? this.smooth : this[oldContext].smooth;
114 | let newSmooth = this.context == 'desktop' ? this.smooth : this[this.context].smooth;
115 |
116 | if (oldSmooth != newSmooth) window.location.reload();
117 | }
118 | }
119 |
120 | initEvents() {
121 | this.scrollToEls = this.el.querySelectorAll(`[data-${this.name}-to]`);
122 | this.setScrollTo = this.setScrollTo.bind(this);
123 |
124 | this.scrollToEls.forEach((el) => {
125 | el.addEventListener('click', this.setScrollTo, false);
126 | });
127 | }
128 |
129 | setScrollTo(event) {
130 | event.preventDefault();
131 |
132 | this.scrollTo(
133 | event.currentTarget.getAttribute(`data-${this.name}-href`) ||
134 | event.currentTarget.getAttribute('href'),
135 | {
136 | offset: event.currentTarget.getAttribute(`data-${this.name}-offset`)
137 | }
138 | );
139 | }
140 |
141 | addElements() {}
142 |
143 | detectElements(hasCallEventSet) {
144 | const scrollTop = this.instance.scroll.y;
145 | const scrollBottom = scrollTop + this.windowHeight;
146 |
147 | const scrollLeft = this.instance.scroll.x;
148 | const scrollRight = scrollLeft + this.windowWidth;
149 |
150 | Object.entries(this.els).forEach(([i, el]) => {
151 | if (el && (!el.inView || hasCallEventSet)) {
152 | if (this.direction === 'horizontal') {
153 | if (scrollRight >= el.left && scrollLeft < el.right) {
154 | this.setInView(el, i);
155 | }
156 | } else {
157 | if (scrollBottom >= el.top && scrollTop < el.bottom) {
158 | this.setInView(el, i);
159 | }
160 | }
161 | }
162 |
163 | if (el && el.inView) {
164 | if (this.direction === 'horizontal') {
165 | let width = el.right - el.left;
166 | el.progress =
167 | (this.instance.scroll.x - (el.left - this.windowWidth)) /
168 | (width + this.windowWidth);
169 |
170 | if (scrollRight < el.left || scrollLeft > el.right) {
171 | this.setOutOfView(el, i);
172 | }
173 | } else {
174 | let height = el.bottom - el.top;
175 | el.progress =
176 | (this.instance.scroll.y - (el.top - this.windowHeight)) /
177 | (height + this.windowHeight);
178 |
179 | if (scrollBottom < el.top || scrollTop > el.bottom) {
180 | this.setOutOfView(el, i);
181 | }
182 | }
183 | }
184 | });
185 |
186 | // this.els = this.els.filter((current, i) => {
187 | // return current !== null;
188 | // });
189 |
190 | this.hasScrollTicking = false;
191 | }
192 |
193 | setInView(current, i) {
194 | this.els[i].inView = true;
195 | current.el.classList.add(current.class);
196 |
197 | this.currentElements[i] = current;
198 |
199 | if (current.call && this.hasCallEventSet) {
200 | this.dispatchCall(current, 'enter');
201 |
202 | if (!current.repeat) {
203 | this.els[i].call = false;
204 | }
205 | }
206 |
207 | // if (!current.repeat && !current.speed && !current.sticky) {
208 | // if (!current.call || current.call && this.hasCallEventSet) {
209 | // this.els[i] = null
210 | // }
211 | // }
212 | }
213 |
214 | setOutOfView(current, i) {
215 | // if (current.repeat || current.speed !== undefined) {
216 | this.els[i].inView = false;
217 | // }
218 |
219 | Object.keys(this.currentElements).forEach((el) => {
220 | el === i && delete this.currentElements[el];
221 | });
222 |
223 | if (current.call && this.hasCallEventSet) {
224 | this.dispatchCall(current, 'exit');
225 | }
226 |
227 | if (current.repeat) {
228 | current.el.classList.remove(current.class);
229 | }
230 | }
231 |
232 | dispatchCall(current, way) {
233 | this.callWay = way;
234 | this.callValue = current.call.split(',').map((item) => item.trim());
235 | this.callObj = current;
236 |
237 | if (this.callValue.length == 1) this.callValue = this.callValue[0];
238 |
239 | const callEvent = new Event(this.namespace + 'call');
240 | this.el.dispatchEvent(callEvent);
241 | }
242 |
243 | dispatchScroll() {
244 | const scrollEvent = new Event(this.namespace + 'scroll');
245 | this.el.dispatchEvent(scrollEvent);
246 | }
247 |
248 | setEvents(event, func) {
249 | if (!this.listeners[event]) {
250 | this.listeners[event] = [];
251 | }
252 |
253 | const list = this.listeners[event];
254 | list.push(func);
255 |
256 | if (list.length === 1) {
257 | this.el.addEventListener(this.namespace + event, this.checkEvent, false);
258 | }
259 |
260 | if (event === 'call') {
261 | this.hasCallEventSet = true;
262 | this.detectElements(true);
263 | }
264 | }
265 |
266 | unsetEvents(event, func) {
267 | if (!this.listeners[event]) return;
268 |
269 | const list = this.listeners[event];
270 | const index = list.indexOf(func);
271 |
272 | if (index < 0) return;
273 |
274 | list.splice(index, 1);
275 |
276 | if (list.index === 0) {
277 | this.el.removeEventListener(this.namespace + event, this.checkEvent, false);
278 | }
279 | }
280 |
281 | checkEvent(event) {
282 | const name = event.type.replace(this.namespace, '');
283 | const list = this.listeners[name];
284 |
285 | if (!list || list.length === 0) return;
286 |
287 | list.forEach((func) => {
288 | switch (name) {
289 | case 'scroll':
290 | return func(this.instance);
291 | case 'call':
292 | return func(this.callValue, this.callWay, this.callObj);
293 | default:
294 | return func();
295 | }
296 | });
297 | }
298 |
299 | startScroll() {}
300 |
301 | stopScroll() {}
302 |
303 | setScroll(x, y) {
304 | this.instance.scroll = {
305 | x: 0,
306 | y: 0
307 | };
308 | }
309 |
310 | destroy() {
311 | window.removeEventListener('resize', this.checkResize, false);
312 |
313 | Object.keys(this.listeners).forEach((event) => {
314 | this.el.removeEventListener(this.namespace + event, this.checkEvent, false);
315 | });
316 | this.listeners = {};
317 |
318 | this.scrollToEls.forEach((el) => {
319 | el.removeEventListener('click', this.setScrollTo, false);
320 | });
321 |
322 | this.html.classList.remove(this.initClass);
323 | }
324 | }
325 |
--------------------------------------------------------------------------------
/src/scripts/Main.js:
--------------------------------------------------------------------------------
1 | import { defaults } from './options';
2 | import NativeScroll from './Native';
3 | import SmoothScroll from './Smooth';
4 |
5 | export class Smooth {
6 | constructor(options = {}) {
7 | this.options = options;
8 |
9 | // Override default options with given ones
10 | Object.assign(this, defaults, options);
11 | this.smartphone = defaults.smartphone;
12 | if (options.smartphone) Object.assign(this.smartphone, options.smartphone);
13 | this.tablet = defaults.tablet;
14 | if (options.tablet) Object.assign(this.tablet, options.tablet);
15 |
16 | if (!this.smooth && this.direction == 'horizontal')
17 | console.warn('🚨 `smooth:false` & `horizontal` direction are not yet compatible');
18 | if (!this.tablet.smooth && this.tablet.direction == 'horizontal')
19 | console.warn(
20 | '🚨 `smooth:false` & `horizontal` direction are not yet compatible (tablet)'
21 | );
22 | if (!this.smartphone.smooth && this.smartphone.direction == 'horizontal')
23 | console.warn(
24 | '🚨 `smooth:false` & `horizontal` direction are not yet compatible (smartphone)'
25 | );
26 |
27 | this.init();
28 | }
29 |
30 | init() {
31 | this.options.isMobile =
32 | /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ||
33 | (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1) ||
34 | window.innerWidth < this.tablet.breakpoint;
35 | this.options.isTablet =
36 | this.options.isMobile && window.innerWidth >= this.tablet.breakpoint;
37 |
38 | if (
39 | (this.smooth && !this.options.isMobile) ||
40 | (this.tablet.smooth && this.options.isTablet) ||
41 | (this.smartphone.smooth && this.options.isMobile && !this.options.isTablet)
42 | ) {
43 | this.scroll = new SmoothScroll(this.options);
44 | } else {
45 | this.scroll = new NativeScroll(this.options);
46 | }
47 |
48 | this.scroll.init();
49 |
50 | if (window.location.hash) {
51 | // Get the hash without the '#' and find the matching element
52 | const id = window.location.hash.slice(1, window.location.hash.length);
53 | let target = document.getElementById(id);
54 |
55 | // If found, scroll to the element
56 | if (target) this.scroll.scrollTo(target);
57 | }
58 | }
59 |
60 | update() {
61 | this.scroll.update();
62 | }
63 |
64 | start() {
65 | this.scroll.startScroll();
66 | }
67 |
68 | stop() {
69 | this.scroll.stopScroll();
70 | }
71 |
72 | scrollTo(target, options) {
73 | this.scroll.scrollTo(target, options);
74 | }
75 |
76 | setScroll(x, y) {
77 | this.scroll.setScroll(x, y);
78 | }
79 |
80 | on(event, func) {
81 | this.scroll.setEvents(event, func);
82 | }
83 |
84 | off(event, func) {
85 | this.scroll.unsetEvents(event, func);
86 | }
87 |
88 | destroy() {
89 | this.scroll.destroy();
90 | }
91 | }
92 |
93 | export default Smooth;
94 |
--------------------------------------------------------------------------------
/src/scripts/Native.js:
--------------------------------------------------------------------------------
1 | import Core from './Core';
2 | import smoothscroll from 'smoothscroll-polyfill';
3 |
4 | export default class extends Core {
5 | constructor(options = {}) {
6 | super(options);
7 |
8 | if (this.resetNativeScroll) {
9 | if (history.scrollRestoration) {
10 | history.scrollRestoration = 'manual';
11 | }
12 | window.scrollTo(0, 0);
13 | }
14 |
15 | window.addEventListener('scroll', this.checkScroll, false);
16 |
17 | if (window.smoothscrollPolyfill === undefined) {
18 | window.smoothscrollPolyfill = smoothscroll;
19 | window.smoothscrollPolyfill.polyfill();
20 | }
21 | }
22 |
23 | init() {
24 | this.instance.scroll.y = window.pageYOffset;
25 |
26 | this.addElements();
27 | this.detectElements();
28 |
29 | super.init();
30 | }
31 |
32 | checkScroll() {
33 | super.checkScroll();
34 |
35 | if (this.getDirection) {
36 | this.addDirection();
37 | }
38 |
39 | if (this.getSpeed) {
40 | this.addSpeed();
41 | this.speedTs = Date.now();
42 | }
43 |
44 | this.instance.scroll.y = window.pageYOffset;
45 |
46 | if (Object.entries(this.els).length) {
47 | if (!this.hasScrollTicking) {
48 | requestAnimationFrame(() => {
49 | this.detectElements();
50 | });
51 | this.hasScrollTicking = true;
52 | }
53 | }
54 | }
55 |
56 | addDirection() {
57 | if (window.pageYOffset > this.instance.scroll.y) {
58 | if (this.instance.direction !== 'down') {
59 | this.instance.direction = 'down';
60 | }
61 | } else if (window.pageYOffset < this.instance.scroll.y) {
62 | if (this.instance.direction !== 'up') {
63 | this.instance.direction = 'up';
64 | }
65 | }
66 | }
67 |
68 | addSpeed() {
69 | if (window.pageYOffset != this.instance.scroll.y) {
70 | this.instance.speed =
71 | (window.pageYOffset - this.instance.scroll.y) /
72 | Math.max(1, Date.now() - this.speedTs);
73 | } else {
74 | this.instance.speed = 0;
75 | }
76 | }
77 |
78 | resize() {
79 | if (Object.entries(this.els).length) {
80 | this.windowHeight = window.innerHeight;
81 | this.updateElements();
82 | }
83 | }
84 |
85 | addElements() {
86 | this.els = {};
87 | const els = this.el.querySelectorAll('[data-' + this.name + ']');
88 |
89 | els.forEach((el, index) => {
90 | const BCR = el.getBoundingClientRect();
91 | let cl = el.dataset[this.name + 'Class'] || this.class;
92 | let id =
93 | typeof el.dataset[this.name + 'Id'] === 'string'
94 | ? el.dataset[this.name + 'Id']
95 | : index;
96 | let top;
97 | let left;
98 | let offset =
99 | typeof el.dataset[this.name + 'Offset'] === 'string'
100 | ? el.dataset[this.name + 'Offset'].split(',')
101 | : this.offset;
102 | let repeat = el.dataset[this.name + 'Repeat'];
103 | let call = el.dataset[this.name + 'Call'];
104 |
105 | let target = el.dataset[this.name + 'Target'];
106 | let targetEl;
107 |
108 | if (target !== undefined) {
109 | targetEl = document.querySelector(`${target}`);
110 | } else {
111 | targetEl = el;
112 | }
113 |
114 | const targetElBCR = targetEl.getBoundingClientRect();
115 | top = targetElBCR.top + this.instance.scroll.y;
116 | left = targetElBCR.left + this.instance.scroll.x;
117 |
118 | let bottom = top + targetEl.offsetHeight;
119 | let right = left + targetEl.offsetWidth;
120 |
121 | if (repeat == 'false') {
122 | repeat = false;
123 | } else if (repeat != undefined) {
124 | repeat = true;
125 | } else {
126 | repeat = this.repeat;
127 | }
128 |
129 | let relativeOffset = this.getRelativeOffset(offset);
130 | top = top + relativeOffset[0];
131 | bottom = bottom - relativeOffset[1];
132 |
133 | const mappedEl = {
134 | el: el,
135 | targetEl: targetEl,
136 | id,
137 | class: cl,
138 | top: top,
139 | bottom: bottom,
140 | left,
141 | right,
142 | offset,
143 | progress: 0,
144 | repeat,
145 | inView: false,
146 | call
147 | };
148 |
149 | this.els[id] = mappedEl;
150 | if (el.classList.contains(cl)) {
151 | this.setInView(this.els[id], id);
152 | }
153 | });
154 | }
155 |
156 | updateElements() {
157 | Object.entries(this.els).forEach(([i, el]) => {
158 | const top = el.targetEl.getBoundingClientRect().top + this.instance.scroll.y;
159 | const bottom = top + el.targetEl.offsetHeight;
160 | const relativeOffset = this.getRelativeOffset(el.offset);
161 |
162 | this.els[i].top = top + relativeOffset[0];
163 | this.els[i].bottom = bottom - relativeOffset[1];
164 | });
165 |
166 | this.hasScrollTicking = false;
167 | }
168 |
169 | getRelativeOffset(offset) {
170 | let relativeOffset = [0, 0];
171 |
172 | if (offset) {
173 | for (var i = 0; i < offset.length; i++) {
174 | if (typeof offset[i] == 'string') {
175 | if (offset[i].includes('%')) {
176 | relativeOffset[i] = parseInt(
177 | (offset[i].replace('%', '') * this.windowHeight) / 100
178 | );
179 | } else {
180 | relativeOffset[i] = parseInt(offset[i]);
181 | }
182 | } else {
183 | relativeOffset[i] = offset[i];
184 | }
185 | }
186 | }
187 |
188 | return relativeOffset;
189 | }
190 |
191 | /**
192 | * Scroll to a desired target.
193 | *
194 | * @param Available options :
195 | * target {node, string, "top", "bottom", int} - The DOM element we want to scroll to
196 | * options {object} - Options object for additional settings.
197 | * @return {void}
198 | */
199 | scrollTo(target, options = {}) {
200 | // Parse options
201 | let offset = parseInt(options.offset) || 0; // An offset to apply on top of given `target` or `sourceElem`'s target
202 | const callback = options.callback ? options.callback : false; // function called when scrollTo completes (note that it won't wait for lerp to stabilize)
203 |
204 | if (typeof target === 'string') {
205 | // Selector or boundaries
206 | if (target === 'top') {
207 | target = this.html;
208 | } else if (target === 'bottom') {
209 | target = this.html.offsetHeight - window.innerHeight;
210 | } else {
211 | target = document.querySelector(target);
212 | // If the query fails, abort
213 | if (!target) {
214 | return;
215 | }
216 | }
217 | } else if (typeof target === 'number') {
218 | // Absolute coordinate
219 | target = parseInt(target);
220 | } else if (target && target.tagName) {
221 | // DOM Element
222 | // We good 👍
223 | } else {
224 | console.warn('`target` parameter is not valid');
225 | return;
226 | }
227 |
228 | // We have a target that is not a coordinate yet, get it
229 | if (typeof target !== 'number') {
230 | offset = target.getBoundingClientRect().top + offset + this.instance.scroll.y;
231 | } else {
232 | offset = target + offset;
233 | }
234 |
235 | const isTargetReached = () => {
236 | return parseInt(window.pageYOffset) === parseInt(offset);
237 | };
238 | if (callback) {
239 | if (isTargetReached()) {
240 | callback();
241 | return;
242 | } else {
243 | let onScroll = function () {
244 | if (isTargetReached()) {
245 | window.removeEventListener('scroll', onScroll);
246 | callback();
247 | }
248 | };
249 | window.addEventListener('scroll', onScroll);
250 | }
251 | }
252 |
253 | window.scrollTo({
254 | top: offset,
255 | behavior: options.duration === 0 ? 'auto' : 'smooth'
256 | });
257 | }
258 |
259 | update() {
260 | this.addElements();
261 | this.detectElements();
262 | }
263 |
264 | destroy() {
265 | super.destroy();
266 |
267 | window.removeEventListener('scroll', this.checkScroll, false);
268 | }
269 | }
270 |
--------------------------------------------------------------------------------
/src/scripts/NativeMain.js:
--------------------------------------------------------------------------------
1 | import { defaults } from './options';
2 | import Scroll from './Native';
3 |
4 | export class Native {
5 | constructor(options = {}) {
6 | this.options = options;
7 |
8 | // Override default options with given ones
9 | Object.assign(this, defaults, options);
10 | this.smartphone = defaults.smartphone;
11 | if (options.smartphone) Object.assign(this.smartphone, options.smartphone);
12 | this.tablet = defaults.tablet;
13 | if (options.tablet) Object.assign(this.tablet, options.tablet);
14 |
15 | this.init();
16 | }
17 |
18 | init() {
19 | this.scroll = new Scroll(this.options);
20 |
21 | this.scroll.init();
22 |
23 | if (window.location.hash) {
24 | // Get the hash without the '#' and find the matching element
25 | const id = window.location.hash.slice(1, window.location.hash.length);
26 | let target = document.getElementById(id);
27 |
28 | // If found, scroll to the element
29 | if (target) this.scroll.scrollTo(target);
30 | }
31 | }
32 |
33 | update() {
34 | this.scroll.update();
35 | }
36 |
37 | start() {
38 | this.scroll.startScroll();
39 | }
40 |
41 | stop() {
42 | this.scroll.stopScroll();
43 | }
44 |
45 | scrollTo(target, options) {
46 | this.scroll.scrollTo(target, options);
47 | }
48 |
49 | setScroll(x, y) {
50 | this.scroll.setScroll(x, y);
51 | }
52 |
53 | on(event, func) {
54 | this.scroll.setEvents(event, func);
55 | }
56 |
57 | off(event, func) {
58 | this.scroll.unsetEvents(event, func);
59 | }
60 |
61 | destroy() {
62 | this.scroll.destroy();
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/scripts/options.js:
--------------------------------------------------------------------------------
1 | export const defaults = {
2 | el: document,
3 | name: 'scroll',
4 | offset: [0, 0],
5 | repeat: false,
6 | smooth: false,
7 | initPosition: { x: 0, y: 0 },
8 | direction: 'vertical',
9 | gestureDirection: 'vertical',
10 | reloadOnContextChange: false,
11 | lerp: 0.1,
12 | class: 'is-inview',
13 | scrollbarContainer: false,
14 | scrollbarClass: 'c-scrollbar',
15 | scrollingClass: 'has-scroll-scrolling',
16 | draggingClass: 'has-scroll-dragging',
17 | smoothClass: 'has-scroll-smooth',
18 | initClass: 'has-scroll-init',
19 | getSpeed: false,
20 | getDirection: false,
21 | scrollFromAnywhere: false,
22 | multiplier: 1,
23 | firefoxMultiplier: 50,
24 | touchMultiplier: 2,
25 | resetNativeScroll: true,
26 | tablet: {
27 | smooth: false,
28 | direction: 'vertical',
29 | gestureDirection: 'vertical',
30 | breakpoint: 1024
31 | },
32 | smartphone: {
33 | smooth: false,
34 | direction: 'vertical',
35 | gestureDirection: 'vertical'
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/src/scripts/utils/html.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Returns an array containing all the parent nodes of the given node
3 | * @param {object} node
4 | * @return {array} parent nodes
5 | */
6 | export function getParents(elem) {
7 | // Set up a parent array
8 | let parents = [];
9 |
10 | // Push each parent element to the array
11 | for (; elem && elem !== document; elem = elem.parentNode) {
12 | parents.push(elem);
13 | }
14 |
15 | // Return our parent array
16 | return parents;
17 | }
18 |
19 | // https://gomakethings.com/how-to-get-the-closest-parent-element-with-a-matching-selector-using-vanilla-javascript/
20 | export function queryClosestParent(elem, selector) {
21 | // Element.matches() polyfill
22 | if (!Element.prototype.matches) {
23 | Element.prototype.matches =
24 | Element.prototype.matchesSelector ||
25 | Element.prototype.mozMatchesSelector ||
26 | Element.prototype.msMatchesSelector ||
27 | Element.prototype.oMatchesSelector ||
28 | Element.prototype.webkitMatchesSelector ||
29 | function (s) {
30 | var matches = (this.document || this.ownerDocument).querySelectorAll(s),
31 | i = matches.length;
32 | while (--i >= 0 && matches.item(i) !== this) {}
33 | return i > -1;
34 | };
35 | }
36 |
37 | // Get the closest matching element
38 | for (; elem && elem !== document; elem = elem.parentNode) {
39 | if (elem.matches(selector)) return elem;
40 | }
41 | return null;
42 | }
43 |
--------------------------------------------------------------------------------
/src/scripts/utils/maths.js:
--------------------------------------------------------------------------------
1 | export function lerp(start, end, amt) {
2 | return (1 - amt) * start + amt * end;
3 | }
4 |
--------------------------------------------------------------------------------
/src/scripts/utils/transform.js:
--------------------------------------------------------------------------------
1 | export function transform(el, transformValue) {
2 | el.style.webkitTransform = transformValue;
3 | el.style.msTransform = transformValue;
4 | el.style.transform = transformValue;
5 | }
6 |
7 | export function getTranslate(el) {
8 | const translate = {};
9 | if (!window.getComputedStyle) return;
10 |
11 | const style = getComputedStyle(el);
12 | const transform = style.transform || style.webkitTransform || style.mozTransform;
13 |
14 | let mat = transform.match(/^matrix3d\((.+)\)$/);
15 | if (mat) {
16 | translate.x = mat ? parseFloat(mat[1].split(', ')[12]) : 0;
17 | translate.y = mat ? parseFloat(mat[1].split(', ')[13]) : 0;
18 | } else {
19 | mat = transform.match(/^matrix\((.+)\)$/);
20 | translate.x = mat ? parseFloat(mat[1].split(', ')[4]) : 0;
21 | translate.y = mat ? parseFloat(mat[1].split(', ')[5]) : 0;
22 | }
23 | return translate;
24 | }
25 |
--------------------------------------------------------------------------------
/src/styles/_base.scss:
--------------------------------------------------------------------------------
1 | html {
2 | &.has-scroll-smooth {
3 | overflow: hidden;
4 | }
5 |
6 | &.has-scroll-dragging {
7 | user-select: none;
8 | }
9 | }
10 |
11 | body {
12 | .has-scroll-smooth & {
13 | overflow: hidden;
14 | }
15 | }
16 |
17 | [data-scroll-container] {
18 | .has-scroll-smooth & {
19 | min-height: 100vh;
20 | }
21 | [data-scroll-direction="horizontal"] & {
22 | height: 100vh;
23 | display: inline-block;
24 | white-space: nowrap;
25 | }
26 | }
27 |
28 | [data-scroll-section] {
29 | [data-scroll-direction="horizontal"] & {
30 | display: inline-block;
31 | vertical-align: top;
32 | white-space: nowrap;
33 | height: 100%;
34 | }
35 | }
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/styles/_scrollbar.scss:
--------------------------------------------------------------------------------
1 | .c-scrollbar {
2 | position: absolute;
3 | right: 0;
4 | top: 0;
5 | width: 11px;
6 | height: 100%;
7 | transform-origin: center right;
8 | transition: transform 0.3s, opacity 0.3s;
9 | opacity: 0;
10 |
11 | &:hover {
12 | transform: scaleX(1.45);
13 | }
14 |
15 | &:hover, .has-scroll-scrolling &, .has-scroll-dragging & {
16 | opacity: 1;
17 | }
18 |
19 | [data-scroll-direction="horizontal"] & {
20 | width: 100%;
21 | height: 10px;
22 | top: auto;
23 | bottom: 0;
24 | transform: scaleY(1);
25 |
26 | &:hover {
27 | transform: scaleY(1.3);
28 | }
29 | }
30 |
31 | }
32 |
33 | .c-scrollbar_thumb {
34 | position: absolute;
35 | top: 0;
36 | right: 0;
37 | background-color: black;
38 | opacity: 0.5;
39 | width: 7px;
40 | border-radius: 10px;
41 | margin: 2px;
42 | cursor: grab;
43 |
44 | .has-scroll-dragging & {
45 | cursor: grabbing;
46 | }
47 |
48 | [data-scroll-direction="horizontal"] & {
49 | right: auto;
50 | bottom: 0;
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/www:
--------------------------------------------------------------------------------
1 | ./docs
--------------------------------------------------------------------------------