define(["require", "exports"], function (require, exports) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
- exports.stringToBool = exports.getStoragePrefix = exports.triggerEvent = exports.serialize = exports.getUuid = exports.getType = exports.isPlainObject = exports.inherit = exports.extend = exports.convertLegacyUrl = exports.clone = void 0;
+ exports.debounce = exports.stringToBool = exports.getStoragePrefix = exports.triggerEvent = exports.serialize = exports.getUuid = exports.getType = exports.isPlainObject = exports.inherit = exports.extend = exports.convertLegacyUrl = exports.clone = void 0;
const _clone = function (variable) {
if (typeof variable === 'object' && (Array.isArray(variable) || isPlainObject(variable))) {
return _cloneObject(variable);
return value === '1' || value === 'true';
}
exports.stringToBool = stringToBool;
+ /**
+ * A function that emits a side effect and does not return anything.
+ *
+ * @see https://github.com/chodorowicz/ts-debounce/blob/62f30f2c3379b7b5e778fb1793e1fbfa17354894/src/index.ts
+ */
+ function debounce(func, waitMilliseconds = 50, options = {
+ isImmediate: false,
+ }) {
+ let timeoutId;
+ return function (...args) {
+ const context = this;
+ const doLater = function () {
+ timeoutId = undefined;
+ if (!options.isImmediate) {
+ func.apply(context, args);
+ }
+ };
+ const shouldCallNow = options.isImmediate && timeoutId === undefined;
+ if (timeoutId !== undefined) {
+ clearTimeout(timeoutId);
+ }
+ timeoutId = setTimeout(doLater, waitMilliseconds);
+ if (shouldCallNow) {
+ func.apply(context, args);
+ }
+ };
+ }
+ exports.debounce = debounce;
});
/**
* Provides page actions such as "jump to top" and clipboard actions.
*
- * @author Alexander Ebert
- * @copyright 2001-2020 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module WoltLabSuite/Core/Ui/Page/Action
+ * @author Alexander Ebert
+ * @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', 'Language'], function (Dictionary, Language) {
- 'use strict';
- var _buttons = new Dictionary();
- /** @var {Element} */
- var _container;
- var _didInit = false;
- var _lastPosition = -1;
- /** @var {Element} */
- var _toTopButton;
- /** @var {Element} */
- var _wrapper;
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
+}) : (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+ o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || function (mod) {
+ if (mod && mod.__esModule) return mod;
+ var result = {};
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
+ __setModuleDefault(result, mod);
+ return result;
+};
+define(["require", "exports", "../../Core", "../../Language"], function (require, exports, Core, Language) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.show = exports.hide = exports.remove = exports.get = exports.has = exports.add = exports.setup = void 0;
+ Core = __importStar(Core);
+ Language = __importStar(Language);
+ const _buttons = new Map();
+ let _container;
+ let _didInit = false;
+ let _lastPosition = -1;
+ let _toTopButton;
+ let _wrapper;
+ function buildToTopButton() {
+ const button = document.createElement('a');
+ button.className = 'button buttonPrimary pageActionButtonToTop initiallyHidden jsTooltip';
+ button.href = '';
+ button.title = Language.get('wcf.global.scrollUp');
+ button.setAttribute('aria-hidden', 'true');
+ button.innerHTML = '<span class="icon icon32 fa-angle-up"></span>';
+ button.addEventListener('click', scrollToTop);
+ return button;
+ }
+ function onScroll() {
+ if (document.documentElement.classList.contains('disableScrolling')) {
+ // Ignore any scroll events that take place while body scrolling is disabled,
+ // because it messes up the scroll offsets.
+ return;
+ }
+ const offset = window.pageYOffset;
+ if (offset === _lastPosition) {
+ // Ignore any scroll event that is fired but without a position change. This can
+ // happen after closing a dialog that prevented the body from being scrolled.
+ return;
+ }
+ if (offset >= 300) {
+ if (_toTopButton.classList.contains('initiallyHidden')) {
+ _toTopButton.classList.remove('initiallyHidden');
+ }
+ _toTopButton.setAttribute('aria-hidden', 'false');
+ }
+ else {
+ _toTopButton.setAttribute('aria-hidden', 'true');
+ }
+ renderContainer();
+ if (_lastPosition !== -1) {
+ _wrapper.classList[offset < _lastPosition ? 'remove' : 'add']('scrolledDown');
+ }
+ _lastPosition = offset;
+ }
+ function scrollToTop(event) {
+ event.preventDefault();
+ const topAnchor = document.getElementById('top');
+ topAnchor.scrollIntoView({ behavior: 'smooth' });
+ }
/**
- * @exports WoltLabSuite/Core/Ui/Page/Action
+ * Toggles the container's visibility.
*/
- return {
- /**
- * Initializes the page action container.
- */
- setup: function () {
- if (_didInit) {
- return;
- }
- _didInit = true;
- _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) {
- if (document.documentElement.classList.contains('disableScrolling')) {
- // Ignore any scroll events that take place while body scrolling is disabled,
- // because it messes up the scroll offsets.
- return;
- }
- var offset = window.pageYOffset;
- if (offset === _lastPosition) {
- // Ignore any scroll event that is fired but without a position change. This can
- // happen after closing a dialog that prevented the body from being scrolled.
- return;
- }
- 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) {
- this.setup();
- // 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', 'true');
- button.classList.add('button');
- button.classList.add('buttonPrimary');
- 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;
+ function renderContainer() {
+ const visibleChild = Array.from(_container.children).find(element => {
+ return element.getAttribute('aria-hidden') === 'false';
+ });
+ _container.classList[visibleChild ? 'add' : 'remove']('active');
+ }
+ /**
+ * Initializes the page action container.
+ */
+ function setup() {
+ if (_didInit) {
+ return;
+ }
+ _didInit = true;
+ _wrapper = document.createElement('div');
+ _wrapper.className = 'pageAction';
+ _container = document.createElement('div');
+ _container.className = 'pageActionButtons';
+ _wrapper.appendChild(_container);
+ _toTopButton = buildToTopButton();
+ _wrapper.appendChild(_toTopButton);
+ document.body.appendChild(_wrapper);
+ window.addEventListener('scroll', Core.debounce(onScroll, 100), { passive: true });
+ onScroll();
+ }
+ exports.setup = setup;
+ /**
+ * 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.
+ */
+ function add(buttonName, button, insertBeforeButton) {
+ setup();
+ // 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.
+ const wrapper = document.createElement('div');
+ wrapper.className = 'pageActionButton';
+ wrapper.dataset.name = buttonName;
+ wrapper.setAttribute('aria-hidden', 'true');
+ button.classList.add('button');
+ button.classList.add('buttonPrimary');
+ wrapper.appendChild(button);
+ let insertBefore = null;
+ if (insertBeforeButton) {
+ insertBefore = _buttons.get(insertBeforeButton) || null;
+ if (insertBefore) {
+ insertBefore = insertBefore.parentElement;
}
- _container.insertBefore(wrapper, insertBefore);
- _wrapper.classList.remove('scrolledDown');
- _buttons.set(buttonName, button);
- // Query a layout related property to force a reflow, otherwise the transition is optimized away.
- // noinspection BadExpressionStatementJS
- wrapper.offsetParent;
- // Toggle the visibility to force the transition to be applied.
- elAttr(wrapper, 'aria-hidden', 'false');
- 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
- */
- has: function (buttonName) {
- return _buttons.has(buttonName);
- },
- /**
- * Returns the stored button by name or undefined.
- *
- * @param {string} buttonName unique identifier
- * @return {Element} button element or undefined
- */
- get: function (buttonName) {
- return _buttons.get(buttonName);
- },
- /**
- * Removes a button by its button name.
- *
- * @param {string} buttonName unique identifier
- */
- remove: function (buttonName) {
- var button = _buttons.get(buttonName);
- if (button !== undefined) {
- var listItem = button.parentNode;
- var callback = function () {
- try {
- if (elAttrBool(listItem, 'aria-hidden')) {
- _container.removeChild(listItem);
- _buttons.delete(buttonName);
- }
- listItem.removeEventListener('transitionend', callback);
- }
- catch (e) {
- // ignore errors if the element has already been removed
+ }
+ if (!insertBefore && _container.childElementCount) {
+ insertBefore = _container.children[0];
+ }
+ if (!insertBefore) {
+ insertBefore = _container.firstChild;
+ }
+ _container.insertBefore(wrapper, insertBefore);
+ _wrapper.classList.remove('scrolledDown');
+ _buttons.set(buttonName, button);
+ // Query a layout related property to force a reflow, otherwise the transition is optimized away.
+ // noinspection BadExpressionStatementJS
+ wrapper.offsetParent;
+ // Toggle the visibility to force the transition to be applied.
+ wrapper.setAttribute('aria-hidden', 'false');
+ renderContainer();
+ }
+ exports.add = add;
+ /**
+ * Returns true if there is a registered button with the provided name.
+ */
+ function has(buttonName) {
+ return _buttons.has(buttonName);
+ }
+ exports.has = has;
+ /**
+ * Returns the stored button by name or undefined.
+ */
+ function get(buttonName) {
+ return _buttons.get(buttonName);
+ }
+ exports.get = get;
+ /**
+ * Removes a button by its button name.
+ */
+ function remove(buttonName) {
+ const button = _buttons.get(buttonName);
+ if (button !== undefined) {
+ const listItem = button.parentElement;
+ const callback = () => {
+ try {
+ if (Core.stringToBool(listItem.getAttribute('aria-hidden'))) {
+ _container.removeChild(listItem);
+ _buttons.delete(buttonName);
}
- };
- listItem.addEventListener('transitionend', callback);
- this.hide(buttonName);
- }
- },
- /**
- * Hides a button by its button name.
- *
- * @param {string} buttonName unique identifier
- */
- hide: function (buttonName) {
- var button = _buttons.get(buttonName);
- if (button) {
- elAttr(button.parentNode, 'aria-hidden', 'true');
- this._renderContainer();
- }
- },
- /**
- * Shows a button by its button name.
- *
- * @param {string} buttonName unique identifier
- */
- show: function (buttonName) {
- var button = _buttons.get(buttonName);
- if (button) {
- if (button.parentNode.classList.contains('initiallyHidden')) {
- button.parentNode.classList.remove('initiallyHidden');
+ listItem.removeEventListener('transitionend', callback);
}
- elAttr(button.parentNode, 'aria-hidden', 'false');
- _wrapper.classList.remove('scrolledDown');
- this._renderContainer();
- }
- },
- /**
- * Toggles the container's visibility.
- *
- * @protected
- */
- _renderContainer: function () {
- var hasVisibleItems = false;
- if (_container.childElementCount) {
- for (var i = 0, length = _container.childElementCount; i < length; i++) {
- if (elAttr(_container.children[i], 'aria-hidden') === 'false') {
- hasVisibleItems = true;
- break;
- }
+ catch (e) {
+ // ignore errors if the element has already been removed
}
+ };
+ listItem.addEventListener('transitionend', callback);
+ hide(buttonName);
+ }
+ }
+ exports.remove = remove;
+ /**
+ * Hides a button by its button name.
+ */
+ function hide(buttonName) {
+ const button = _buttons.get(buttonName);
+ if (button) {
+ const parent = button.parentElement;
+ parent.setAttribute('aria-hidden', 'true');
+ renderContainer();
+ }
+ }
+ exports.hide = hide;
+ /**
+ * Shows a button by its button name.
+ */
+ function show(buttonName) {
+ const button = _buttons.get(buttonName);
+ if (button) {
+ const parent = button.parentElement;
+ if (parent.classList.contains('initiallyHidden')) {
+ parent.classList.remove('initiallyHidden');
}
- _container.classList[(hasVisibleItems ? 'add' : 'remove')]('active');
+ parent.setAttribute('aria-hidden', 'false');
+ _wrapper.classList.remove('scrolledDown');
+ renderContainer();
}
- };
+ }
+ exports.show = show;
});
}
/**
- * Interprets a string value as a boolean value similar to the behavior of the
+ * Interprets a string value as a boolean value similar to the behavior of the
* legacy functions `elAttrBool()` and `elDataBool()`.
*/
export function stringToBool(value: string | null): boolean {
return value === '1' || value === 'true';
}
+
+
+type DebounceCallback = (...args: any[]) => void;
+
+interface DebounceOptions {
+ isImmediate: boolean;
+}
+
+/**
+ * A function that emits a side effect and does not return anything.
+ *
+ * @see https://github.com/chodorowicz/ts-debounce/blob/62f30f2c3379b7b5e778fb1793e1fbfa17354894/src/index.ts
+ */
+export function debounce<F extends DebounceCallback>(
+ func: F,
+ waitMilliseconds = 50,
+ options: DebounceOptions = {
+ isImmediate: false,
+ },
+): (this: ThisParameterType<F>, ...args: Parameters<F>) => void {
+ let timeoutId: ReturnType<typeof setTimeout> | undefined;
+
+ return function (this: ThisParameterType<F>, ...args: Parameters<F>) {
+ const context = this;
+
+ const doLater = function () {
+ timeoutId = undefined;
+ if (!options.isImmediate) {
+ func.apply(context, args);
+ }
+ };
+
+ const shouldCallNow = options.isImmediate && timeoutId === undefined;
+
+ if (timeoutId !== undefined) {
+ clearTimeout(timeoutId);
+ }
+
+ timeoutId = setTimeout(doLater, waitMilliseconds);
+
+ if (shouldCallNow) {
+ func.apply(context, args);
+ }
+ };
+}
+++ /dev/null
-/**
- * Provides page actions such as "jump to top" and clipboard actions.
- *
- * @author Alexander Ebert
- * @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', 'Language'], function (Dictionary, Language) {
- 'use strict';
-
- var _buttons = new Dictionary();
-
- /** @var {Element} */
- var _container;
-
- var _didInit = false;
-
- var _lastPosition = -1;
-
- /** @var {Element} */
- var _toTopButton;
-
- /** @var {Element} */
- var _wrapper;
-
- /**
- * @exports WoltLabSuite/Core/Ui/Page/Action
- */
- return {
- /**
- * Initializes the page action container.
- */
- setup: function () {
- if (_didInit) {
- return;
- }
-
- _didInit = true;
-
- _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) {
- if (document.documentElement.classList.contains('disableScrolling')) {
- // Ignore any scroll events that take place while body scrolling is disabled,
- // because it messes up the scroll offsets.
- return;
- }
-
- var offset = window.pageYOffset;
- if (offset === _lastPosition) {
- // Ignore any scroll event that is fired but without a position change. This can
- // happen after closing a dialog that prevented the body from being scrolled.
- return;
- }
-
- 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) {
- this.setup();
-
- // 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', 'true');
-
- button.classList.add('button');
- button.classList.add('buttonPrimary');
- 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);
- _wrapper.classList.remove('scrolledDown');
-
- _buttons.set(buttonName, button);
-
- // Query a layout related property to force a reflow, otherwise the transition is optimized away.
- // noinspection BadExpressionStatementJS
- wrapper.offsetParent;
-
- // Toggle the visibility to force the transition to be applied.
- elAttr(wrapper, 'aria-hidden', 'false');
-
- 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
- */
- has: function (buttonName) {
- return _buttons.has(buttonName);
- },
-
- /**
- * Returns the stored button by name or undefined.
- *
- * @param {string} buttonName unique identifier
- * @return {Element} button element or undefined
- */
- get: function (buttonName) {
- return _buttons.get(buttonName);
- },
-
- /**
- * Removes a button by its button name.
- *
- * @param {string} buttonName unique identifier
- */
- remove: function (buttonName) {
- var button = _buttons.get(buttonName);
- if (button !== undefined) {
- var listItem = button.parentNode;
- var callback = function () {
- try {
- if (elAttrBool(listItem, 'aria-hidden')) {
- _container.removeChild(listItem);
- _buttons.delete(buttonName);
- }
-
- listItem.removeEventListener('transitionend', callback);
- }
- catch (e) {
- // ignore errors if the element has already been removed
- }
- };
-
- listItem.addEventListener('transitionend', callback);
-
- this.hide(buttonName);
- }
- },
-
- /**
- * Hides a button by its button name.
- *
- * @param {string} buttonName unique identifier
- */
- hide: function (buttonName) {
- var button = _buttons.get(buttonName);
- if (button) {
- elAttr(button.parentNode, 'aria-hidden', 'true');
- this._renderContainer();
- }
- },
-
- /**
- * Shows a button by its button name.
- *
- * @param {string} buttonName unique identifier
- */
- show: function (buttonName) {
- var button = _buttons.get(buttonName);
- if (button) {
- if (button.parentNode.classList.contains('initiallyHidden')) {
- button.parentNode.classList.remove('initiallyHidden');
- }
-
- elAttr(button.parentNode, 'aria-hidden', 'false');
- _wrapper.classList.remove('scrolledDown');
- this._renderContainer();
- }
- },
-
- /**
- * Toggles the container's visibility.
- *
- * @protected
- */
- _renderContainer: function () {
- var hasVisibleItems = false;
- if (_container.childElementCount) {
- for (var i = 0, length = _container.childElementCount; i < length; i++) {
- if (elAttr(_container.children[i], 'aria-hidden') === 'false') {
- hasVisibleItems = true;
- break;
- }
- }
- }
-
- _container.classList[(hasVisibleItems ? 'add' : 'remove')]('active');
- }
- };
-});
--- /dev/null
+/**
+ * Provides page actions such as "jump to top" and clipboard actions.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2020 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @module WoltLabSuite/Core/Ui/Page/Action
+ */
+
+import * as Core from '../../Core';
+import * as Language from '../../Language';
+
+const _buttons = new Map<string, HTMLElement>();
+
+let _container: HTMLElement;
+let _didInit = false;
+let _lastPosition = -1;
+let _toTopButton: HTMLElement;
+let _wrapper: HTMLElement;
+
+function buildToTopButton(): HTMLAnchorElement {
+ const button = document.createElement('a');
+ button.className = 'button buttonPrimary pageActionButtonToTop initiallyHidden jsTooltip';
+ button.href = '';
+ button.title = Language.get('wcf.global.scrollUp');
+ button.setAttribute('aria-hidden', 'true');
+ button.innerHTML = '<span class="icon icon32 fa-angle-up"></span>';
+
+ button.addEventListener('click', scrollToTop);
+
+ return button;
+}
+
+function onScroll(): void {
+ if (document.documentElement.classList.contains('disableScrolling')) {
+ // Ignore any scroll events that take place while body scrolling is disabled,
+ // because it messes up the scroll offsets.
+ return;
+ }
+
+ const offset = window.pageYOffset;
+ if (offset === _lastPosition) {
+ // Ignore any scroll event that is fired but without a position change. This can
+ // happen after closing a dialog that prevented the body from being scrolled.
+ return;
+ }
+
+ if (offset >= 300) {
+ if (_toTopButton.classList.contains('initiallyHidden')) {
+ _toTopButton.classList.remove('initiallyHidden');
+ }
+
+ _toTopButton.setAttribute('aria-hidden', 'false');
+ } else {
+ _toTopButton.setAttribute('aria-hidden', 'true');
+ }
+
+ renderContainer();
+
+ if (_lastPosition !== -1) {
+ _wrapper.classList[offset < _lastPosition ? 'remove' : 'add']('scrolledDown');
+ }
+
+ _lastPosition = offset;
+}
+
+function scrollToTop(event: MouseEvent): void {
+ event.preventDefault();
+
+ const topAnchor = document.getElementById('top')!;
+ topAnchor.scrollIntoView({behavior: 'smooth'});
+}
+
+/**
+ * Toggles the container's visibility.
+ */
+function renderContainer() {
+ const visibleChild = Array.from(_container.children).find(element => {
+ return element.getAttribute('aria-hidden') === 'false';
+ });
+
+ _container.classList[visibleChild ? 'add' : 'remove']('active');
+}
+
+/**
+ * Initializes the page action container.
+ */
+export function setup() {
+ if (_didInit) {
+ return;
+ }
+
+ _didInit = true;
+
+ _wrapper = document.createElement('div');
+ _wrapper.className = 'pageAction';
+
+ _container = document.createElement('div');
+ _container.className = 'pageActionButtons';
+ _wrapper.appendChild(_container);
+
+ _toTopButton = buildToTopButton();
+ _wrapper.appendChild(_toTopButton);
+
+ document.body.appendChild(_wrapper);
+
+ window.addEventListener(
+ 'scroll',
+ Core.debounce(onScroll, 100),
+ {passive: true},
+ );
+
+ onScroll();
+}
+
+/**
+ * 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.
+ */
+export function add(buttonName: string, button: HTMLElement, insertBeforeButton?: string) {
+ setup();
+
+ // 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.
+ const wrapper = document.createElement('div');
+ wrapper.className = 'pageActionButton';
+ wrapper.dataset.name = buttonName;
+ wrapper.setAttribute('aria-hidden', 'true');
+
+ button.classList.add('button');
+ button.classList.add('buttonPrimary');
+ wrapper.appendChild(button);
+
+ let insertBefore: HTMLElement | null = null;
+ if (insertBeforeButton) {
+ insertBefore = _buttons.get(insertBeforeButton) || null;
+ if (insertBefore) {
+ insertBefore = insertBefore.parentElement;
+ }
+ }
+
+ if (!insertBefore && _container.childElementCount) {
+ insertBefore = _container.children[0] as HTMLElement;
+ }
+ if (!insertBefore) {
+ insertBefore = _container.firstChild as HTMLElement;
+ }
+
+ _container.insertBefore(wrapper, insertBefore);
+ _wrapper.classList.remove('scrolledDown');
+
+ _buttons.set(buttonName, button);
+
+ // Query a layout related property to force a reflow, otherwise the transition is optimized away.
+ // noinspection BadExpressionStatementJS
+ wrapper.offsetParent;
+
+ // Toggle the visibility to force the transition to be applied.
+ wrapper.setAttribute('aria-hidden', 'false');
+
+ renderContainer();
+}
+
+/**
+ * Returns true if there is a registered button with the provided name.
+ */
+export function has(buttonName: string): boolean {
+ return _buttons.has(buttonName);
+}
+
+/**
+ * Returns the stored button by name or undefined.
+ */
+export function get(buttonName: string): HTMLElement | undefined {
+ return _buttons.get(buttonName);
+}
+
+/**
+ * Removes a button by its button name.
+ */
+export function remove(buttonName: string): void {
+ const button = _buttons.get(buttonName);
+ if (button !== undefined) {
+ const listItem = button.parentElement!;
+ const callback = () => {
+ try {
+ if (Core.stringToBool(listItem.getAttribute('aria-hidden'))) {
+ _container.removeChild(listItem);
+ _buttons.delete(buttonName);
+ }
+
+ listItem.removeEventListener('transitionend', callback);
+ } catch (e) {
+ // ignore errors if the element has already been removed
+ }
+ };
+
+ listItem.addEventListener('transitionend', callback);
+
+ hide(buttonName);
+ }
+}
+
+/**
+ * Hides a button by its button name.
+ */
+export function hide(buttonName: string): void {
+ const button = _buttons.get(buttonName);
+ if (button) {
+ const parent = button.parentElement!;
+ parent.setAttribute('aria-hidden', 'true');
+
+ renderContainer();
+ }
+}
+
+/**
+ * Shows a button by its button name.
+ */
+export function show(buttonName: string): void {
+ const button = _buttons.get(buttonName);
+ if (button) {
+ const parent = button.parentElement!;
+ if (parent.classList.contains('initiallyHidden')) {
+ parent.classList.remove('initiallyHidden');
+ }
+
+ parent.setAttribute('aria-hidden', 'false');
+ _wrapper.classList.remove('scrolledDown');
+
+ renderContainer();
+ }
+}
+
+