Path: blob/master/webroot/rsrc/externals/javelin/lib/Scrollbar.js
12242 views
/**1* @provides javelin-scrollbar2* @requires javelin-install3* javelin-dom4* javelin-stratcom5* javelin-vector6* @javelin7*/89/**10* Provides an aesthetic scrollbar.11*12* This shoves an element's scrollbar under a hidden overflow and draws a13* pretty looking fake one in its place. This makes complex UIs with multiple14* independently scrollable panels less hideous by (a) making the scrollbar15* itself prettier and (b) reclaiming the space occupied by the scrollbar.16*17* Note that on OSX the heavy scrollbars are normally drawn only if you have18* a mouse connected. OSX uses more aesthetic touchpad scrollbars normally,19* which these scrollbars emulate.20*21* This class was initially adapted from "Trackpad Scroll Emulator", by22* Jonathan Nicol. See <https://github.com/jnicol/trackpad-scroll-emulator>.23*/24JX.install('Scrollbar', {2526construct: function(frame) {27this._frame = frame;2829JX.DOM.listen(frame, 'load', null, JX.bind(this, this._onload));30this._onload();3132// Before doing anything, check if the scrollbar control has a measurable33// width. If it doesn't, we're already in an environment with an aesthetic34// scrollbar (like Safari on OSX with no mouse connected, or an iPhone)35// and we don't need to do anything.36if (JX.Scrollbar.getScrollbarControlWidth() === 0) {37return;38}3940// Wrap the frame content in a bunch of nodes. The frame itself stays on41// the outside so that any positioning information the node had isn't42// disrupted.4344// We put a "viewport" node inside of it, which is what actually scrolls.45// This is the node that gets a scrollbar, but we make the viewport very46// slightly too wide for the frame. That hides the scrollbar underneath47// the edge of the frame.4849// We put a "content" node inside of the viewport. This allows us to50// measure the content height so we can resize and offset the scrollbar51// handle properly.5253// We move all the actual frame content into the "content" node. So it54// ends up wrapped by the "content" node, then by the "viewport" node,55// and finally by the original "frame" node.5657JX.DOM.alterClass(frame, 'jx-scrollbar-frame', true);5859var content = JX.$N('div', {className: 'jx-scrollbar-content'});60while (frame.firstChild) {61JX.DOM.appendContent(content, frame.firstChild);62}6364var viewport = JX.$N('div', {className: 'jx-scrollbar-viewport'}, content);65JX.DOM.appendContent(frame, viewport);6667this._viewport = viewport;68this._content = content;6970// The handle is the visible node which you can click and drag.71this._handle = JX.$N('div', {className: 'jx-scrollbar-handle'});7273// The bar is the area the handle slides up and down in.74this._bar = JX.$N('div', {className: 'jx-scrollbar-bar'}, this._handle);7576JX.DOM.prependContent(frame, this._bar);7778JX.DOM.listen(this._handle, 'mousedown', null, JX.bind(this, this._ondrag));79JX.DOM.listen(this._bar, 'mousedown', null, JX.bind(this, this._onjump));8081JX.enableDispatch(document.body, 'mouseenter');82JX.DOM.listen(viewport, 'mouseenter', null, JX.bind(this, this._onenter));8384JX.DOM.listen(frame, 'scroll', null, JX.bind(this, this._onscroll));8586// Enabling dispatch for this event on `window` allows us to scroll even87// if the mouse cursor is dragged outside the window in at least some88// browsers (for example, Safari on OSX).89JX.enableDispatch(window, 'mousemove');90JX.Stratcom.listen('mousemove', null, JX.bind(this, this._onmove));9192JX.Stratcom.listen('mouseup', null, JX.bind(this, this._ondrop));93JX.Stratcom.listen('resize', null, JX.bind(this, this._onresize));9495this._resizeViewport();96this._resizeBar();97},9899statics: {100_controlWidth: null,101102103/**104* Compute the width of the browser's scrollbar control, in pixels.105*/106getScrollbarControlWidth: function() {107var self = JX.Scrollbar;108109if (self._controlWidth === null) {110var tmp = JX.$N('div', {className: 'jx-scrollbar-test'}, '-');111document.body.appendChild(tmp);112var d1 = JX.Vector.getDim(tmp);113tmp.style.overflowY = 'scroll';114var d2 = JX.Vector.getDim(tmp);115JX.DOM.remove(tmp);116117self._controlWidth = (d2.x - d1.x);118}119120return self._controlWidth;121},122123124/**125* Get the margin width required to avoid double scrollbars.126*127* For most browsers which render a real scrollbar control, this is 0.128* Adjacent elements may touch the edge of the content directly without129* overlapping.130*131* On OSX with a trackpad, scrollbars are only drawn when content is132* scrolled. Content panes with internal scrollbars may overlap adjacent133* scrollbars if they are not laid out with a margin.134*135* @return int Control margin width in pixels.136*/137getScrollbarControlMargin: function() {138var self = JX.Scrollbar;139140// If this browser and OS don't render a real scrollbar control, we141// need to leave a margin. Generally, this is OSX with no mouse attached.142if (self.getScrollbarControlWidth() === 0) {143return 12;144}145146return 0;147}148149150},151152members: {153_frame: null,154_viewport: null,155_content: null,156157_bar: null,158_handle: null,159160_timeout: null,161_dragOrigin: null,162_scrollOrigin: null,163_lastHeight: null,164165166/**167* Mark this content as the scroll frame.168*169* This changes the behavior of the @{class:JX.DOM} scroll functions so the170* continue to work properly if the main page content is reframed to scroll171* independently.172*/173setAsScrollFrame: function() {174if (this._viewport) {175// If we activated the scrollbar, the viewport and content nodes become176// the new scroll and content frames.177JX.DOM.setContentFrame(this._viewport, this._content);178179// If nothing is focused, or the document body is focused, change focus180// to the viewport. This makes the arrow keys, spacebar, and page181// up/page down keys work immediately after the page loads, without182// requiring a click.183184// Focusing the <div /> itself doesn't work on any browser, so we185// add a fake, focusable element and focus that instead.186var focus = document.activeElement;187if (!focus || focus == window.document.body) {188var link = JX.$N('a', {href: '#', className: 'jx-scrollbar-link'});189JX.DOM.listen(link, 'blur', null, function() {190// When the user clicks anything else, remove this.191try {192JX.DOM.remove(link);193} catch (ignored) {194// We can get a second blur event, likey related to T447.195// Fix doesn't seem trivial so just ignore it.196}197});198JX.DOM.listen(link, 'click', null, function(e) {199// Don't respond to clicks. Since the link isn't visible, this200// most likely means the user hit enter or something like that.201e.kill();202});203JX.DOM.prependContent(this._viewport, link);204JX.DOM.focus(link);205}206} else {207// Otherwise, the unaltered content frame is both the scroll frame and208// content frame.209JX.DOM.setContentFrame(this._frame, this._frame);210}211},212213214/**215* After the user scrolls the page, show the scrollbar to give them216* feedback about their position.217*/218_onscroll: function() {219this._showBar();220},221222223/**224* When the user mouses over the viewport, show the scrollbar.225*/226_onenter: function() {227this._showBar();228},229230231/**232* When the user resizes the window, recalculate everything.233*/234_onresize: function() {235this._resizeViewport();236this._resizeBar();237},238239240/**241* When the user clicks the bar area (but not the handle), jump up or242* down a page.243*/244_onjump: function(e) {245if (e.getTarget() === this._handle) {246return;247}248249var distance = JX.Vector.getDim(this._viewport).y * (7/8);250var epos = JX.$V(e);251var hpos = JX.$V(this._handle);252253if (epos.y > hpos.y) {254this._viewport.scrollTop += distance;255} else {256this._viewport.scrollTop -= distance;257}258},259260261/**262* When the user clicks the scroll handle, begin dragging it.263*/264_ondrag: function(e) {265e.kill();266267// Store the position where the drag started.268this._dragOrigin = JX.$V(e);269270// Store the original position of the handle.271this._scrollOrigin = this._viewport.scrollTop;272},273274275/**276* As the user drags the scroll handle up or down, scroll the viewport.277*/278_onmove: function(e) {279if (this._dragOrigin === null) {280return;281}282283var p = JX.$V(e);284var offset = (p.y - this._dragOrigin.y);285var ratio = offset / JX.Vector.getDim(this._bar).y;286var adjust = ratio * JX.Vector.getDim(this._content).y;287288if (this._shouldSnapback()) {289if (Math.abs(p.x - this._dragOrigin.x) > 140) {290adjust = 0;291}292}293294this._viewport.scrollTop = this._scrollOrigin + adjust;295},296297298/**299* Should the scrollbar snap back to the original position if the user300* drags the mouse away to the left or right, perpendicular to the301* scrollbar?302*303* Scrollbars have this behavior on Windows, but not on OSX or Linux.304*/305_shouldSnapback: function() {306// Since this is an OS-specific behavior, detect the OS. We can't307// reasonably use feature detection here.308return (navigator.platform.indexOf('Win') > -1);309},310311312/**313* When the user releases the mouse after a drag, stop moving the314* viewport.315*/316_ondrop: function() {317this._dragOrigin = null;318319// Reset the timer to hide the bar.320this._showBar();321},322323324325/**326* Something inside the frame fired a load event.327*328* The typical case is that an image loaded. This may have changed the329* height of the scroll area, and we may want to make adjustments.330*/331_onload: function() {332var viewport = this.getViewportNode();333var height = viewport.scrollHeight;334var visible = JX.Vector.getDim(viewport).y;335if (this._lastHeight !== null && this._lastHeight != height) {336337// If the viewport was scrollable and was scrolled down to near the338// bottom, scroll it down to account for the new height. The effect339// of this rule is to keep panels like the chat column scrolled to340// the bottom as images load into the thread.341if (viewport.scrollTop > 0) {342if ((viewport.scrollTop + visible + 64) >= this._lastHeight) {343viewport.scrollTop += (height - this._lastHeight);344}345}346347}348349this._lastHeight = height;350},351352353/**354* Shove the scrollbar on the viewport under the edge of the frame so the355* user can't see it.356*/357_resizeViewport: function() {358var fdim = JX.Vector.getDim(this._frame);359fdim.x += JX.Scrollbar.getScrollbarControlWidth();360fdim.setDim(this._viewport);361},362363364/**365* Figure out the correct size and offset of the scrollbar handle.366*/367_resizeBar: function() {368// We're hiding and showing the bar itself, not just the handle, because369// pages that contain other panels may have scrollbars underneath the370// bar. If we don't hide the bar, it ends up eating clicks targeting371// these panels.372373// Because the bar may be hidden, we can't measure it. Measure the374// viewport instead.375376var cdim = JX.Vector.getDim(this._content);377var spos = JX.Vector.getAggregateScrollForNode(this._viewport);378var vdim = JX.Vector.getDim(this._viewport);379380var ratio = (vdim.y / cdim.y);381382// We're scaling things down very slightly to leave a 2px margin at383// either end of the scroll gutter, so the bar doesn't quite bump up384// against the chrome.385ratio = ratio * (vdim.y / (vdim.y + 4));386387var offset = Math.round(ratio * spos.y) + 2;388var size = Math.floor(ratio * vdim.y);389390if (size < cdim.y) {391this._handle.style.top = offset + 'px';392this._handle.style.height = size + 'px';393394JX.DOM.show(this._bar);395} else {396JX.DOM.hide(this._bar);397}398},399400401/**402* Show the scrollbar for the next second.403*/404_showBar: function() {405this._resizeBar();406407JX.DOM.alterClass(this._handle, 'jx-scrollbar-visible', true);408409this._clearTimeout();410this._timeout = setTimeout(JX.bind(this, this._hideBar), 1000);411},412413414/**415* Hide the scrollbar.416*/417_hideBar: function() {418if (this._dragOrigin !== null) {419// If we're currently dragging the handle, we never want to hide420// it.421return;422}423424JX.DOM.alterClass(this._handle, 'jx-scrollbar-visible', false);425this._clearTimeout();426},427428429/**430* Clear the scrollbar hide timeout, if one is set.431*/432_clearTimeout: function() {433if (this._timeout) {434clearTimeout(this._timeout);435this._timeout = null;436}437},438439getContentNode: function() {440return this._content || this._frame;441},442443getViewportNode: function() {444return this._viewport || this._frame;445},446447scrollTo: function(scroll) {448if (this._viewport !== null) {449this._viewport.scrollTop = scroll;450} else {451this._frame.scrollTop = scroll;452}453return this;454}455}456457});458459460