Skip to content
Snippets Groups Projects
Commit 40d25197 authored by Daniel Kurowski's avatar Daniel Kurowski
Browse files

Merge branch 'improve-structure' into 'master'

Improve structure

See merge request !7
parents 2d143420 bf7e6b60
No related branches found
No related tags found
1 merge request!7Improve structure
Pipeline #27195 passed
.idea/
.rpt2_cache/
.babelrc
demo.html
rollup.config.js
tsconfig.json
yarn.lock
......@@ -17,9 +17,13 @@ yarn add @grifart/smoothscroll
```javascript
import SmoothScroll from '@grifart/smoothscroll';
SmoothScroll({
load: true,
interaction: true,
// use defaults
SmoothScroll.enable();
// customize
SmoothScroll.enable({
scrollOnLoad: false,
scrollOnLinkClick: true,
});
```
......@@ -27,16 +31,18 @@ SmoothScroll({
| Option | Value | Default value | Description |
| --- | --- | --- | --- |
| `load` | `true`/`false` | `true` | Causes smooth scroll to anchored element when the page is loaded.\*
| `interaction` | `true`/`false` | `true` | Causes smooth scroll on given element when user clicks on an `a` tag having `href` starting with `#` character.
| `scrollOnLoad`\* | `true`/`false` | `true` | Causes smooth scroll to anchored element when the page is loaded.
| `scrollOnLinkClick` | `true`/`false` | `true` | Causes smooth scroll on given element when user clicks on an `a` tag having `href` starting with `#` character.
\*Note: when the page load lasts more than 500 ms, load smooth scrolling is disabled as it would lead to user-unfriendly behaviour like jumping on the page up and down.
\*Note: when the page load lasts more than 500 ms, load smooth scroll effect is disabled as it would lead to user-unfriendly behaviour like jumping on the page up and down due to browser native behaviour.
## Development
```bash
yarn install
yarn dev
```
If you need to check visually how the smooth scrolling behaviour acts like, you can take advantage of a testing file `demo.html` which has some lorem ipsum data and few of links to navigate through the content and test smooth scrolling.
Every piece of this library comes with its unit test sitting alongside the script. Whole library is covered by integration test sitting in `src` folder.
Note that you have to build assets first (`yarn build`) before running a test.
/**
* Represents valid hash as per https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/hash
*/
export class HashTarget
{
private constructor(
private readonly value: string,
private readonly targetElement: HTMLElement,
) {}
public static fromString(value: string, document: HTMLDocument): HashTarget
{
if (value === '' || value === '#') {
throw new Error('Hash does not contain any fragment.');
}
const targetElementId = value.substring(1);
const targetElement = document.getElementById(targetElementId);
if (targetElement === null) {
throw new Error(`No referenced element with ID ${targetElementId} exists.`);
}
return new this(value, targetElement);
}
public getHash(): string
{
return this.value;
}
public getElement(): HTMLElement
{
return this.targetElement;
}
}
export class AssertionError extends Error {}
export function assert(condition: any, message?: string): asserts condition {
if ( ! condition) {
throw new AssertionError(message);
}
}
import * as Velocity from 'velocity-animate';
declare const setupVelocity: (velocity: typeof Velocity) => void;
export declare const EASE_IN_SKIP_OUT_EASING: string;
export default setupVelocity;
export const EASE_IN_SKIP_OUT_EASING = 'ease-in-skip-out';
// e.g. (5, 5, 10, 500, 1000) => 500
// e.g. (5, 0, 10, 500, 1000) => 750
const mapIntervalLinear = (number, originalFrom, originalTo, newFrom, newTo) => {
......@@ -67,7 +69,7 @@ const computeHowMuchToSkip = (tweenDelta) => {
};
const setupVelocity = (velocity) => {
velocity.Easings['ease-in-skip-out'] = composeEasing(
velocity.Easings[EASE_IN_SKIP_OUT_EASING] = composeEasing(
(time, opts, tweenDelta) => mapIntervalLinear(
velocity.Easings['ease-in'](time, opts, tweenDelta),
0, 1, // from interval
......
import {scrollToTarget} from '../../scrollers/scrollToTarget';
import {assert} from '../../assert';
import {HashTarget} from '../../HashTarget';
export function initializeOnLinkClickScroll(): void
{
document.addEventListener('DOMContentLoaded', () =>
document.querySelectorAll('a[href^="#"]').forEach((item) =>
item.addEventListener('click', (event) => {
const element = event.currentTarget as HTMLAnchorElement;
assert(element !== null);
if (element.hash === '' || element.hash === '#') {
return;
}
event.preventDefault();
scrollToTarget(HashTarget.fromString(element.hash, document));
})));
}
This diff is collapsed.
import {scrollToTarget} from '../../scrollers/scrollToTarget';
import {HashTarget} from '../../HashTarget';
import {assert} from '../../assert';
/**
* Scrolls smoothly to an element if there is a hash in the URL.
* E.g. you have `#example` in URL, then it scrolls to element with id `example`.
*
* Note:
* Because of UX, this behaviour is limited only when whole document is loaded in less than 500ms.
* Otherwise, it jumps directly to desired element without smooth scrolling, because too visible jumping through the page would appear.
*/
export function initializeOnLoadScroll(): void
{
const hash = window.location.hash;
if (hash === '' || hash === '#') {
return;
}
let hashTarget: HashTarget|null = null;
const start = performance.now();
document.addEventListener('DOMContentLoaded', () => {
hashTarget = HashTarget.fromString(hash, document);
});
/**
* The `load` event has been chosen intentionally as it is the state when everything is ready -
* all styles are loaded and offsets are computed correctly - so the scroll will be computed correctly.
*/
window.addEventListener('load', () => {
const end = performance.now();
// If difference between start and stop is greater than 500ms, do nothing.
if (end - start > 500) {
return;
}
// First, we need to go to top immediately (hack to prevent jump to desired element).
window.scroll({top: 0, left: 0});
// Then, scroll down to it smoothly.
assert(hashTarget !== null, 'Hash target should be set on DOM loaded.');
scrollToTarget(hashTarget);
});
}
This diff is collapsed.
import * as Velocity from 'velocity-animate';
import setupVelocity from './setupVelocity';
import setupVelocity from './easing/setupVelocity';
import {initializeOnLoadScroll} from './handlers/loadScroll/initializeOnLoadScroll';
import {initializeOnLinkClickScroll} from './handlers/linkClickScroll/initializeOnLinkClickScroll';
interface SmoothScrollOptions {
readonly load?: boolean;
readonly interaction?: boolean;
export interface SmoothScrollOptions {
readonly scrollOnLoad?: boolean;
readonly scrollOnLinkClick?: boolean;
}
const SmoothScroll = (options?: SmoothScrollOptions) => {
setupVelocity(Velocity);
if ( ! (options && options.load === false)) {
handleLoad();
}
if ( ! (options && options.interaction === false)) {
handleInteraction();
}
};
export default SmoothScroll;
/**
* This function scrolls smoothly to an element if there is a hash in the URL.
* E.g. you have `#example` in URL, then it scrolls to element with id `example`.
*
* Note:
* Because of UX, this behaviour is limited only when whole document is loaded in less than 500ms.
* Otherwise, it jumps directly to desired element without smooth scrolling, because too visible jumping through the page would appear.
* Wrapped into class for intuitive API use – `SmoothScroll.enable()`
*/
const handleLoad = () => {
// If no hash, we do not need to run scrolling.
if ( ! window.location.hash) {
return;
}
// If performance is not present, the browser would not scroll smoothly otherwise I guess. So let's skip it completely, it's not worth fallbacking to Date() function.
if (performance === undefined) {
return;
}
const start = performance.now();
/*
* The `load` event has been chosen intentionally as it is the state when everything is ready -
* all styles are loaded and offsets are computed correctly - so the scroll will be computed correctly.
*/
window.addEventListener('load', () => {
const end = performance.now();
// If difference between start and stop is greater than 500ms, do nothing.
if (end - start > 500) {
return;
class SmoothScroll
{
public static enable(options?: SmoothScrollOptions): void
{
setupVelocity(Velocity);
if ( ! (options && options.scrollOnLoad === false)) {
initializeOnLoadScroll();
}
// First, we need to go to top immediately (hack to prevent jump to desired element).
window.scroll({top: 0, left: 0});
// Then, scroll down to it smoothly.
scrollTo(window.location.hash);
});
};
const handleInteraction = () => {
document.addEventListener('DOMContentLoaded', () => {
const items = document.querySelectorAll('a[href^="#"]');
for (const i in items) {
if ( ! (items as object).hasOwnProperty(i)) {
continue;
}
items[i].addEventListener('click', (e) => {
const element = e.currentTarget as HTMLAnchorElement;
if ( ! element.hash) {
return;
}
e.preventDefault();
scrollTo(element.hash);
});
if ( ! (options && options.scrollOnLinkClick === false)) {
initializeOnLinkClickScroll();
}
});
};
const scrollTo = (hash: string) => {
const element = document.getElementById(hash.substring(1));
if (element === null) {
return;
}
}
Velocity.animate(element, 'scroll', {
duration: 1200, // todo: different depending on offset from page top?
easing: 'ease-in-skip-out',
complete: () => window.location.hash = hash,
});
};
export default SmoothScroll;
......@@ -3,13 +3,13 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<title>Visual test of smoothscroll</title>
<title>Integration test</title>
<script src="https://cdn.jsdelivr.net/npm/velocity-animate@1.5/velocity.min.js"></script>
<script src="dist/index.js" type="text/javascript"></script>
<script src="../dist/index.js" type="text/javascript"></script>
<script>
SmoothScroll({
load: true,
interaction: true,
SmoothScroll.enable({
scrollOnLoad: true,
scrollOnLinkClick: true,
});
</script>
<style type="text/css">
......@@ -34,6 +34,8 @@
</style>
</head>
<body>
<!-- @todo: improve this file -->
<div id="nahoru"></div>
<a href="#item1">Item1</a>
<a href="#item2">Item2</a>
......
import * as Velocity from 'velocity-animate';
import {HashTarget} from '../HashTarget';
import {EASE_IN_SKIP_OUT_EASING} from '../easing/setupVelocity';
export function scrollToTarget(hashTarget: HashTarget): void
{
Velocity.animate(hashTarget.getElement(), 'scroll', {
duration: 1200, // todo: different depending on offset from page top?
easing: EASE_IN_SKIP_OUT_EASING,
complete: () => window.location.hash = hashTarget.getHash(),
});
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment