Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/webroot/rsrc/externals/javelin/lib/Scrollbar.js
12242 views
1
/**
2
* @provides javelin-scrollbar
3
* @requires javelin-install
4
* javelin-dom
5
* javelin-stratcom
6
* javelin-vector
7
* @javelin
8
*/
9
10
/**
11
* Provides an aesthetic scrollbar.
12
*
13
* This shoves an element's scrollbar under a hidden overflow and draws a
14
* pretty looking fake one in its place. This makes complex UIs with multiple
15
* independently scrollable panels less hideous by (a) making the scrollbar
16
* itself prettier and (b) reclaiming the space occupied by the scrollbar.
17
*
18
* Note that on OSX the heavy scrollbars are normally drawn only if you have
19
* a mouse connected. OSX uses more aesthetic touchpad scrollbars normally,
20
* which these scrollbars emulate.
21
*
22
* This class was initially adapted from "Trackpad Scroll Emulator", by
23
* Jonathan Nicol. See <https://github.com/jnicol/trackpad-scroll-emulator>.
24
*/
25
JX.install('Scrollbar', {
26
27
construct: function(frame) {
28
this._frame = frame;
29
30
JX.DOM.listen(frame, 'load', null, JX.bind(this, this._onload));
31
this._onload();
32
33
// Before doing anything, check if the scrollbar control has a measurable
34
// width. If it doesn't, we're already in an environment with an aesthetic
35
// scrollbar (like Safari on OSX with no mouse connected, or an iPhone)
36
// and we don't need to do anything.
37
if (JX.Scrollbar.getScrollbarControlWidth() === 0) {
38
return;
39
}
40
41
// Wrap the frame content in a bunch of nodes. The frame itself stays on
42
// the outside so that any positioning information the node had isn't
43
// disrupted.
44
45
// We put a "viewport" node inside of it, which is what actually scrolls.
46
// This is the node that gets a scrollbar, but we make the viewport very
47
// slightly too wide for the frame. That hides the scrollbar underneath
48
// the edge of the frame.
49
50
// We put a "content" node inside of the viewport. This allows us to
51
// measure the content height so we can resize and offset the scrollbar
52
// handle properly.
53
54
// We move all the actual frame content into the "content" node. So it
55
// ends up wrapped by the "content" node, then by the "viewport" node,
56
// and finally by the original "frame" node.
57
58
JX.DOM.alterClass(frame, 'jx-scrollbar-frame', true);
59
60
var content = JX.$N('div', {className: 'jx-scrollbar-content'});
61
while (frame.firstChild) {
62
JX.DOM.appendContent(content, frame.firstChild);
63
}
64
65
var viewport = JX.$N('div', {className: 'jx-scrollbar-viewport'}, content);
66
JX.DOM.appendContent(frame, viewport);
67
68
this._viewport = viewport;
69
this._content = content;
70
71
// The handle is the visible node which you can click and drag.
72
this._handle = JX.$N('div', {className: 'jx-scrollbar-handle'});
73
74
// The bar is the area the handle slides up and down in.
75
this._bar = JX.$N('div', {className: 'jx-scrollbar-bar'}, this._handle);
76
77
JX.DOM.prependContent(frame, this._bar);
78
79
JX.DOM.listen(this._handle, 'mousedown', null, JX.bind(this, this._ondrag));
80
JX.DOM.listen(this._bar, 'mousedown', null, JX.bind(this, this._onjump));
81
82
JX.enableDispatch(document.body, 'mouseenter');
83
JX.DOM.listen(viewport, 'mouseenter', null, JX.bind(this, this._onenter));
84
85
JX.DOM.listen(frame, 'scroll', null, JX.bind(this, this._onscroll));
86
87
// Enabling dispatch for this event on `window` allows us to scroll even
88
// if the mouse cursor is dragged outside the window in at least some
89
// browsers (for example, Safari on OSX).
90
JX.enableDispatch(window, 'mousemove');
91
JX.Stratcom.listen('mousemove', null, JX.bind(this, this._onmove));
92
93
JX.Stratcom.listen('mouseup', null, JX.bind(this, this._ondrop));
94
JX.Stratcom.listen('resize', null, JX.bind(this, this._onresize));
95
96
this._resizeViewport();
97
this._resizeBar();
98
},
99
100
statics: {
101
_controlWidth: null,
102
103
104
/**
105
* Compute the width of the browser's scrollbar control, in pixels.
106
*/
107
getScrollbarControlWidth: function() {
108
var self = JX.Scrollbar;
109
110
if (self._controlWidth === null) {
111
var tmp = JX.$N('div', {className: 'jx-scrollbar-test'}, '-');
112
document.body.appendChild(tmp);
113
var d1 = JX.Vector.getDim(tmp);
114
tmp.style.overflowY = 'scroll';
115
var d2 = JX.Vector.getDim(tmp);
116
JX.DOM.remove(tmp);
117
118
self._controlWidth = (d2.x - d1.x);
119
}
120
121
return self._controlWidth;
122
},
123
124
125
/**
126
* Get the margin width required to avoid double scrollbars.
127
*
128
* For most browsers which render a real scrollbar control, this is 0.
129
* Adjacent elements may touch the edge of the content directly without
130
* overlapping.
131
*
132
* On OSX with a trackpad, scrollbars are only drawn when content is
133
* scrolled. Content panes with internal scrollbars may overlap adjacent
134
* scrollbars if they are not laid out with a margin.
135
*
136
* @return int Control margin width in pixels.
137
*/
138
getScrollbarControlMargin: function() {
139
var self = JX.Scrollbar;
140
141
// If this browser and OS don't render a real scrollbar control, we
142
// need to leave a margin. Generally, this is OSX with no mouse attached.
143
if (self.getScrollbarControlWidth() === 0) {
144
return 12;
145
}
146
147
return 0;
148
}
149
150
151
},
152
153
members: {
154
_frame: null,
155
_viewport: null,
156
_content: null,
157
158
_bar: null,
159
_handle: null,
160
161
_timeout: null,
162
_dragOrigin: null,
163
_scrollOrigin: null,
164
_lastHeight: null,
165
166
167
/**
168
* Mark this content as the scroll frame.
169
*
170
* This changes the behavior of the @{class:JX.DOM} scroll functions so the
171
* continue to work properly if the main page content is reframed to scroll
172
* independently.
173
*/
174
setAsScrollFrame: function() {
175
if (this._viewport) {
176
// If we activated the scrollbar, the viewport and content nodes become
177
// the new scroll and content frames.
178
JX.DOM.setContentFrame(this._viewport, this._content);
179
180
// If nothing is focused, or the document body is focused, change focus
181
// to the viewport. This makes the arrow keys, spacebar, and page
182
// up/page down keys work immediately after the page loads, without
183
// requiring a click.
184
185
// Focusing the <div /> itself doesn't work on any browser, so we
186
// add a fake, focusable element and focus that instead.
187
var focus = document.activeElement;
188
if (!focus || focus == window.document.body) {
189
var link = JX.$N('a', {href: '#', className: 'jx-scrollbar-link'});
190
JX.DOM.listen(link, 'blur', null, function() {
191
// When the user clicks anything else, remove this.
192
try {
193
JX.DOM.remove(link);
194
} catch (ignored) {
195
// We can get a second blur event, likey related to T447.
196
// Fix doesn't seem trivial so just ignore it.
197
}
198
});
199
JX.DOM.listen(link, 'click', null, function(e) {
200
// Don't respond to clicks. Since the link isn't visible, this
201
// most likely means the user hit enter or something like that.
202
e.kill();
203
});
204
JX.DOM.prependContent(this._viewport, link);
205
JX.DOM.focus(link);
206
}
207
} else {
208
// Otherwise, the unaltered content frame is both the scroll frame and
209
// content frame.
210
JX.DOM.setContentFrame(this._frame, this._frame);
211
}
212
},
213
214
215
/**
216
* After the user scrolls the page, show the scrollbar to give them
217
* feedback about their position.
218
*/
219
_onscroll: function() {
220
this._showBar();
221
},
222
223
224
/**
225
* When the user mouses over the viewport, show the scrollbar.
226
*/
227
_onenter: function() {
228
this._showBar();
229
},
230
231
232
/**
233
* When the user resizes the window, recalculate everything.
234
*/
235
_onresize: function() {
236
this._resizeViewport();
237
this._resizeBar();
238
},
239
240
241
/**
242
* When the user clicks the bar area (but not the handle), jump up or
243
* down a page.
244
*/
245
_onjump: function(e) {
246
if (e.getTarget() === this._handle) {
247
return;
248
}
249
250
var distance = JX.Vector.getDim(this._viewport).y * (7/8);
251
var epos = JX.$V(e);
252
var hpos = JX.$V(this._handle);
253
254
if (epos.y > hpos.y) {
255
this._viewport.scrollTop += distance;
256
} else {
257
this._viewport.scrollTop -= distance;
258
}
259
},
260
261
262
/**
263
* When the user clicks the scroll handle, begin dragging it.
264
*/
265
_ondrag: function(e) {
266
e.kill();
267
268
// Store the position where the drag started.
269
this._dragOrigin = JX.$V(e);
270
271
// Store the original position of the handle.
272
this._scrollOrigin = this._viewport.scrollTop;
273
},
274
275
276
/**
277
* As the user drags the scroll handle up or down, scroll the viewport.
278
*/
279
_onmove: function(e) {
280
if (this._dragOrigin === null) {
281
return;
282
}
283
284
var p = JX.$V(e);
285
var offset = (p.y - this._dragOrigin.y);
286
var ratio = offset / JX.Vector.getDim(this._bar).y;
287
var adjust = ratio * JX.Vector.getDim(this._content).y;
288
289
if (this._shouldSnapback()) {
290
if (Math.abs(p.x - this._dragOrigin.x) > 140) {
291
adjust = 0;
292
}
293
}
294
295
this._viewport.scrollTop = this._scrollOrigin + adjust;
296
},
297
298
299
/**
300
* Should the scrollbar snap back to the original position if the user
301
* drags the mouse away to the left or right, perpendicular to the
302
* scrollbar?
303
*
304
* Scrollbars have this behavior on Windows, but not on OSX or Linux.
305
*/
306
_shouldSnapback: function() {
307
// Since this is an OS-specific behavior, detect the OS. We can't
308
// reasonably use feature detection here.
309
return (navigator.platform.indexOf('Win') > -1);
310
},
311
312
313
/**
314
* When the user releases the mouse after a drag, stop moving the
315
* viewport.
316
*/
317
_ondrop: function() {
318
this._dragOrigin = null;
319
320
// Reset the timer to hide the bar.
321
this._showBar();
322
},
323
324
325
326
/**
327
* Something inside the frame fired a load event.
328
*
329
* The typical case is that an image loaded. This may have changed the
330
* height of the scroll area, and we may want to make adjustments.
331
*/
332
_onload: function() {
333
var viewport = this.getViewportNode();
334
var height = viewport.scrollHeight;
335
var visible = JX.Vector.getDim(viewport).y;
336
if (this._lastHeight !== null && this._lastHeight != height) {
337
338
// If the viewport was scrollable and was scrolled down to near the
339
// bottom, scroll it down to account for the new height. The effect
340
// of this rule is to keep panels like the chat column scrolled to
341
// the bottom as images load into the thread.
342
if (viewport.scrollTop > 0) {
343
if ((viewport.scrollTop + visible + 64) >= this._lastHeight) {
344
viewport.scrollTop += (height - this._lastHeight);
345
}
346
}
347
348
}
349
350
this._lastHeight = height;
351
},
352
353
354
/**
355
* Shove the scrollbar on the viewport under the edge of the frame so the
356
* user can't see it.
357
*/
358
_resizeViewport: function() {
359
var fdim = JX.Vector.getDim(this._frame);
360
fdim.x += JX.Scrollbar.getScrollbarControlWidth();
361
fdim.setDim(this._viewport);
362
},
363
364
365
/**
366
* Figure out the correct size and offset of the scrollbar handle.
367
*/
368
_resizeBar: function() {
369
// We're hiding and showing the bar itself, not just the handle, because
370
// pages that contain other panels may have scrollbars underneath the
371
// bar. If we don't hide the bar, it ends up eating clicks targeting
372
// these panels.
373
374
// Because the bar may be hidden, we can't measure it. Measure the
375
// viewport instead.
376
377
var cdim = JX.Vector.getDim(this._content);
378
var spos = JX.Vector.getAggregateScrollForNode(this._viewport);
379
var vdim = JX.Vector.getDim(this._viewport);
380
381
var ratio = (vdim.y / cdim.y);
382
383
// We're scaling things down very slightly to leave a 2px margin at
384
// either end of the scroll gutter, so the bar doesn't quite bump up
385
// against the chrome.
386
ratio = ratio * (vdim.y / (vdim.y + 4));
387
388
var offset = Math.round(ratio * spos.y) + 2;
389
var size = Math.floor(ratio * vdim.y);
390
391
if (size < cdim.y) {
392
this._handle.style.top = offset + 'px';
393
this._handle.style.height = size + 'px';
394
395
JX.DOM.show(this._bar);
396
} else {
397
JX.DOM.hide(this._bar);
398
}
399
},
400
401
402
/**
403
* Show the scrollbar for the next second.
404
*/
405
_showBar: function() {
406
this._resizeBar();
407
408
JX.DOM.alterClass(this._handle, 'jx-scrollbar-visible', true);
409
410
this._clearTimeout();
411
this._timeout = setTimeout(JX.bind(this, this._hideBar), 1000);
412
},
413
414
415
/**
416
* Hide the scrollbar.
417
*/
418
_hideBar: function() {
419
if (this._dragOrigin !== null) {
420
// If we're currently dragging the handle, we never want to hide
421
// it.
422
return;
423
}
424
425
JX.DOM.alterClass(this._handle, 'jx-scrollbar-visible', false);
426
this._clearTimeout();
427
},
428
429
430
/**
431
* Clear the scrollbar hide timeout, if one is set.
432
*/
433
_clearTimeout: function() {
434
if (this._timeout) {
435
clearTimeout(this._timeout);
436
this._timeout = null;
437
}
438
},
439
440
getContentNode: function() {
441
return this._content || this._frame;
442
},
443
444
getViewportNode: function() {
445
return this._viewport || this._frame;
446
},
447
448
scrollTo: function(scroll) {
449
if (this._viewport !== null) {
450
this._viewport.scrollTop = scroll;
451
} else {
452
this._frame.scrollTop = scroll;
453
}
454
return this;
455
}
456
}
457
458
});
459
460