@glitchr/stickyjs 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/package.json +1 -1
- package/src/js/sticky.js +7 -1
- package/src/js/stops.js +49 -3
package/README.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Sticky JS library
|
|
2
2
|
|
|
3
|
+
## The address follows the reading
|
|
4
|
+
|
|
5
|
+
With `replacehash` on (the default), the page's address carries the `#id` of
|
|
6
|
+
the headline being read: `.sticky-headlines[id]`, any `[id]` inside a
|
|
7
|
+
`.sticky-headlines`, or a `.sticky-magnet[id]` (`.sticky-headlines-skip` left
|
|
8
|
+
out). A headline is being read once it has reached where its own link would
|
|
9
|
+
land it - the scroller's `scroll-padding-top` plus the headline's
|
|
10
|
+
`scroll-margin-top` - and until it has scrolled out of sight. At the top, with
|
|
11
|
+
none reached, the address has no hash. The change is a `history.replaceState`:
|
|
12
|
+
it adds nothing to the history.
|
|
13
|
+
|
|
14
|
+
```html
|
|
15
|
+
<section id="menus" class="sticky-headlines" style="scroll-margin-top: 4rem">…</section>
|
|
16
|
+
```
|
|
17
|
+
|
|
3
18
|
## Scroll stops
|
|
4
19
|
|
|
5
20
|
`src/js/stops.js` - places on the page a scroll settles on, once it has come
|
package/package.json
CHANGED
package/src/js/sticky.js
CHANGED
|
@@ -1173,7 +1173,13 @@ $.fn.serializeObject = function () {
|
|
|
1173
1173
|
|
|
1174
1174
|
var elAll = $(ids).filter(function() {
|
|
1175
1175
|
if(this === $(Settings.identifier)) return false;
|
|
1176
|
-
|
|
1176
|
+
// Reached where an anchor jump would land it: the scroller's
|
|
1177
|
+
// scroll-padding-top plus the headline's own scroll-margin-top
|
|
1178
|
+
// (a section that lands under a fixed header, a band lower
|
|
1179
|
+
// than the rest). Without the margin, a headline landed on
|
|
1180
|
+
// by its own link was not the current one yet.
|
|
1181
|
+
var margin = parseFloat(getComputedStyle(this).scrollMarginTop) || 0;
|
|
1182
|
+
return this.getBoundingClientRect().top - margin < scrollPaddingTop;
|
|
1177
1183
|
}).toArray().sort(function (el1, el2) {
|
|
1178
1184
|
return el1.offsetTop > el2.offsetTop ? -1
|
|
1179
1185
|
: (el1.offsetTop < el2.offsetTop ? 1 : 0);
|
package/src/js/stops.js
CHANGED
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
//
|
|
20
20
|
// A band never reaches past halfway to the neighbouring stop, so every stop
|
|
21
21
|
// stays reachable however the bands are set. Two stops at the same place are
|
|
22
|
-
// one stop.
|
|
22
|
+
// one stop. A stop past the end of the scroll (a footer shorter than the
|
|
23
|
+
// screen) is at the end of the scroll.
|
|
23
24
|
//
|
|
24
25
|
// Nothing happens during the scroll. Acting at the first movement fights
|
|
25
26
|
// trackpad momentum, which keeps pushing during and after the glide; and the
|
|
@@ -30,6 +31,11 @@
|
|
|
30
31
|
// undo, a movement the reader started. With prefers-reduced-motion the glide
|
|
31
32
|
// is a jump.
|
|
32
33
|
//
|
|
34
|
+
// Nor does a scroll the browser makes to bring a focused field into view: an
|
|
35
|
+
// invalid form submission revealing its first empty field, or Tab. Heading up
|
|
36
|
+
// from the text, that scroll was taken for the reader's and carried the page
|
|
37
|
+
// on to the stop above - the form gone from the screen it had just asked for.
|
|
38
|
+
//
|
|
33
39
|
// Progress: with two stops or more, the scroller (the <html> element for the
|
|
34
40
|
// page) carries three custom properties - --sticky-stop, the index of the stop
|
|
35
41
|
// above the scroll; --sticky-progress, 0 at that stop and 1 at the next; and
|
|
@@ -68,6 +74,12 @@
|
|
|
68
74
|
// The reader taking over ends a glide; the browser has already stopped it.
|
|
69
75
|
var INTERRUPT = ['wheel', 'touchstart', 'keydown', 'mousedown'];
|
|
70
76
|
|
|
77
|
+
// ms after a focus change in which a scroll starting is the browser
|
|
78
|
+
// revealing the focused field, not the reader.
|
|
79
|
+
var REVEAL = 250;
|
|
80
|
+
|
|
81
|
+
function now() { return window.performance ? performance.now() : Date.now(); }
|
|
82
|
+
|
|
71
83
|
function extend(target) {
|
|
72
84
|
for (var i = 1; i < arguments.length; i++) {
|
|
73
85
|
var source = arguments[i];
|
|
@@ -114,17 +126,25 @@
|
|
|
114
126
|
this.timer = null;
|
|
115
127
|
this.glideTimer = null;
|
|
116
128
|
this.frame = null;
|
|
129
|
+
this.focusedAt = -Infinity;
|
|
130
|
+
this.revealing = false;
|
|
117
131
|
|
|
118
132
|
this.onScroll = this.onScroll.bind(this);
|
|
119
133
|
this.onInterrupt = this.onInterrupt.bind(this);
|
|
134
|
+
this.onFocus = this.onFocus.bind(this);
|
|
120
135
|
this.onResize = this.onResize.bind(this);
|
|
121
136
|
this.settle = this.settle.bind(this);
|
|
122
137
|
this.tick = this.tick.bind(this);
|
|
123
138
|
|
|
139
|
+
// focusin bubbles, so the scroller hears its own fields; the window
|
|
140
|
+
// does not, so the page's are heard on the document.
|
|
141
|
+
this.focusTarget = this.isWindow ? document : this.scroller;
|
|
142
|
+
|
|
124
143
|
this.scroller.addEventListener('scroll', this.onScroll, { passive: true });
|
|
125
144
|
for (var i = 0; i < INTERRUPT.length; i++) {
|
|
126
145
|
this.scroller.addEventListener(INTERRUPT[i], this.onInterrupt, { passive: true });
|
|
127
146
|
}
|
|
147
|
+
this.focusTarget.addEventListener('focusin', this.onFocus, true);
|
|
128
148
|
window.addEventListener('resize', this.onResize);
|
|
129
149
|
|
|
130
150
|
this.refresh();
|
|
@@ -135,6 +155,7 @@
|
|
|
135
155
|
for (var i = 0; i < INTERRUPT.length; i++) {
|
|
136
156
|
this.scroller.removeEventListener(INTERRUPT[i], this.onInterrupt);
|
|
137
157
|
}
|
|
158
|
+
this.focusTarget.removeEventListener('focusin', this.onFocus, true);
|
|
138
159
|
window.removeEventListener('resize', this.onResize);
|
|
139
160
|
clearTimeout(this.timer);
|
|
140
161
|
clearTimeout(this.glideTimer);
|
|
@@ -158,6 +179,13 @@
|
|
|
158
179
|
return this.isWindow ? (window.scrollY || window.pageYOffset || 0) : this.scroller.scrollTop;
|
|
159
180
|
};
|
|
160
181
|
|
|
182
|
+
/** The furthest the scroller can scroll. */
|
|
183
|
+
Stops.prototype.max = function () {
|
|
184
|
+
return Math.max(0, this.isWindow
|
|
185
|
+
? document.documentElement.scrollHeight - window.innerHeight
|
|
186
|
+
: this.scroller.scrollHeight - this.scroller.clientHeight);
|
|
187
|
+
};
|
|
188
|
+
|
|
161
189
|
/** The scroller's scroll-padding-top: where a stop should rest below its edge. */
|
|
162
190
|
Stops.prototype.padding = function () {
|
|
163
191
|
var value = parseFloat(getComputedStyle(this.root).scrollPaddingTop);
|
|
@@ -173,6 +201,7 @@
|
|
|
173
201
|
Stops.prototype.measure = function () {
|
|
174
202
|
var options = this.options;
|
|
175
203
|
var elements = this.root.querySelectorAll(options.selector);
|
|
204
|
+
var max = this.max();
|
|
176
205
|
var stops = [];
|
|
177
206
|
var i;
|
|
178
207
|
|
|
@@ -181,7 +210,11 @@
|
|
|
181
210
|
if (!el.getClientRects().length) continue; // display: none
|
|
182
211
|
stops.push({
|
|
183
212
|
element: el,
|
|
184
|
-
top
|
|
213
|
+
// A stop the scroll cannot bring to the top (a footer shorter
|
|
214
|
+
// than the screen) rests where the scroll ends. Taken as out
|
|
215
|
+
// of reach, it made every scroll up from the very bottom -
|
|
216
|
+
// Safari's bounce included - glide away to the stop above.
|
|
217
|
+
top: Math.min(Math.round(this.position(el)), max),
|
|
185
218
|
bandUp: Math.max(0, toPixels(el.dataset.stickyBandUp, el, options.bandUp)),
|
|
186
219
|
bandDown: Math.max(0, toPixels(el.dataset.stickyBandDown, el, options.bandDown))
|
|
187
220
|
});
|
|
@@ -230,6 +263,12 @@
|
|
|
230
263
|
this.gliding = false;
|
|
231
264
|
return;
|
|
232
265
|
}
|
|
266
|
+
// The browser bringing a focused field into view: it rests where the
|
|
267
|
+
// field is, wherever that falls between the stops.
|
|
268
|
+
if (this.revealing) {
|
|
269
|
+
this.revealing = false;
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
233
272
|
if (this.paused || api.paused) return;
|
|
234
273
|
|
|
235
274
|
var stops = this.measure();
|
|
@@ -285,7 +324,10 @@
|
|
|
285
324
|
clearTimeout(this.glideTimer);
|
|
286
325
|
|
|
287
326
|
// The first event after a rest starts a new gesture.
|
|
288
|
-
if (this.timer === null && !this.gliding)
|
|
327
|
+
if (this.timer === null && !this.gliding) {
|
|
328
|
+
this.start = this.last;
|
|
329
|
+
this.revealing = now() - this.focusedAt < REVEAL;
|
|
330
|
+
}
|
|
289
331
|
this.last = y;
|
|
290
332
|
|
|
291
333
|
// Which way the READER is going, not our own glide.
|
|
@@ -301,6 +343,10 @@
|
|
|
301
343
|
this.gliding = false;
|
|
302
344
|
};
|
|
303
345
|
|
|
346
|
+
Stops.prototype.onFocus = function () {
|
|
347
|
+
this.focusedAt = now();
|
|
348
|
+
};
|
|
349
|
+
|
|
304
350
|
Stops.prototype.onResize = function () {
|
|
305
351
|
this.refresh();
|
|
306
352
|
};
|