Skip to content

Commit 4aba853

Browse files
committed
Fix fallback, add "clamp" option
1 parent 9c915ff commit 4aba853

File tree

4 files changed

+108
-69
lines changed

4 files changed

+108
-69
lines changed

CHANGELOG.rst

+7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
Changelog
22
#########
33

4+
2.2.0
5+
*****
6+
7+
- added the "clamp" option
8+
- fixed fallback to the closest section if none are in view
9+
10+
411
2.1.2
512
*****
613

README.rst

+4
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,10 @@ Option Default Description
133133
intersected by the bottom of the view, forces the last
134134
section to be active regardless of other conditions.
135135
---------------------- ------------------ --------------------------------------------------------
136+
``clamp`` ``false`` When enabled, the space between two sections is
137+
considered a part of the first section. Useful when
138+
there are large gaps between sections.
139+
---------------------- ------------------ --------------------------------------------------------
136140
``throttle`` ``true`` When enabled, the callback is invoked only when the
137141
active section changes
138142

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "jquery-scrollwatch",
3-
"version": "2.1.2",
3+
"version": "2.2.0",
44
"license": "MIT",
55
"description": "jQuery plugin for determining active sections on the page based on scrolling",
66
"keywords": ["jQuery", "scrollwatch", "scroll", "navigation", "active", "class"],

src/scrollwatch.js

