'favico', 'enquire', 'perfect-scrollbar', 'WoltLabSuite/Core/Date/Time/Relative',
'Ui/SimpleDropdown', 'WoltLabSuite/Core/Ui/Mobile', 'WoltLabSuite/Core/Ui/TabMenu', 'WoltLabSuite/Core/Ui/FlexibleMenu',
'Ui/Dialog', 'WoltLabSuite/Core/Ui/Tooltip', 'WoltLabSuite/Core/Language', 'WoltLabSuite/Core/Environment',
- 'WoltLabSuite/Core/Date/Picker', 'EventHandler', 'Core', 'WoltLabSuite/Core/Ui/Page/JumpToTop',
+ 'WoltLabSuite/Core/Date/Picker', 'EventHandler', 'Core', 'WoltLabSuite/Core/Ui/Page/Action',
'Devtools', 'Dom/ChangeListener'
],
function(
favico, enquire, perfectScrollbar, DateTimeRelative,
UiSimpleDropdown, UiMobile, UiTabMenu, UiFlexibleMenu,
UiDialog, UiTooltip, Language, Environment,
- DatePicker, EventHandler, Core, UiPageJumpToTop,
+ DatePicker, EventHandler, Core, UiPageAction,
Devtools, DomChangeListener
)
{
// putting it at the end of the jQuery queue avoids trashing the
// layout too early and thus delaying the page initialization
window.jQuery(function() {
- new UiPageJumpToTop();
+ UiPageAction.setup();
});
window.jQuery.holdReady(false);
/**
* Provides page actions such as "jump to top" and clipboard actions.
- *
+ *
* @author Alexander Ebert
- * @copyright 2001-2019 WoltLab GmbH
+ * @copyright 2001-2020 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @module WoltLabSuite/Core/Ui/Page/Action
*/
-define(['Dictionary', 'Dom/Util'], function(Dictionary, DomUtil) {
- "use strict";
+define(['Dictionary', 'Language'], function (Dictionary, Language) {
+ 'use strict';
var _buttons = new Dictionary();
- var _container = null;
+
+ /** @var {Element} */
+ var _container;
+
var _didInit = false;
+ var _lastPosition = -1;
+
+ /** @var {Element} */
+ var _toTopButton;
+
+ /** @var {Element} */
+ var _wrapper;
+
/**
* @exports WoltLabSuite/Core/Ui/Page/Action
*/
/**
* Initializes the page action container.
*/
- setup: function() {
+ setup: function () {
_didInit = true;
- _container = elCreate('ul');
- _container.className = 'pageAction';
- document.body.appendChild(_container);
+ _wrapper = elCreate('div');
+ _wrapper.className = 'pageAction';
+
+ _container = elCreate('div');
+ _container.className = 'pageActionButtons';
+ _wrapper.appendChild(_container);
+
+ _toTopButton = this._buildToTopButton();
+ _wrapper.appendChild(_toTopButton);
+
+ document.body.appendChild(_wrapper);
+
+ window.addEventListener(
+ 'scroll',
+ window.debounce(this._onScroll.bind(this), 100, false),
+ {passive: true}
+ );
+
+ this._onScroll();
+ },
+
+ _buildToTopButton: function () {
+ var button = elCreate('a');
+ button.className = 'button buttonPrimary pageActionButtonToTop initiallyHidden jsTooltip';
+ button.href = '';
+ elAttr(button, 'title', Language.get('wcf.global.scrollUp'));
+ elAttr(button, 'aria-hidden', 'true');
+ button.innerHTML = '<span class="icon icon32 fa-angle-up"></span>';
+
+ button.addEventListener(WCF_CLICK_EVENT, this._scrollTopTop.bind(this));
+
+ return button;
+ },
+
+ /**
+ * @param {Event=} event
+ */
+ _onScroll: function (event) {
+ var offset = window.pageYOffset;
+ console.log(offset, _lastPosition);
+ if (offset >= 300) {
+ if (_toTopButton.classList.contains('initiallyHidden')) {
+ _toTopButton.classList.remove('initiallyHidden');
+ }
+
+ elAttr(_toTopButton, 'aria-hidden', 'false');
+ }
+ else {
+ elAttr(_toTopButton, 'aria-hidden', 'true');
+ }
+
+ this._renderContainer();
+
+ if (_lastPosition !== -1) {
+ _wrapper.classList[offset < _lastPosition ? 'remove' : 'add']('scrolledDown');
+ }
+
+ _lastPosition = offset;
+ },
+
+ /**
+ * @param {Event} event
+ */
+ _scrollTopTop: function (event) {
+ event.preventDefault();
+
+ elById('top').scrollIntoView({behavior: 'smooth'});
},
/**
* Adds a button to the page action list. You can optionally provide a button name to
* insert the button right before it. Unmatched button names or empty value will cause
* the button to be prepended to the list.
- *
+ *
* @param {string} buttonName unique identifier
* @param {Element} button button element, must not be wrapped in a <li>
* @param {string=} insertBeforeButton insert button before element identified by provided button name
*/
- add: function(buttonName, button, insertBeforeButton) {
+ add: function (buttonName, button, insertBeforeButton) {
if (_didInit === false) this.setup();
- var listItem = elCreate('li');
+ // The wrapper is required for backwards compatibility, because some implementations rely on a
+ // dedicated parent element to insert elements, for example, for drop-down menus.
+ var wrapper = elCreate('div');
+ wrapper.className = 'pageActionButton';
+ wrapper.name = buttonName;
+ elAttr(wrapper, 'aria-hidden', 'false');
+
button.classList.add('button');
button.classList.add('buttonPrimary');
- listItem.appendChild(button);
- elAttr(listItem, 'aria-hidden', (buttonName === 'toTop' ? 'true' : 'false'));
- elData(listItem, 'name', buttonName);
-
- // force 'to top' button to be always at the most outer position
- if (buttonName === 'toTop') {
- listItem.className = 'toTop initiallyHidden';
- _container.appendChild(listItem);
- }
- else {
- var insertBefore = null;
- if (insertBeforeButton) {
- insertBefore = _buttons.get(insertBeforeButton);
- if (insertBefore !== undefined) {
- insertBefore = insertBefore.parentNode;
- }
- }
-
- if (insertBefore === null && _container.childElementCount) {
- insertBefore = _container.children[0];
- }
-
- if (insertBefore === null) {
- DomUtil.prepend(listItem, _container);
- }
- else {
- _container.insertBefore(listItem, insertBefore);
+ wrapper.appendChild(button);
+
+ var insertBefore = null;
+ if (insertBeforeButton) {
+ insertBefore = _buttons.get(insertBeforeButton);
+ if (insertBefore !== undefined) {
+ insertBefore = insertBefore.parentNode;
}
}
+ if (insertBefore === null && _container.childElementCount) {
+ insertBefore = _container.children[0];
+ }
+ if (insertBefore === null) {
+ insertBefore = _container.firstChild;
+ }
+
+ _container.insertBefore(wrapper, insertBefore);
+
_buttons.set(buttonName, button);
this._renderContainer();
},
/**
* Returns true if there is a registered button with the provided name.
- *
+ *
* @param {string} buttonName unique identifier
* @return {boolean} true if there is a registered button with this name
*/
/**
* Returns the stored button by name or undefined.
- *
+ *
* @param {string} buttonName unique identifier
* @return {Element} button element or undefined
*/
- get: function(buttonName) {
+ get: function (buttonName) {
return _buttons.get(buttonName);
},
/**
* Removes a button by its button name.
- *
+ *
* @param {string} buttonName unique identifier
*/
- remove: function(buttonName) {
+ remove: function (buttonName) {
var button = _buttons.get(buttonName);
if (button !== undefined) {
var listItem = button.parentNode;
/**
* Hides a button by its button name.
- *
+ *
* @param {string} buttonName unique identifier
*/
- hide: function(buttonName) {
+ hide: function (buttonName) {
var button = _buttons.get(buttonName);
if (button) {
elAttr(button.parentNode, 'aria-hidden', 'true');
/**
* Shows a button by its button name.
- *
+ *
* @param {string} buttonName unique identifier
*/
- show: function(buttonName) {
+ show: function (buttonName) {
var button = _buttons.get(buttonName);
if (button) {
if (button.parentNode.classList.contains('initiallyHidden')) {
/**
* Toggles the container's visibility.
- *
+ *
* @protected
*/
- _renderContainer: function() {
+ _renderContainer: function () {
var hasVisibleItems = false;
if (_container.childElementCount) {
for (var i = 0, length = _container.childElementCount; i < length; i++) {
+++ /dev/null
-/**
- * Provides a link to scroll to top once the page is scrolled by at least 50% the height of the window.
- *
- * @author Alexander Ebert
- * @copyright 2001-2019 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module WoltLabSuite/Core/Ui/Page/JumpToTop
- */
-define(['Environment', 'Language', './Action'], function(Environment, Language, PageAction) {
- "use strict";
-
- /**
- * @constructor
- */
- function JumpToTop() { this.init(); }
- JumpToTop.prototype = {
- /**
- * Initializes the top link for desktop browsers only.
- */
- init: function() {
- // top link is not available on smartphones and tablets (they have a built-in function to accomplish this)
- if (Environment.platform() !== 'desktop') {
- return;
- }
-
- this._callbackScrollEnd = this._afterScroll.bind(this);
- this._timeoutScroll = null;
-
- var button = elCreate('a');
- button.className = 'jsTooltip';
- button.href = '#';
- elAttr(button, 'title', Language.get('wcf.global.scrollUp'));
- elAttr(button, 'aria-hidden', 'true');
- button.innerHTML = '<span class="icon icon32 fa-angle-up"></span>';
-
- button.addEventListener(WCF_CLICK_EVENT, this._jump.bind(this));
-
- PageAction.add('toTop', button);
-
- window.addEventListener('scroll', this._scroll.bind(this));
-
- // invoke callback on page load
- this._afterScroll();
- },
-
- /**
- * Handles clicks on the top link.
- *
- * @param {Event} event event object
- * @protected
- */
- _jump: function(event) {
- event.preventDefault();
-
- elById('top').scrollIntoView({ behavior: 'smooth' });
- },
-
- /**
- * Callback executed whenever the window is being scrolled.
- *
- * @protected
- */
- _scroll: function() {
- if (this._timeoutScroll !== null) {
- window.clearTimeout(this._timeoutScroll);
- }
-
- this._timeoutScroll = window.setTimeout(this._callbackScrollEnd, 100);
- },
-
- /**
- * Delayed callback executed once the page has not been scrolled for a certain amount of time.
- *
- * @protected
- */
- _afterScroll: function() {
- this._timeoutScroll = null;
-
- PageAction[(window.pageYOffset >= 300) ? 'show' : 'hide']('toTop');
- }
- };
-
- return JumpToTop;
-});
@keyframes wcfPageAction {
- 0% { visibility: visible; transform: translateY(-10px); opacity: 0; }
- 100% { visibility: visible; transform: translateY(0); opacity: 1; }
+ 0% { visibility: visible; opacity: 0; }
+ 100% { visibility: visible; opacity: 1; }
}
@keyframes wcfPageActionOut {
- 0% { visibility: visible; transform: translateY(0); opacity: 1; }
- 100% { visibility: hidden; transform: translateY(-10px); opacity: 0; }
+ 0% { visibility: visible; opacity: 1; }
+ 100% { visibility: hidden; opacity: 0; }
}
@keyframes wcfPageActionRemove {
- 0% { visibility: visible; transform: translateY(0); opacity: 1; }
- 60% { visibility: hidden; transform: translateY(-10px); opacity: 0; }
- 100% { visibility: hidden; transform: translateY(-10px); opacity: 0; max-width: 0; }
+ 0% { visibility: visible; opacity: 1; }
+ 60% { visibility: hidden; opacity: 0; }
+ 100% { visibility: hidden; opacity: 0; max-width: 0; }
}
.pageAction {
bottom: 10px;
+ display: flex;
+ justify-content: flex-end;
+ left: 10px;
+ pointer-events: none;
position: fixed;
right: 10px;
z-index: 400;
- @include inlineList;
+ .pageActionButtons {
+ display: flex;
+ flex: 0 auto;
+ overflow: auto;
+ }
+
+ .pageActionButtons,
+ .pageActionButtonToTop {
+ pointer-events: all;
+ }
- > li {
- animation: wcfPageActionOut .3s;
- animation-fill-mode: forwards;
+ .pageActionButton {
display: flex;
+ flex: 0 0 auto;
// required to animate 'max-width' properly when removing items
max-width: 400px;
white-space: nowrap;
- &[aria-hidden="false"] {
- animation: wcfPageAction .3s;
- animation-fill-mode: forwards;
+ &:not(:first-child) {
+ margin-left: 5px;
}
&.remove {
- animation: wcfPageActionRemove .5s;
- animation-fill-mode: forwards;
+ animation: wcfPageActionRemove .48s;
+ }
+ }
+
+ .pageActionButton,
+ .pageActionButtonToTop {
+ animation: wcfPageActionOut .24s forwards;
+
+ &[aria-hidden="false"] {
+ animation: wcfPageAction .24s;
}
&.initiallyHidden {
animation: 0;
visibility: hidden;
}
-
- &.toTop > a {
- padding: 2px;
- }
}
- @include screen-xs {
- flex-wrap: nowrap;
- left: 10px;
+ .pageActionButtonToTop {
+ align-self: flex-start;
+ flex: 0 0 auto;
+ margin-left: 5px;
+ padding: 2px;
+ }
+
+ @include screen-sm-down {
+ /* The iOS bottom touch zone is approximately 44px high. Any touches within will show the
+ menu instead of registering any touch on a button. */
+ bottom: 44px;
+
+ opacity: 1;
+ transition: opacity .12s linear;
- > li {
- flex: 1 1 auto;
- max-width: none;
- overflow: hidden;
-
- > a {
- overflow: hidden;
- text-align: center;
- text-overflow: ellipsis;
- width: 100%;
- }
+ &.scrolledDown {
+ opacity: 0;
+ transition-delay: .4s;
}
}
}