diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => '8d1c0f87', + 'core.pkg.css' => 'c32e4b1a', 'core.pkg.js' => '2d9bfc06', 'darkconsole.pkg.js' => '8ab24e01', 'differential.pkg.css' => '8af45893', @@ -39,7 +39,7 @@ 'rsrc/css/application/base/main-menu-view.css' => '3cf893a9', 'rsrc/css/application/base/notification-menu.css' => '6aa0a74b', 'rsrc/css/application/base/phabricator-application-launch-view.css' => '5d71008f', - 'rsrc/css/application/base/standard-page-view.css' => '2c96cfb5', + 'rsrc/css/application/base/standard-page-view.css' => '15ace741', 'rsrc/css/application/chatlog/chatlog.css' => '852140ff', 'rsrc/css/application/config/config-options.css' => '7fedf08b', 'rsrc/css/application/config/config-template.css' => '25d446d6', @@ -104,7 +104,7 @@ 'rsrc/css/application/slowvote/slowvote.css' => '266df6a1', 'rsrc/css/application/tokens/tokens.css' => '3d0f239e', 'rsrc/css/application/uiexample/example.css' => '528b19de', - 'rsrc/css/core/core.css' => 'ca42b69f', + 'rsrc/css/core/core.css' => 'd7f6ec35', 'rsrc/css/core/remarkup.css' => '0ee3d256', 'rsrc/css/core/syntax.css' => '56c1ba38', 'rsrc/css/core/z-index.css' => '44e1d311', @@ -199,6 +199,7 @@ 'rsrc/externals/javelin/lib/Resource.js' => '44959b73', 'rsrc/externals/javelin/lib/Routable.js' => 'b3e7d692', 'rsrc/externals/javelin/lib/Router.js' => '29274e2b', + 'rsrc/externals/javelin/lib/Scrollbar.js' => 'aed4f2d0', 'rsrc/externals/javelin/lib/URI.js' => '6eff08aa', 'rsrc/externals/javelin/lib/Vector.js' => 'cc1bd0b0', 'rsrc/externals/javelin/lib/WebSocket.js' => '3f840822', @@ -476,6 +477,7 @@ 'rsrc/js/core/behavior-remarkup-preview.js' => 'f7379f45', 'rsrc/js/core/behavior-reorder-applications.js' => '76b9fc3e', 'rsrc/js/core/behavior-reveal-content.js' => '60821bc7', + 'rsrc/js/core/behavior-scrollbar.js' => '77607b63', 'rsrc/js/core/behavior-search-typeahead.js' => '724b1247', 'rsrc/js/core/behavior-select-on-click.js' => '4e3e79a6', 'rsrc/js/core/behavior-toggle-class.js' => 'e566f52c', @@ -641,6 +643,7 @@ 'javelin-behavior-reorder-applications' => '76b9fc3e', 'javelin-behavior-reorder-columns' => 'e1d25dfb', 'javelin-behavior-repository-crossreference' => 'f9539603', + 'javelin-behavior-scrollbar' => '77607b63', 'javelin-behavior-search-reorder-queries' => 'e9581f08', 'javelin-behavior-select-on-click' => '4e3e79a6', 'javelin-behavior-slowvote-embed' => 'd6f54db0', @@ -670,6 +673,7 @@ 'javelin-resource' => '44959b73', 'javelin-routable' => 'b3e7d692', 'javelin-router' => '29274e2b', + 'javelin-scrollbar' => 'aed4f2d0', 'javelin-stratcom' => '8b0ad945', 'javelin-tokenizer' => '7644823e', 'javelin-typeahead' => '70baed2f', @@ -705,7 +709,7 @@ 'phabricator-busy' => '6453c869', 'phabricator-chatlog-css' => '852140ff', 'phabricator-content-source-view-css' => '4b8b05d4', - 'phabricator-core-css' => 'ca42b69f', + 'phabricator-core-css' => 'd7f6ec35', 'phabricator-countdown-css' => '86b7b0a0', 'phabricator-crumbs-view-css' => 'd5aa87e4', 'phabricator-dashboard-css' => 'a2bfdcbf', @@ -735,7 +739,7 @@ 'phabricator-side-menu-view-css' => '7e8c6341', 'phabricator-slowvote-css' => '266df6a1', 'phabricator-source-code-view-css' => '7d346aa4', - 'phabricator-standard-page-view' => '2c96cfb5', + 'phabricator-standard-page-view' => '15ace741', 'phabricator-textareautils' => '5c93c52c', 'phabricator-title' => '5c1c758c', 'phabricator-tooltip' => '1d298e3a', @@ -1328,6 +1332,10 @@ 'javelin-reactor', 'javelin-util', ), + '77607b63' => array( + 'javelin-behavior', + 'javelin-scrollbar', + ), '7814b593' => array( 'javelin-request', 'javelin-behavior', @@ -1569,6 +1577,10 @@ 'javelin-dom', 'javelin-vector', ), + 'aed4f2d0' => array( + 'javelin-install', + 'javelin-dom', + ), 'b07b009f' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -340,10 +340,16 @@ } } - return phutil_tag( + Javelin::initBehavior( + 'scrollbar', + array( + 'nodeID' => 'phabricator-standard-page', + )); + + $main_page = phutil_tag( 'div', array( - 'id' => 'base-page', + 'id' => 'phabricator-standard-page', 'class' => 'phabricator-standard-page', ), array( @@ -356,6 +362,15 @@ $this->renderFooter(), )), )); + + return phutil_tag( + 'div', + array( + 'class' => 'main-page-frame', + ), + array( + $main_page, + )); } protected function getTail() { diff --git a/webroot/rsrc/css/application/base/standard-page-view.css b/webroot/rsrc/css/application/base/standard-page-view.css --- a/webroot/rsrc/css/application/base/standard-page-view.css +++ b/webroot/rsrc/css/application/base/standard-page-view.css @@ -112,3 +112,74 @@ display: inline-block; margin: 2px 2px -2px 0; } + +.main-page-frame { + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + overflow: hidden; +} + +.jx-scrollbar-frame { + position: relative; + height: 100%; + overflow: hidden; +} + +.jx-scrollbar-viewport { + position: absolute; + overflow-x: hidden; + overflow-y: scroll; + top: 0; + bottom: 0; + left: 0; + right: 0; +} + +.jx-scrollbar-test { + position: absolute; + left: -300px; +} + +.jx-scrollbar-bar { + z-index: 99; + position: absolute; + top: 0; + right: 0; + bottom: 7px; + width: 11px; +} + +.jx-scrollbar-bar .jx-scrollbar-handle { + position: absolute; + right: 2px; + -webkit-border-radius: 7px; + -moz-border-radius: 7px; + border-radius: 7px; + min-height: 10px; + width: 7px; + opacity: 0; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + -o-transition: opacity 0.2s linear; + -ms-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; + background: #6c6e71; + -webkit-background-clip: padding-box; + -moz-background-clip: padding; +} + +.jx-scrollbar-bar:hover .jx-scrollbar-handle { + opacity: 0.7; + -webkit-transition: opacity 0 linear; + -moz-transition: opacity 0 linear; + -o-transition: opacity 0 linear; + -ms-transition: opacity 0 linear; + transition: opacity 0 linear; +} + +.jx-scrollbar-bar .jx-scrollbar-visible { + opacity: 0.7; +} diff --git a/webroot/rsrc/css/core/core.css b/webroot/rsrc/css/core/core.css --- a/webroot/rsrc/css/core/core.css +++ b/webroot/rsrc/css/core/core.css @@ -2,16 +2,6 @@ * @provides phabricator-core-css */ -body { - /* Always show the vertical scrollbar so that going from a page without a - scrollbar to a page with a scrollbar doesn't make content jump a few - pixels left when the viewport narrows. */ - overflow-y: scroll; - /* reset behavior in ie7, as it will add an extra scrollbar regardless - selector * targets ie6 and ie7 only */ - *overflow-y: auto; -} - .device-phone { /* By default, the iPhone zooms all text on the page by some percentage when you rotate from portrait mode to landscape mode. Disable this, since it diff --git a/webroot/rsrc/externals/javelin/lib/Scrollbar.js b/webroot/rsrc/externals/javelin/lib/Scrollbar.js new file mode 100644 --- /dev/null +++ b/webroot/rsrc/externals/javelin/lib/Scrollbar.js @@ -0,0 +1,275 @@ +/** + * @provides javelin-scrollbar + * @requires javelin-install + * javelin-dom + * javelin-stratcom + * javelin-vector + * @javelin + */ + +/** + * Provides an aesthetic scrollbar. + * + * This shoves an element's scrollbar under a hidden overflow and draws a + * pretty looking fake one in its place. This makes complex UIs with multiple + * independently scrollable panels less hideous by (a) making the scrollbar + * itself prettier and (b) reclaiming the space occupied by the scrollbar. + * + * Note that on OSX the heavy scrollbars are normally drawn only if you have + * a mouse connected. OSX uses more aesthetic touchpad scrollbars normally, + * which these scrollbars emulate. + * + * This class was initially adapted from "Trackpad Scroll Emulator", by + * Jonathan Nicol. See . + */ +JX.install('Scrollbar', { + + construct: function(frame) { + // Wrap the frame content in a bunch of nodes. The frame itself stays on + // the outside so that any positioning information the node had isn't + // disrupted. + + // We put a "viewport" node inside of it, which is what actually scrolls. + // This is the node that gets a scrollbar, but we make the viewport very + // slightly too wide for the frame. That hides the scrollbar underneath + // the edge of the frame. + + // We put a "content" node inside of the viewport. This allows us to + // measure the content height so we can resize and offset the scrollbar + // handle properly. + + // We move all the actual frame content into the "content" node. So it + // ends up wrapped by the "content" node, then by the "viewport" node, + // and finally by the original "frame" node. + + JX.DOM.alterClass(frame, 'jx-scrollbar-frame', true); + + var content = JX.$N('div', {className: 'jx-scrollbar-content'}); + while (frame.firstChild) { + content.appendChild(frame.firstChild); + } + + var viewport = JX.$N('div', {className: 'jx-scrollbar-viewport'}); + viewport.appendChild(content); + + frame.appendChild(viewport); + + this._frame = frame; + this._viewport = viewport; + this._content = content; + + // The handle is the visible node which you can click and drag. + this._handle = JX.$N('div', {className: 'jx-scrollbar-handle'}); + + // The bar is the area the handle slides up and down in. + this._bar = JX.$N('div', {className: 'jx-scrollbar-bar'}, this._handle); + + JX.DOM.prependContent(frame, this._bar); + + JX.DOM.listen(this._handle, 'mousedown', null, JX.bind(this, this._ondrag)); + JX.DOM.listen(this._bar, 'mousedown', null, JX.bind(this, this._onjump)); + + JX.enableDispatch(document.body, 'mouseenter'); + JX.enableDispatch(document.body, 'mousemove'); + + JX.DOM.listen(viewport, 'mouseenter', null, JX.bind(this, this._onenter)); + JX.DOM.listen(frame, 'scroll', null, JX.bind(this, this._onscroll)); + + JX.DOM.listen(viewport, 'mouseenter', null, JX.bind(this, this._onenter)); + JX.DOM.listen(viewport, 'mouseenter', null, JX.bind(this, this._onenter)); + + JX.Stratcom.listen('mousemove', null, JX.bind(this, this._onmove)); + JX.Stratcom.listen('mouseup', null, JX.bind(this, this._ondrop)); + JX.Stratcom.listen('resize', null, JX.bind(this, this._onresize)); + + this._resizeViewport(); + this._resizeBar(); + }, + + statics: { + _controlWidth: null, + + /** + * Compute the width of the browser's scrollbar control, in pixels. + */ + _getScrollbarControlWidth: function() { + var self = JX.Scrollbar; + + if (self._controlWidth === null) { + var tmp = JX.$N('div', {className: 'jx-scrollbar-test'}, '-'); + document.body.appendChild(tmp); + var d1 = JX.Vector.getDim(tmp); + tmp.style.overflowY = 'scroll'; + var d2 = JX.Vector.getDim(tmp); + JX.DOM.remove(tmp); + + self._controlWidth = (d2.x - d1.x); + } + + return self._controlWidth; + } + + }, + + members: { + _frame: null, + _viewport: null, + _content: null, + + _bar: null, + _handle: null, + + _timeout: null, + _dragOrigin: null, + + + /** + * After the user scrolls the page, show the scrollbar to give them + * feedback about their position. + */ + _onscroll: function() { + this._showBar(); + }, + + + /** + * When the user mouses over the viewport, show the scrollbar. + */ + _onenter: function() { + this._showBar(); + }, + + + /** + * When the user resizes the window, recalculate everything. + */ + _onresize: function() { + this._resizeViewport(); + this._resizeBar(); + }, + + + /** + * When the user clicks the bar area (but not the handle), jump up or + * down a page. + */ + _onjump: function(e) { + if (e.getTarget() === this._handle) { + return; + } + + var distance = JX.Vector.getDim(this._viewport).y * (7/8); + var epos = JX.$V(e); + var hpos = JX.$V(this._handle); + + if (epos.y > hpos.y) { + this._viewport.scrollTop += distance; + } else { + this._viewport.scrollTop -= distance; + } + }, + + + /** + * When the user clicks the scroll handle, begin dragging it. + */ + _ondrag: function(e) { + e.kill(); + this._dragOrigin = JX.$V(e).y; + }, + + + /** + * As the user drags the scroll handle up or down, scroll the viewport. + */ + _onmove: function(e) { + if (this._dragOrigin === null) { + return; + } + + var offset = (JX.$V(e).y - this._dragOrigin); + var ratio = offset / JX.Vector.getDim(this._bar).y; + var target = ratio * JX.Vector.getDim(this._content).y; + + this._viewport.scrollTop = target; + }, + + + /** + * When the user releases the mouse after a drag, stop moving the + * viewport. + */ + _ondrop: function() { + this._dragOrigin = null; + }, + + + /** + * Shove the scrollbar on the viewport under the edge of the frame so the + * user can't see it. + */ + _resizeViewport: function() { + var fdim = JX.Vector.getDim(this._frame); + fdim.x += JX.Scrollbar._getScrollbarControlWidth(); + fdim.setDim(this._viewport); + }, + + + /** + * Figure out the correct size and offset of the scrollbar handle. + */ + _resizeBar: function() { + var cdim = JX.Vector.getDim(this._content); + var spos = JX.Vector.getAggregateScrollForNode(this._viewport); + var bdim = JX.Vector.getDim(this._bar); + + var ratio = bdim.y / cdim.y; + + var offset = Math.round(ratio * spos.y) + 2; + var size = Math.floor(ratio * (bdim.y - 2)) - 2; + + if (size < cdim.y) { + this._handle.style.top = offset + 'px'; + this._handle.style.height = size + 'px'; + + JX.DOM.show(this._handle); + } else { + JX.DOM.hide(this._handle); + } + }, + + + /** + * Show the scrollbar for the next second. + */ + _showBar: function() { + this._resizeBar(); + + JX.DOM.alterClass(this._handle, 'jx-scrollbar-visible', true); + + this._clearTimeout(); + this._timeout = setTimeout(JX.bind(this, this._hideBar), 1000); + }, + + + /** + * Hide the scrollbar. + */ + _hideBar: function() { + JX.DOM.alterClass(this._handle, 'jx-scrollbar-visible', false); + this._clearTimeout(); + }, + + + /** + * Clear the scrollbar hide timeout, if one is set. + */ + _clearTimeout: function() { + if (this._timeout) { + clearTimeout(this._timeout); + this._timeout = null; + } + } + } + +}); diff --git a/webroot/rsrc/js/application/slowvote/behavior-slowvote-embed.js b/webroot/rsrc/js/application/slowvote/behavior-slowvote-embed.js --- a/webroot/rsrc/js/application/slowvote/behavior-slowvote-embed.js +++ b/webroot/rsrc/js/application/slowvote/behavior-slowvote-embed.js @@ -20,7 +20,7 @@ var request = new JX.Request(voteURI, function(r) { var updated_poll = JX.$H(r.contentHTML); - var root = JX.$('base-page'); + var root = JX.$('phabricator-standard-page'); var polls = JX.DOM.scry(root, 'div', 'slowvote-embed'); diff --git a/webroot/rsrc/js/core/behavior-scrollbar.js b/webroot/rsrc/js/core/behavior-scrollbar.js new file mode 100644 --- /dev/null +++ b/webroot/rsrc/js/core/behavior-scrollbar.js @@ -0,0 +1,9 @@ +/** + * @provides javelin-behavior-scrollbar + * @requires javelin-behavior + * javelin-scrollbar + */ + +JX.behavior('scrollbar', function(config) { + new JX.Scrollbar(JX.$(config.nodeID)); +});