Skip to content

Commit 11f86af

Browse files
authored
Changelog: 🚀
### Added * New `handleScroll` option allows customizing scrolling behavior. ### Changed * Animation logic is separated from scroll calculation logic. This allows skip importing animation dependencies and reduces bundle sizes when you don't need the built in animation feature. ### What this means Take control over how the target is scrolled into view. This function is called for each parent node that need scrolling. `scrollLeft` and `scrollTop` are destination coordinates. The from coordinates you'll have to get yourself if you want to animate the transition using a different library. When using this option you likely don't need the built in animation feature. To cut down on filesize you can do the following adjustment if you are using a recent version of webpack or rollbar (and use ES6 imports): ```diff -import scrollIntoViewIfNeeded from 'scroll-into-view-if-needed' +import maybeScrollIntoView from 'scroll-into-view-if-needed/dist/calculate' -scrollIntoViewIfNeeded(node) +maybeScrollIntoView(node, {handleScroll: (parent, {scrollLeft, scrollTop}, config) => { + // The following is actually the default implementation + // if this is all you need you can skip passing this option + parent.scrollLeft = scrollLeft + parent.scrollTop = scrollTop +}}) ```
1 parent 18147e1 commit 11f86af

File tree

4 files changed

+199
-111
lines changed

4 files changed

+199
-111
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ and this project adheres to
88

99
## [Unreleased]
1010

11+
## [1.4.0] - 2017-11-17
12+
13+
### Added
14+
15+
* New `handleScroll` option allows customizing scrolling behavior.
16+
17+
### Changed
18+
19+
* Animation logic is separated from scroll calculation logic. This allows skip
20+
importing animation dependencies and reduces bundle sizes when you don't need
21+
the built in animation feature.
22+
1123
## [1.3.0] - 2017-11-12
1224

