@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glitchr/stickyjs",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Sticky scroll library",
5
5
  "main": "src/index.js",
6
6
  "repository": {
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
- return this.getBoundingClientRect().top < scrollPaddingTop;
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: Math.round(this.position(el)),
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) this.start = this.last;
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
  };