+96-68
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,21 @@
33
var Shira;
44
(function (Shira, $) {
55
(function (ScrollWatch) {
6+
/**
7+
* Iterate an array (helper)
8+
*
9+
* @param {Object} thisArg
10+
* @param {Array} arr
11+
* @param {Function} callback
12+
*/
13+
function foreach(thisArg, arr, callback) {
14+
for (var i = 0; i < arr.length; ++i) {
15+
if (callback.call(thisArg, i, arr[i]) === false) {
16+
break;
17+
}
18+
}
19+
}
20+
621
/**
722
* @constructor
823
*
@@ -53,6 +68,7 @@ var Shira;
5368
viewMarginBottom: 0,
5469
stickyOffsetTop: 5,
5570
stickyOffsetBottom: 5,
71+
clamp: false,
5672
focusRatio: 0.38196601125010515,
5773
focusOffset: 0,
5874
debugFocusLine: false
@@ -99,7 +115,7 @@ var Shira;
99115
while (!scrollable) {
100116
elem = elem.offsetParent;
101117

102-
if (elem && 1 === elem.nodeType && 'BODY' !== elem.tagName && 'HTML' !== elem.tagName) {
118+
if (elem && elem.nodeType === 1 && 'BODY' !== elem.tagName && 'HTML' !== elem.tagName) {
103119
var overflowY = $(elem).css('overflow-y');
104120
scrollable = 'auto' === overflowY || 'scroll' === overflowY;
105121
} else {
@@ -140,23 +156,32 @@ var Shira;
140156
updateSectionBoundaries: function () {
141157
this.sectionBoundaries = [];
142158

143-
for (var i = 0; i < this.sections.length; ++i) {
144-
var top = this.getElementY(this.sections[i], this.scroller);
145-
var bottom = top + this.sections[i].offsetHeight;
146-
this.sectionBoundaries.push([top, bottom]);
147-
}
159+
foreach(this, this.sections, function (i, section) {
160+
var top = this.getElementY(section, this.scroller);
161+
var bottom = top + section.offsetHeight;
162+
this.sectionBoundaries.push({index: i, top: top, bottom: bottom});
163+
});
164+
148165
this.sectionBoundaries.sort(this.sortSectionBoundaries);
166+
167+
if (this.options.clamp) {
168+
foreach (this, this.sectionBoundaries, function (i, boundary) {
169+
if (i < this.sectionBoundaries.length - 1) {
170+
boundary.bottom = this.sectionBoundaries[i + 1].top - 1;
171+
}
172+
});
173+
}
149174
},
150175

151176
/**
152177
* Sort calculated section boundaries
153178
*
154-
* @param {Array} a
155-
* @param {Array} b
179+
* @param {Object} a
180+
* @param {Object} b
156181
* @returns {Number}
157182
*/
158183
sortSectionBoundaries: function (a, b) {
159-
return a[0] - b[0];
184+
return a.top - b.top;
160185
},
161186

162187
/**
@@ -181,10 +206,10 @@ var Shira;
181206
var top = $(this.scroller).scrollTop();
182207
var bottom = top + this.scrollerVisibleHeight;
183208

184-
if (0 !== this.options.viewMarginTop) {
209+
if (this.options.viewMarginTop !== 0) {
185210
top += this.options.viewMarginTop;
186211
}
187-
if (0 !== this.options.viewMarginBottom) {
212+
if (this.options.viewMarginBottom !== 0) {
188213
bottom = Math.max(top + 1, bottom - this.options.viewMarginBottom);
189214
}
190215

@@ -201,63 +226,68 @@ var Shira;
201226
* @returns {Array}
202227
*/
203228
determineFocusCandidates: function (view) {
204-
var focusCandidates = [], forcedIndex = null;
229+
var that = this, focusCandidates = [], forcedBoundary = null;
205230

206-
// see if a certain section must be forced
231+
// see if a certain boundary must be forced
207232
if (this.scrollerFullHeight - view.bottom < this.options.stickyOffsetBottom) {
208-
// always choose last section if the view is near the end
209-
forcedIndex = this.sectionBoundaries.length - 1;
233+
// always choose last boundary if the view is near the end
234+
forcedBoundary = this.sectionBoundaries[this.sectionBoundaries.length - 1];
210235
} else if (view.top - this.options.viewMarginTop < this.options.stickyOffsetTop) {
211-
// always choose first section if the view is near the beginning
212-
forcedIndex = 0;
236+
// always choose first boundary if the view is near the beginning
237+
forcedBoundary = this.sectionBoundaries[0];
213238
}
214239

215240
// determine candidates
216-
if (null !== forcedIndex) {
241+
if (forcedBoundary !== null) {
217242
// forced
218243
focusCandidates.push({
219-
index: forcedIndex,
244+
index: forcedBoundary.index,
220245
intersection: this.getIntersection(
221246
view.top,
222247
view.bottom,
223-
this.sectionBoundaries[forcedIndex][0],
224-
this.sectionBoundaries[forcedIndex][1]
248+
forcedBoundary.top,
249+
forcedBoundary.bottom
225250
),
226-
section: this.sections[forcedIndex]
251+
section: this.sections[forcedBoundary.index]
227252
});
228253
} else {
229254
// find intersecting sections
230-
for (var i = 0; i < this.sectionBoundaries.length; ++i) {
231-
var intersection = this.getIntersection(
232-
view.top,
233-
view.bottom,
234-
this.sectionBoundaries[i][0],
235-
this.sectionBoundaries[i][1]
236-
);
255+
foreach(this, this.sectionBoundaries, function (i, boundary) {
256+
var intersection = that.getIntersection(view.top, view.bottom, boundary.top, boundary.bottom);
237257

238-
if (null !== intersection) {
258+
if (intersection !== null) {
239259
focusCandidates.push({
240-
index: i,
260+
index: boundary.index,
241261
intersection: intersection,
242-
section: this.sections[i]
262+
section: that.sections[boundary.index]
243263
});
244264
}
245-
}
265+
});
246266

247-
// use section closest to the top of the view if no intersection was found
248-
if (0 === focusCandidates.length) {
249-
var sectionClosest = null, sectionOffsetTop;
250-
for (i = 0; i < this.sectionBoundaries.length; ++i) {
251-
sectionOffsetTop = Math.abs(this.sectionBoundaries[i][0] - view.top);
252-
if (null === sectionClosest || sectionClosest[1] > sectionOffsetTop) {
253-
sectionClosest = i;
267+
// find the closest boundary above if there are no intersections
268+
if (focusCandidates.length === 0) {
269+
var closestBoundary = null;
270+
271+
foreach(this, this.sectionBoundaries, function (_, boundary) {
272+
if (
273+
boundary.bottom < view.top
274+
&& (
275+
closestBoundary === null
276+
|| boundary.bottom > closestBoundary.bottom
277+
)
278+
) {
279+
closestBoundary = boundary;
254280
}
281+
});
282+
283+
if (closestBoundary === null) {
284+
closestBoundary = this.sectionBoundaries[0];
255285
}
256286

257287
focusCandidates.push({
258-
index: sectionClosest,
288+
index: closestBoundary.index,
259289
intersection: null,
260-
section: this.sections[sectionClosest]
290+
section: this.sections[closestBoundary.index]
261291
});
262292
}
263293
}
@@ -276,7 +306,7 @@ var Shira;
276306
var that = this;
277307
var chosenCandidate = null;
278308

279-
if (1 === focusCandidates.length) {
309+
if (focusCandidates.length === 1) {
280310
// single candidate available
281311
chosenCandidate = focusCandidates[0];
282312
} else {
@@ -296,37 +326,36 @@ var Shira;
296326

297327
// choose using intersection or distance from the focus line
298328
case 'focus-line':
299-
var i;
300329
var viewFocusLineOffset = view.top + (view.bottom - view.top) * this.options.focusRatio + this.options.focusOffset;
301330

302331
if (this.options.debugFocusLine) {
303332
this.updateDebugFocusLine(Math.round(viewFocusLineOffset));
304333
}
305334

306335
// find direct intersection with the focus line
307-
for (i = 0; i < focusCandidates.length; ++i) {
308-
if (focusCandidates[i].intersection[0] <= viewFocusLineOffset && focusCandidates[i].intersection[1] >= viewFocusLineOffset) {
309-
chosenCandidate = focusCandidates[i];
310-
break;
336+
foreach (this, focusCandidates, function (_, candidate) {
337+
if (candidate.intersection[0] <= viewFocusLineOffset && candidate.intersection[1] >= viewFocusLineOffset) {
338+
chosenCandidate = candidate;
339+
return false;
311340
}
312-
}
341+
});
313342

314343
// find nearest candidate if no direct intersection exists
315-
if (null === chosenCandidate) {
316-
for (i = 0; i < focusCandidates.length; ++i) {
317-
focusCandidates[i].focusRatioOffsetDistance = Math.min(
318-
Math.abs(focusCandidates[i].intersection[0] - viewFocusLineOffset),
319-
Math.abs(focusCandidates[i].intersection[1] - viewFocusLineOffset)
344+
if (chosenCandidate === null) {
345+
foreach (this, focusCandidates, function (_, candidate) {
346+
candidate.focusRatioOffsetDistance = Math.min(
347+
Math.abs(candidate.intersection[0] - viewFocusLineOffset),
348+
Math.abs(candidate.intersection[1] - viewFocusLineOffset)
320349
);
321-
}
350+
});
322351
focusCandidates.sort(this.sortFocusCandidatesByDistanceToFocusRatioOffset);
323352
chosenCandidate = focusCandidates[0];
324353
}
325354
break;
326355

327356
// use custom resolver
328357
case 'custom':
329-
if (null === this.options.resolver) {
358+
if (this.options.resolver === null) {
330359
throw new Error('No resolver has been set');
331360
}
332361
chosenCandidate = this.options.resolver(focusCandidates, view, this);
@@ -352,14 +381,14 @@ var Shira;
352381
updateDebugFocusLine: function (focusLineOffset) {
353382
var that = this;
354383

355-
if (null === this.debugFocusLine) {
384+
if (this.debugFocusLine === null) {
356385
this.debugFocusLine = $('<div class="scrollwatch-debug-focus-line" style="position:absolute;left:0;top:0;width:100%;border-bottom:1px solid white;outline:1px solid black;z-index:10000;box-shadow: 0 0 5px black;"></div>')
357386
.appendTo(window === this.scroller ? document.body : this.scroller);
358387
}
359388

360389
this.debugFocusLine.css('top', focusLineOffset + 'px');
361390

362-
if (null !== this.debugFocusLineTimeout) {
391+
if (this.debugFocusLineTimeout !== null) {
363392
clearTimeout(this.debugFocusLineTimeout);
364393
}
365394

@@ -474,17 +503,16 @@ var Shira;
474503
* @param {Array} newActiveIndexes
475504
*/
476505
handleFocusChange: function (newActiveIndexes) {
477-
var i;
478506
var toDeactivate = $(this.currentActiveIndexes).not(newActiveIndexes).get();
479507
var toActivate = $(newActiveIndexes).not(this.currentActiveIndexes).get();
480508

481-
for (i = 0; i < toDeactivate.length; ++i) {
482-
$(this.items[toDeactivate[i]]).removeClass(this.activeClass);
483-
}
509+
foreach (this, toDeactivate, function (_, itemIndex) {
510+
$(this.items[itemIndex]).removeClass(this.activeClass);
511+
});
484512

485-
for (i = 0; i < toActivate.length; ++i) {
486-
$(this.items[toActivate[i]]).addClass(this.activeClass);
487-
}
513+
foreach (this, toActivate, function (_, itemIndex) {
514+
$(this.items[itemIndex]).addClass(this.activeClass);
515+
});
488516

489517
this.currentActiveIndexes = newActiveIndexes;
490518
},
@@ -501,9 +529,9 @@ var Shira;
501529
var newActiveIndexes = [];
502530

503531
if (focus instanceof Array) {
504-
for (var i = 0; i < focus.length; ++i) {
505-
newActiveIndexes.push(focus[i].index);
506-
}
532+
foreach (this, focus, function (_, focus) {
533+
newActiveIndexes.push(focus.index);
534+
});
507535
} else {
508536
newActiveIndexes.push(focus.index);
509537
}

0 commit comments

Comments
 (0)