1325
### Added

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,34 @@ Change the easing mechanism. This option takes effect when `duration` is set. In
7777
[bezier easing](https://www.npmjs.com/package/bezier-easing) similar to CSS
7878
[`cubic-bezier()`](<https://developer.mozilla.org/en-US/docs/Web/CSS/single-transition-timing-function#cubic-bezier()>).
7979

80+
#### handleScroll(parent, {scrollLeft, scrollTop}, options)
81+
82+
> Introduced in v1.4.0
83+
84+
Type: `function`
85+
86+
Take control over how the target is scrolled into view. This function is called
87+
for each parent node that need scrolling. `scrollLeft` and `scrollTop` are
88+
destination coordinates. The from coordinates you'll have to get yourself if you
89+
want to animate the transition using a different library.
90+
91+
When using this option you likely don't need the built in animation feature. To
92+
cut down on filesize you can do the following adjustment if you are using a
93+
recent version of webpack or rollbar (and use ES6 imports):
94+
95+
```diff
96+
-import scrollIntoViewIfNeeded from 'scroll-into-view-if-needed'
97+
+import maybeScrollIntoView from 'scroll-into-view-if-needed/dist/calculate'
98+
99+
-scrollIntoViewIfNeeded(node)
100+
+maybeScrollIntoView(node, {handleScroll: (parent, {scrollLeft, scrollTop}, config) => {
101+
+ // The following is actually the default implementation
102+
+ // if this is all you need you can skip passing this option
103+
+ parent.scrollLeft = scrollLeft
104+
+ parent.scrollTop = scrollTop
105+
+}})
106+
```
107+
80108
#### offset
81109

82110
Type: `Object`

src/calculate.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
export interface Offset {
2+
top?: number
3+
right?: number
4+
bottom?: number
5+
left?: number
6+
}
7+
8+
export interface ScrollCoordinates {
9+
scrollLeft: number
10+
scrollTop: number
11+
}
12+
export type handleScrollCallback = (
13+
parent: HTMLElement,
14+
coordinates: ScrollCoordinates,
15+
config: Options
16+
) => void
17+
18+
export interface Options {
19+
// A handler that handles scrolling the view to the new coordinates
20+
handleScroll?: handleScrollCallback
21+
boundary?: Element
22+
centerIfNeeded?: boolean
23+
offset?: Offset
24+
}
25+
26+
const handleScroll: handleScrollCallback = (
27+
parent: HTMLElement,
28+
{ scrollLeft, scrollTop }
29+
) => {
30+
parent.scrollLeft = scrollLeft
31+
parent.scrollTop = scrollTop
32+
}
33+
34+
export default function calculate(target: Element, options: Options) {
35+
if (!target || !(target instanceof HTMLElement))
36+
throw new Error('Element is required in scrollIntoViewIfNeeded')
37+
38+
const config = { handleScroll, ...options }
39+
const defaultOffset = { top: 0, right: 0, bottom: 0, left: 0 }
40+
config.offset = config.offset
41+
? { ...defaultOffset, ...config.offset }
42+
: defaultOffset
43+
44+
function withinBounds(value, min, max, extent) {
45+
if (
46+
config.centerIfNeeded === false ||
47+
(max <= value + extent && value <= min + extent)
48+
) {
49+
return Math.min(max, Math.max(min, value))
50+
} else {
51+
return (min + max) / 2
52+
}
53+
}
54+
55+
const { offset } = config
56+
const offsetTop = offset.top
57+
const offsetLeft = offset.left
58+
const offsetBottom = offset.bottom
59+
const offsetRight = offset.right
60+
61+
function makeArea(left, top, width, height) {
62+
return {
63+
left: left + offsetLeft,
64+
top: top + offsetTop,
65+
width: width,
66+
height: height,
67+
right: left + offsetLeft + width + offsetRight,
68+
bottom: top + offsetTop + height + offsetBottom,
69+
translate: function(x, y) {
70+
return makeArea(
71+
x + left + offsetLeft,
72+
y + top + offsetTop,
73+
width,
74+
height
75+
)
76+
},
77+
relativeFromTo: function(lhs, rhs) {
78+
let newLeft = left + offsetLeft,
79+
newTop = top + offsetTop
80+
lhs = lhs.offsetParent
81+
rhs = rhs.offsetParent
82+
if (lhs === rhs) {
83+
return area
84+
}
85+
for (; lhs; lhs = lhs.offsetParent) {
86+
newLeft += lhs.offsetLeft + lhs.clientLeft
87+
newTop += lhs.offsetTop + lhs.clientTop
88+
}
89+
for (; rhs; rhs = rhs.offsetParent) {
90+
newLeft -= rhs.offsetLeft + rhs.clientLeft
91+
newTop -= rhs.offsetTop + rhs.clientTop
92+
}
93+
return makeArea(newLeft, newTop, width, height)
94+
},
95+
}
96+
}
97+
98+
let parent,
99+
area = makeArea(
100+
target.offsetLeft,
101+
target.offsetTop,
102+
target.offsetWidth,
103+
target.offsetHeight
104+
)
105+
while (
106+
(parent = target.parentNode) instanceof HTMLElement &&
107+
target !== config.boundary
108+
) {
109+
const clientLeft = parent.offsetLeft + parent.clientLeft
110+
const clientTop = parent.offsetTop + parent.clientTop
111+
112+
// Make area relative to parent's client area.
113+
area = area
114+
.relativeFromTo(target, parent)
115+
.translate(-clientLeft, -clientTop)
116+
117+
const scrollLeft = withinBounds(
118+
parent.scrollLeft,
119+
area.right - parent.clientWidth,
120+
area.left,
121+
parent.clientWidth
122+
)
123+
const scrollTop = withinBounds(
124+
parent.scrollTop,
125+
area.bottom - parent.clientHeight,
126+
area.top,
127+
parent.clientHeight
128+
)
129+
// Pass the new coordinates to the handleScroll callback
130+
config.handleScroll(parent, { scrollLeft, scrollTop }, config)
131+
132+
// Determine actual scroll amount by reading back scroll properties.
133+
area = area.translate(
134+
clientLeft - parent.scrollLeft,
135+
clientTop - parent.scrollTop
136+
)
137+
target = parent
138+
}
139+
}

src/index.ts

Lines changed: 20 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import animate from 'amator'
2+
import calculate, { Options as CalculateOptions } from './calculate'
23

34
// Legacy
45
export interface AnimateOptions {
@@ -21,14 +22,27 @@ export interface Offset {
2122
left?: number
2223
}
2324

24-
export interface Options {
25-
boundary?: Element
26-
centerIfNeeded?: boolean
25+
const handleScroll = (parent, { scrollLeft, scrollTop }, config) => {
26+
if (config.duration) {
27+
animate(
28+
parent,
29+
{
30+
scrollLeft: scrollLeft,
31+
scrollTop: scrollTop,
32+
},
33+
{ duration: config.duration, easing: config.easing }
34+
)
35+
} else {
36+
parent.scrollLeft = scrollLeft
37+
parent.scrollTop = scrollTop
38+
}
39+
}
40+
41+
export interface Options extends CalculateOptions {
2742
// Setting a duration will enable animations
2843
duration?: number
2944
// Easing only take effect if a duration is set
3045
easing?: 'ease' | 'easeIn' | 'easeOut' | 'easeInOut' | 'linear'
31-
offset?: Offset
3246
}
3347

3448
function isBoolean(options: boolean | Options): options is boolean {
@@ -45,7 +59,7 @@ export default function scrollIntoViewIfNeeded(
4559
if (!target || !(target instanceof HTMLElement))
4660
throw new Error('Element is required in scrollIntoViewIfNeeded')
4761

48-
let config: Options = { centerIfNeeded: false }
62+
let config: Options = { centerIfNeeded: false, handleScroll }
4963

5064
if (isBoolean(options)) {
5165
config.centerIfNeeded = options
@@ -80,110 +94,5 @@ export default function scrollIntoViewIfNeeded(
8094
config.offset.left = offsetOptions.offsetLeft
8195
}
8296

83-
function withinBounds(value, min, max, extent) {
84-
if (
85-
config.centerIfNeeded === false ||
86-
(max <= value + extent && value <= min + extent)
87-
) {
88-
return Math.min(max, Math.max(min, value))
89-
} else {
90-
return (min + max) / 2
91-
}
92-
}
93-
94-
const { offset } = config
95-
const offsetTop = offset.top
96-
const offsetLeft = offset.left
97-
const offsetBottom = offset.bottom
98-
const offsetRight = offset.right
99-
100-
function makeArea(left, top, width, height) {
101-
return {
102-
left: left + offsetLeft,
103-
top: top + offsetTop,
104-
width: width,
105-
height: height,
106-
right: left + offsetLeft + width + offsetRight,
107-
bottom: top + offsetTop + height + offsetBottom,
108-
translate: function(x, y) {
109-
return makeArea(
110-
x + left + offsetLeft,
111-
y + top + offsetTop,
112-
width,
113-
height
114-
)
115-
},
116-
relativeFromTo: function(lhs, rhs) {
117-
let newLeft = left + offsetLeft,
118-
newTop = top + offsetTop
119-
lhs = lhs.offsetParent
120-
rhs = rhs.offsetParent
121-
if (lhs === rhs) {
122-
return area
123-
}
124-
for (; lhs; lhs = lhs.offsetParent) {
125-
newLeft += lhs.offsetLeft + lhs.clientLeft
126-
newTop += lhs.offsetTop + lhs.clientTop
127-
}
128-
for (; rhs; rhs = rhs.offsetParent) {
129-
newLeft -= rhs.offsetLeft + rhs.clientLeft
130-
newTop -= rhs.offsetTop + rhs.clientTop
131-
}
132-
return makeArea(newLeft, newTop, width, height)
133-
},
134-
}
135-
}
136-
137-
let parent,
138-
area = makeArea(
139-
target.offsetLeft,
140-
target.offsetTop,
141-
target.offsetWidth,
142-
target.offsetHeight
143-
)
144-
while (
145-
(parent = target.parentNode) instanceof HTMLElement &&
146-
target !== config.boundary
147-
) {
148-
const clientLeft = parent.offsetLeft + parent.clientLeft
149-
const clientTop = parent.offsetTop + parent.clientTop
150-
151-
// Make area relative to parent's client area.
152-
area = area
153-
.relativeFromTo(target, parent)
154-
.translate(-clientLeft, -clientTop)
155-
156-
const scrollLeft = withinBounds(
157-
parent.scrollLeft,
158-
area.right - parent.clientWidth,
159-
area.left,
160-
parent.clientWidth
161-
)
162-
const scrollTop = withinBounds(
163-
parent.scrollTop,
164-
area.bottom - parent.clientHeight,
165-
area.top,
166-
parent.clientHeight
167-
)
168-
if (config.duration) {
169-
animate(
170-
parent,
171-
{
172-
scrollLeft: scrollLeft,
173-
scrollTop: scrollTop,
174-
},
175-
{ duration: config.duration, easing: config.easing }
176-
)
177-
} else {
178-
parent.scrollLeft = scrollLeft
179-
parent.scrollTop = scrollTop
180-
}
181-
182-
// Determine actual scroll amount by reading back scroll properties.
183-
area = area.translate(
184-
clientLeft - parent.scrollLeft,
185-
clientTop - parent.scrollTop
186-
)
187-
target = parent
188-
}
97+
return calculate(target, config)
18998
}

0 commit comments

Comments
 (0)