From 7e967d3467a14e109859179e32f541887fd835ea Mon Sep 17 00:00:00 2001 From: Alexander Ebert Date: Fri, 1 Jan 2021 16:08:05 +0100 Subject: [PATCH] Convert `Controller/Popover` to TypeScript --- .../WoltLabSuite/Core/Controller/Popover.js | 446 ++++++++-------- .../files/ts/WoltLabSuite/Core/Ajax/Data.ts | 1 + .../WoltLabSuite/Core/Controller/Popover.js | 425 ---------------- .../WoltLabSuite/Core/Controller/Popover.ts | 475 ++++++++++++++++++ 4 files changed, 706 insertions(+), 641 deletions(-) delete mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Popover.js create mode 100644 wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Popover.ts diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Popover.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Popover.js index bc15086aac..2ef5288302 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Popover.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Controller/Popover.js @@ -1,70 +1,57 @@ /** * Versatile popover manager. * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @module WoltLabSuite/Core/Controller/Popover + * @author Alexander Ebert + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Controller/Popover */ -define(['Ajax', 'Dictionary', 'Environment', 'Dom/ChangeListener', 'Dom/Util', 'Ui/Alignment'], function (Ajax, Dictionary, Environment, DomChangeListener, DomUtil, UiAlignment) { +define(["require", "exports", "tslib", "../Ajax", "../Dom/Change/Listener", "../Dom/Util", "../Environment", "../Ui/Alignment"], function (require, exports, tslib_1, Ajax, Listener_1, Util_1, Environment, UiAlignment) { "use strict"; - var _activeId = null; - var _cache = new Dictionary(); - var _elements = new Dictionary(); - var _handlers = new Dictionary(); - var _hoverId = null; - var _suspended = false; - var _timeoutEnter = null; - var _timeoutLeave = null; - var _popover = null; - var _popoverContent = null; - var _callbackClick = null; - var _callbackHide = null; - var _callbackMouseEnter = null; - var _callbackMouseLeave = null; - /** @const */ var STATE_NONE = 0; - /** @const */ var STATE_LOADING = 1; - /** @const */ var STATE_READY = 2; - /** @const */ var DELAY_HIDE = 500; - /** @const */ var DELAY_SHOW = 800; - /** - * @exports WoltLabSuite/Core/Controller/Popover - */ - return { + Object.defineProperty(exports, "__esModule", { value: true }); + exports.ajaxApi = exports.setContent = exports.init = void 0; + Ajax = tslib_1.__importStar(Ajax); + Listener_1 = tslib_1.__importDefault(Listener_1); + Util_1 = tslib_1.__importDefault(Util_1); + Environment = tslib_1.__importStar(Environment); + UiAlignment = tslib_1.__importStar(UiAlignment); + class ControllerPopover { /** * Builds popover DOM elements and binds event listeners. */ - _setup: function () { - if (_popover !== null) { - return; - } - _popover = elCreate('div'); - _popover.className = 'popover forceHide'; - _popoverContent = elCreate('div'); - _popoverContent.className = 'popoverContent'; - _popover.appendChild(_popoverContent); - var pointer = elCreate('span'); - pointer.className = 'elementPointer'; - pointer.appendChild(elCreate('span')); - _popover.appendChild(pointer); - document.body.appendChild(_popover); - // static binding for callbacks (they don't change anyway and binding each time is expensive) - _callbackClick = this._hide.bind(this); - _callbackMouseEnter = this._mouseEnter.bind(this); - _callbackMouseLeave = this._mouseLeave.bind(this); + constructor() { + this.activeId = ""; + this.cache = new Map(); + this.elements = new Map(); + this.handlers = new Map(); + this.hoverId = ""; + this.suspended = false; + this.timerEnter = undefined; + this.timerLeave = undefined; + this.popover = document.createElement("div"); + this.popover.className = "popover forceHide"; + this.popoverContent = document.createElement("div"); + this.popoverContent.className = "popoverContent"; + this.popover.appendChild(this.popoverContent); + const pointer = document.createElement("span"); + pointer.className = "elementPointer"; + pointer.appendChild(document.createElement("span")); + this.popover.appendChild(pointer); + document.body.appendChild(this.popover); // event listener - _popover.addEventListener('mouseenter', this._popoverMouseEnter.bind(this)); - _popover.addEventListener('mouseleave', _callbackMouseLeave); - _popover.addEventListener('animationend', this._clearContent.bind(this)); - window.addEventListener('beforeunload', (function () { - _suspended = true; - if (_timeoutEnter !== null) { - window.clearTimeout(_timeoutEnter); + this.popover.addEventListener("mouseenter", this.popoverMouseEnter.bind(this)); + this.popover.addEventListener("mouseleave", () => this.mouseLeave()); + this.popover.addEventListener("animationend", this.clearContent.bind(this)); + window.addEventListener("beforeunload", () => { + this.suspended = true; + if (this.timerEnter) { + window.clearTimeout(this.timerEnter); + this.timerEnter = undefined; } - this._hide(true); - }).bind(this)); - DomChangeListener.add('WoltLabSuite/Core/Controller/Popover', this._init.bind(this)); - }, + this.hidePopover(); + }); + Listener_1.default.add("WoltLabSuite/Core/Controller/Popover", (identifier) => this.initHandler(identifier)); + } /** * Initializes a popover handler. * @@ -74,278 +61,305 @@ define(['Ajax', 'Dictionary', 'Environment', 'Dom/ChangeListener', 'Dom/Util', ' * attributeName: 'data-object-id', * className: 'fooLink', * identifier: 'com.example.bar.foo', - * loadCallback: function(objectId, popover) { + * loadCallback: (objectId, popover) => { * // request data for object id (e.g. via WoltLabSuite/Core/Ajax) * * // then call this to set the content * popover.setContent('com.example.bar.foo', objectId, htmlTemplateString); * } * }); - * - * @param {Object} options handler options */ - init: function (options) { - if (Environment.platform() !== 'desktop') { + init(options) { + if (Environment.platform() !== "desktop") { return; } - options.attributeName = options.attributeName || 'data-object-id'; - options.legacy = (options.legacy === true); - this._setup(); - if (_handlers.has(options.identifier)) { + options.attributeName = options.attributeName || "data-object-id"; + options.legacy = options.legacy === true; + if (this.handlers.has(options.identifier)) { return; } - _handlers.set(options.identifier, { + // Legacy implementations provided a selector for `className`. + const selector = options.legacy ? options.className : `.${options.className}`; + this.handlers.set(options.identifier, { attributeName: options.attributeName, dboAction: options.dboAction, - elements: options.legacy ? options.className : elByClass(options.className), legacy: options.legacy, - loadCallback: options.loadCallback + loadCallback: options.loadCallback, + selector, }); - this._init(options.identifier); - }, + this.initHandler(options.identifier); + } /** * Initializes a popover handler. - * - * @param {string} identifier handler identifier */ - _init: function (identifier) { - if (typeof identifier === 'string' && identifier.length) { - this._initElements(_handlers.get(identifier), identifier); + initHandler(identifier) { + if (typeof identifier === "string" && identifier.length) { + this.initElements(this.handlers.get(identifier), identifier); } else { - _handlers.forEach(this._initElements.bind(this)); + this.handlers.forEach((value, key) => { + this.initElements(value, key); + }); } - }, + } /** * Binds event listeners for popover-enabled elements. - * - * @param {Object} options handler options - * @param {string} identifier handler identifier */ - _initElements: function (options, identifier) { - var elements = options.legacy ? elBySelAll(options.elements) : options.elements; - for (var i = 0, length = elements.length; i < length; i++) { - var element = elements[i]; - var id = DomUtil.identify(element); - if (_cache.has(id)) { + initElements(options, identifier) { + document.querySelectorAll(options.selector).forEach((element) => { + const id = Util_1.default.identify(element); + if (this.cache.has(id)) { return; } - // skip if element is in a popover - if (element.closest('.popover') !== null) { - _cache.set(id, { + // Skip elements that are located inside a popover. + if (element.closest(".popover") !== null) { + this.cache.set(id, { content: null, - state: STATE_NONE + state: 0 /* None */, }); return; } - var objectId = (options.legacy) ? id : ~~element.getAttribute(options.attributeName); + const objectId = options.legacy ? id : ~~element.getAttribute(options.attributeName); if (objectId === 0) { - continue; + return; } - element.addEventListener('mouseenter', _callbackMouseEnter); - element.addEventListener('mouseleave', _callbackMouseLeave); - if (element.nodeName === 'A' && elAttr(element, 'href')) { - element.addEventListener('click', _callbackClick); + element.addEventListener("mouseenter", (ev) => this.mouseEnter(ev)); + element.addEventListener("mouseleave", () => this.mouseLeave()); + if (element instanceof HTMLAnchorElement && element.href) { + element.addEventListener("click", () => this.hidePopover()); } - var cacheId = identifier + "-" + objectId; - elData(element, 'cache-id', cacheId); - _elements.set(id, { - element: element, - identifier: identifier, - objectId: objectId + const cacheId = `${identifier}-${objectId}`; + element.dataset.cacheId = cacheId; + this.elements.set(id, { + element, + identifier, + objectId: objectId.toString(), }); - if (!_cache.has(cacheId)) { - _cache.set(identifier + "-" + objectId, { + if (!this.cache.has(cacheId)) { + this.cache.set(cacheId, { content: null, - state: STATE_NONE + state: 0 /* None */, }); } - } - }, + }); + } /** * Sets the content for given identifier and object id. - * - * @param {string} identifier handler identifier - * @param {int} objectId object id - * @param {string} content HTML string */ - setContent: function (identifier, objectId, content) { - var cacheId = identifier + "-" + objectId; - var data = _cache.get(cacheId); + setContent(identifier, objectId, content) { + const cacheId = `${identifier}-${objectId}`; + const data = this.cache.get(cacheId); if (data === undefined) { - throw new Error("Unable to find element for object id '" + objectId + "' (identifier: '" + identifier + "')."); + throw new Error(`Unable to find element for object id '${objectId}' (identifier: '${identifier}').`); + } + let fragment = Util_1.default.createFragmentFromHtml(content); + if (!fragment.childElementCount) { + fragment = Util_1.default.createFragmentFromHtml("

" + content + "

"); } - var fragment = DomUtil.createFragmentFromHtml(content); - if (!fragment.childElementCount) - fragment = DomUtil.createFragmentFromHtml('

' + content + '

'); data.content = fragment; - data.state = STATE_READY; - if (_activeId) { - var activeElement = _elements.get(_activeId).element; - if (elData(activeElement, 'cache-id') === cacheId) { - this._show(); + data.state = 2 /* Ready */; + if (this.activeId) { + const activeElement = this.elements.get(this.activeId).element; + if (activeElement.dataset.cacheId === cacheId) { + this.show(); } } - }, + } /** * Handles the mouse start hovering the popover-enabled element. - * - * @param {object} event event object */ - _mouseEnter: function (event) { - if (_suspended) { + mouseEnter(event) { + if (this.suspended) { return; } - if (_timeoutEnter !== null) { - window.clearTimeout(_timeoutEnter); - _timeoutEnter = null; + if (this.timerEnter) { + window.clearTimeout(this.timerEnter); + this.timerEnter = undefined; } - var id = DomUtil.identify(event.currentTarget); - if (_activeId === id && _timeoutLeave !== null) { - window.clearTimeout(_timeoutLeave); - _timeoutLeave = null; + const id = Util_1.default.identify(event.currentTarget); + if (this.activeId === id && this.timerLeave) { + window.clearTimeout(this.timerLeave); + this.timerLeave = undefined; } - _hoverId = id; - _timeoutEnter = window.setTimeout((function () { - _timeoutEnter = null; - if (_hoverId === id) { - this._show(); + this.hoverId = id; + this.timerEnter = window.setTimeout(() => { + this.timerEnter = undefined; + if (this.hoverId === id) { + this.show(); } - }).bind(this), DELAY_SHOW); - }, + }, 800 /* Show */); + } /** * Handles the mouse leaving the popover-enabled element or the popover itself. */ - _mouseLeave: function () { - _hoverId = null; - if (_timeoutLeave !== null) { + mouseLeave() { + this.hoverId = ""; + if (this.timerLeave) { return; } - if (_callbackHide === null) { - _callbackHide = this._hide.bind(this); - } - if (_timeoutLeave !== null) { - window.clearTimeout(_timeoutLeave); - } - _timeoutLeave = window.setTimeout(_callbackHide, DELAY_HIDE); - }, + this.timerLeave = window.setTimeout(() => this.hidePopover(), 500 /* Hide */); + } /** * Handles the mouse start hovering the popover element. */ - _popoverMouseEnter: function () { - if (_timeoutLeave !== null) { - window.clearTimeout(_timeoutLeave); - _timeoutLeave = null; + popoverMouseEnter() { + if (this.timerLeave) { + window.clearTimeout(this.timerLeave); + this.timerLeave = undefined; } - }, + } /** * Shows the popover and loads content on-the-fly. */ - _show: function () { - if (_timeoutLeave !== null) { - window.clearTimeout(_timeoutLeave); - _timeoutLeave = null; + show() { + if (this.timerLeave) { + window.clearTimeout(this.timerLeave); + this.timerLeave = undefined; } - var forceHide = false; - if (_popover.classList.contains('active')) { - if (_activeId !== _hoverId) { - this._hide(); + let forceHide = false; + if (this.popover.classList.contains("active")) { + if (this.activeId !== this.hoverId) { + this.hidePopover(); forceHide = true; } } - else if (_popoverContent.childElementCount) { + else if (this.popoverContent.childElementCount) { forceHide = true; } if (forceHide) { - _popover.classList.add('forceHide'); + this.popover.classList.add("forceHide"); // force layout //noinspection BadExpressionStatementJS - _popover.offsetTop; - this._clearContent(); - _popover.classList.remove('forceHide'); + this.popover.offsetTop; + this.clearContent(); + this.popover.classList.remove("forceHide"); } - _activeId = _hoverId; - var elementData = _elements.get(_activeId); + this.activeId = this.hoverId; + const elementData = this.elements.get(this.activeId); // check if source element is already gone if (elementData === undefined) { return; } - var data = _cache.get(elData(elementData.element, 'cache-id')); - if (data.state === STATE_READY) { - _popoverContent.appendChild(data.content); - this._rebuild(_activeId); + const cacheId = elementData.element.dataset.cacheId; + const data = this.cache.get(cacheId); + if (data.state === 2 /* Ready */) { + this.popoverContent.appendChild(data.content); + this.rebuild(); } - else if (data.state === STATE_NONE) { - data.state = STATE_LOADING; - var handler = _handlers.get(elementData.identifier); + else if (data.state === 0 /* None */) { + data.state = 1 /* Loading */; + const handler = this.handlers.get(elementData.identifier); if (handler.loadCallback) { handler.loadCallback(elementData.objectId, this, elementData.element); } else if (handler.dboAction) { - var callback = function (data) { + const callback = (data) => { this.setContent(elementData.identifier, elementData.objectId, data.returnValues.template); - }.bind(this); + return true; + }; this.ajaxApi({ - actionName: 'getPopover', + actionName: "getPopover", className: handler.dboAction, - interfaceName: 'wcf\\data\\IPopoverAction', - objectIDs: [elementData.objectId] + interfaceName: "wcf\\data\\IPopoverAction", + objectIDs: [elementData.objectId], }, callback, callback); } } - }, + } /** * Hides the popover element. */ - _hide: function () { - if (_timeoutLeave !== null) { - window.clearTimeout(_timeoutLeave); - _timeoutLeave = null; + hidePopover() { + if (this.timerLeave) { + window.clearTimeout(this.timerLeave); + this.timerLeave = undefined; } - _popover.classList.remove('active'); - }, + this.popover.classList.remove("active"); + } /** * Clears popover content by moving it back into the cache. */ - _clearContent: function () { - if (_activeId && _popoverContent.childElementCount && !_popover.classList.contains('active')) { - var activeElData = _cache.get(elData(_elements.get(_activeId).element, 'cache-id')); - while (_popoverContent.childNodes.length) { - activeElData.content.appendChild(_popoverContent.childNodes[0]); + clearContent() { + if (this.activeId && this.popoverContent.childElementCount && !this.popover.classList.contains("active")) { + const cacheId = this.elements.get(this.activeId).element.dataset.cacheId; + const activeElData = this.cache.get(cacheId); + while (this.popoverContent.childNodes.length) { + activeElData.content.appendChild(this.popoverContent.childNodes[0]); } } - }, + } /** * Rebuilds the popover. */ - _rebuild: function () { - if (_popover.classList.contains('active')) { + rebuild() { + if (this.popover.classList.contains("active")) { return; } - _popover.classList.remove('forceHide'); - _popover.classList.add('active'); - UiAlignment.set(_popover, _elements.get(_activeId).element, { + this.popover.classList.remove("forceHide"); + this.popover.classList.add("active"); + UiAlignment.set(this.popover, this.elements.get(this.activeId).element, { pointer: true, - vertical: 'top' + vertical: "top", }); - }, - _ajaxSetup: function () { + } + _ajaxSuccess() { + // This class was designed in a strange way without utilizing this method. + } + _ajaxSetup() { return { - silent: true + silent: true, }; - }, + } /** * Sends an AJAX requests to the server, simple wrapper to reuse the request object. - * - * @param {Object} data request data - * @param {function} success success callback - * @param {function=} failure error callback */ - ajaxApi: function (data, success, failure) { - if (typeof success !== 'function') { + ajaxApi(data, success, failure) { + if (typeof success !== "function") { throw new TypeError("Expected a valid callback for parameter 'success'."); } Ajax.api(this, data, success, failure); } - }; + } + let controllerPopover; + function getControllerPopover() { + if (!controllerPopover) { + controllerPopover = new ControllerPopover(); + } + return controllerPopover; + } + /** + * Initializes a popover handler. + * + * Usage: + * + * ControllerPopover.init({ + * attributeName: 'data-object-id', + * className: 'fooLink', + * identifier: 'com.example.bar.foo', + * loadCallback: function(objectId, popover) { + * // request data for object id (e.g. via WoltLabSuite/Core/Ajax) + * + * // then call this to set the content + * popover.setContent('com.example.bar.foo', objectId, htmlTemplateString); + * } + * }); + */ + function init(options) { + getControllerPopover().init(options); + } + exports.init = init; + /** + * Sets the content for given identifier and object id. + */ + function setContent(identifier, objectId, content) { + getControllerPopover().setContent(identifier, objectId, content); + } + exports.setContent = setContent; + /** + * Sends an AJAX requests to the server, simple wrapper to reuse the request object. + */ + function ajaxApi(data, success, failure) { + getControllerPopover().ajaxApi(data, success, failure); + } + exports.ajaxApi = ajaxApi; }); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ajax/Data.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ajax/Data.ts index e40de105a9..738ccb5764 100644 --- a/wcfsetup/install/files/ts/WoltLabSuite/Core/Ajax/Data.ts +++ b/wcfsetup/install/files/ts/WoltLabSuite/Core/Ajax/Data.ts @@ -28,6 +28,7 @@ export interface DatabaseObjectActionResponse extends ResponseData { | any[]; } +// Return `false` to suppress the error message. export type CallbackFailure = ( data: ResponseData, responseText: string, diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Popover.js b/wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Popover.js deleted file mode 100644 index b6cd6f8e1c..0000000000 --- a/wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Popover.js +++ /dev/null @@ -1,425 +0,0 @@ -/** - * Versatile popover manager. - * - * @author Alexander Ebert - * @copyright 2001-2019 WoltLab GmbH - * @license GNU Lesser General Public License - * @module WoltLabSuite/Core/Controller/Popover - */ -define(['Ajax', 'Dictionary', 'Environment', 'Dom/ChangeListener', 'Dom/Util', 'Ui/Alignment'], function(Ajax, Dictionary, Environment, DomChangeListener, DomUtil, UiAlignment) { - "use strict"; - - var _activeId = null; - var _cache = new Dictionary(); - var _elements = new Dictionary(); - var _handlers = new Dictionary(); - var _hoverId = null; - var _suspended = false; - var _timeoutEnter = null; - var _timeoutLeave = null; - - var _popover = null; - var _popoverContent = null; - - var _callbackClick = null; - var _callbackHide = null; - var _callbackMouseEnter = null; - var _callbackMouseLeave = null; - - /** @const */ var STATE_NONE = 0; - /** @const */ var STATE_LOADING = 1; - /** @const */ var STATE_READY = 2; - - /** @const */ var DELAY_HIDE = 500; - /** @const */ var DELAY_SHOW = 800; - - /** - * @exports WoltLabSuite/Core/Controller/Popover - */ - return { - /** - * Builds popover DOM elements and binds event listeners. - */ - _setup: function() { - if (_popover !== null) { - return; - } - - _popover = elCreate('div'); - _popover.className = 'popover forceHide'; - - _popoverContent = elCreate('div'); - _popoverContent.className = 'popoverContent'; - _popover.appendChild(_popoverContent); - - var pointer = elCreate('span'); - pointer.className = 'elementPointer'; - pointer.appendChild(elCreate('span')); - _popover.appendChild(pointer); - - document.body.appendChild(_popover); - - // static binding for callbacks (they don't change anyway and binding each time is expensive) - _callbackClick = this._hide.bind(this); - _callbackMouseEnter = this._mouseEnter.bind(this); - _callbackMouseLeave = this._mouseLeave.bind(this); - - // event listener - _popover.addEventListener('mouseenter', this._popoverMouseEnter.bind(this)); - _popover.addEventListener('mouseleave', _callbackMouseLeave); - - _popover.addEventListener('animationend', this._clearContent.bind(this)); - - window.addEventListener('beforeunload', (function() { - _suspended = true; - - if (_timeoutEnter !== null) { - window.clearTimeout(_timeoutEnter); - } - - this._hide(true); - }).bind(this)); - - DomChangeListener.add('WoltLabSuite/Core/Controller/Popover', this._init.bind(this)); - }, - - /** - * Initializes a popover handler. - * - * Usage: - * - * ControllerPopover.init({ - * attributeName: 'data-object-id', - * className: 'fooLink', - * identifier: 'com.example.bar.foo', - * loadCallback: function(objectId, popover) { - * // request data for object id (e.g. via WoltLabSuite/Core/Ajax) - * - * // then call this to set the content - * popover.setContent('com.example.bar.foo', objectId, htmlTemplateString); - * } - * }); - * - * @param {Object} options handler options - */ - init: function(options) { - if (Environment.platform() !== 'desktop') { - return; - } - - options.attributeName = options.attributeName || 'data-object-id'; - options.legacy = (options.legacy === true); - - this._setup(); - - if (_handlers.has(options.identifier)) { - return; - } - - _handlers.set(options.identifier, { - attributeName: options.attributeName, - dboAction: options.dboAction, - elements: options.legacy ? options.className : elByClass(options.className), - legacy: options.legacy, - loadCallback: options.loadCallback - }); - - this._init(options.identifier); - }, - - /** - * Initializes a popover handler. - * - * @param {string} identifier handler identifier - */ - _init: function(identifier) { - if (typeof identifier === 'string' && identifier.length) { - this._initElements(_handlers.get(identifier), identifier); - } - else { - _handlers.forEach(this._initElements.bind(this)); - } - }, - - /** - * Binds event listeners for popover-enabled elements. - * - * @param {Object} options handler options - * @param {string} identifier handler identifier - */ - _initElements: function(options, identifier) { - var elements = options.legacy ? elBySelAll(options.elements) : options.elements; - for (var i = 0, length = elements.length; i < length; i++) { - var element = elements[i]; - - var id = DomUtil.identify(element); - if (_cache.has(id)) { - return; - } - // skip if element is in a popover - if (element.closest('.popover') !== null) { - _cache.set(id, { - content: null, - state: STATE_NONE - }); - return; - } - - var objectId = (options.legacy) ? id : ~~element.getAttribute(options.attributeName); - if (objectId === 0) { - continue; - } - - element.addEventListener('mouseenter', _callbackMouseEnter); - element.addEventListener('mouseleave', _callbackMouseLeave); - - if (element.nodeName === 'A' && elAttr(element, 'href')) { - element.addEventListener('click', _callbackClick); - } - - var cacheId = identifier + "-" + objectId; - elData(element, 'cache-id', cacheId); - - _elements.set(id, { - element: element, - identifier: identifier, - objectId: objectId - }); - - if (!_cache.has(cacheId)) { - _cache.set(identifier + "-" + objectId, { - content: null, - state: STATE_NONE - }); - } - } - }, - - /** - * Sets the content for given identifier and object id. - * - * @param {string} identifier handler identifier - * @param {int} objectId object id - * @param {string} content HTML string - */ - setContent: function(identifier, objectId, content) { - var cacheId = identifier + "-" + objectId; - var data = _cache.get(cacheId); - if (data === undefined) { - throw new Error("Unable to find element for object id '" + objectId + "' (identifier: '" + identifier + "')."); - } - - var fragment = DomUtil.createFragmentFromHtml(content); - if (!fragment.childElementCount) fragment = DomUtil.createFragmentFromHtml('

' + content + '

'); - data.content = fragment; - data.state = STATE_READY; - - if (_activeId) { - var activeElement = _elements.get(_activeId).element; - - if (elData(activeElement, 'cache-id') === cacheId) { - this._show(); - } - } - }, - - /** - * Handles the mouse start hovering the popover-enabled element. - * - * @param {object} event event object - */ - _mouseEnter: function(event) { - if (_suspended) { - return; - } - - if (_timeoutEnter !== null) { - window.clearTimeout(_timeoutEnter); - _timeoutEnter = null; - } - - var id = DomUtil.identify(event.currentTarget); - if (_activeId === id && _timeoutLeave !== null) { - window.clearTimeout(_timeoutLeave); - _timeoutLeave = null; - } - - _hoverId = id; - - _timeoutEnter = window.setTimeout((function() { - _timeoutEnter = null; - - if (_hoverId === id) { - this._show(); - } - }).bind(this), DELAY_SHOW); - }, - - /** - * Handles the mouse leaving the popover-enabled element or the popover itself. - */ - _mouseLeave: function() { - _hoverId = null; - - if (_timeoutLeave !== null) { - return; - } - - if (_callbackHide === null) { - _callbackHide = this._hide.bind(this); - } - - if (_timeoutLeave !== null) { - window.clearTimeout(_timeoutLeave); - } - - _timeoutLeave = window.setTimeout(_callbackHide, DELAY_HIDE); - }, - - /** - * Handles the mouse start hovering the popover element. - */ - _popoverMouseEnter: function() { - if (_timeoutLeave !== null) { - window.clearTimeout(_timeoutLeave); - _timeoutLeave = null; - } - }, - - /** - * Shows the popover and loads content on-the-fly. - */ - _show: function() { - if (_timeoutLeave !== null) { - window.clearTimeout(_timeoutLeave); - _timeoutLeave = null; - } - - var forceHide = false; - if (_popover.classList.contains('active')) { - if (_activeId !== _hoverId) { - this._hide(); - - forceHide = true; - } - } - else if (_popoverContent.childElementCount) { - forceHide = true; - } - - if (forceHide) { - _popover.classList.add('forceHide'); - - // force layout - //noinspection BadExpressionStatementJS - _popover.offsetTop; - - this._clearContent(); - - _popover.classList.remove('forceHide'); - } - - _activeId = _hoverId; - - var elementData = _elements.get(_activeId); - // check if source element is already gone - if (elementData === undefined) { - return; - } - - var data = _cache.get(elData(elementData.element, 'cache-id')); - - if (data.state === STATE_READY) { - _popoverContent.appendChild(data.content); - - this._rebuild(_activeId); - } - else if (data.state === STATE_NONE) { - data.state = STATE_LOADING; - - var handler = _handlers.get(elementData.identifier); - if (handler.loadCallback) { - handler.loadCallback(elementData.objectId, this, elementData.element); - } - else if (handler.dboAction) { - var callback = function(data) { - this.setContent( - elementData.identifier, - elementData.objectId, - data.returnValues.template - ); - }.bind(this); - - this.ajaxApi({ - actionName: 'getPopover', - className: handler.dboAction, - interfaceName: 'wcf\\data\\IPopoverAction', - objectIDs: [ elementData.objectId ] - }, callback, callback); - } - } - }, - - /** - * Hides the popover element. - */ - _hide: function() { - if (_timeoutLeave !== null) { - window.clearTimeout(_timeoutLeave); - _timeoutLeave = null; - } - - _popover.classList.remove('active'); - }, - - /** - * Clears popover content by moving it back into the cache. - */ - _clearContent: function() { - if (_activeId && _popoverContent.childElementCount && !_popover.classList.contains('active')) { - var activeElData = _cache.get(elData(_elements.get(_activeId).element, 'cache-id')); - while (_popoverContent.childNodes.length) { - activeElData.content.appendChild(_popoverContent.childNodes[0]); - } - } - }, - - /** - * Rebuilds the popover. - */ - _rebuild: function() { - if (_popover.classList.contains('active')) { - return; - } - - _popover.classList.remove('forceHide'); - _popover.classList.add('active'); - - UiAlignment.set(_popover, _elements.get(_activeId).element, { - pointer: true, - vertical: 'top' - }); - }, - - _ajaxSetup: function() { - return { - silent: true - }; - }, - - /** - * Sends an AJAX requests to the server, simple wrapper to reuse the request object. - * - * @param {Object} data request data - * @param {function} success success callback - * @param {function=} failure error callback - */ - ajaxApi: function(data, success, failure) { - if (typeof success !== 'function') { - throw new TypeError("Expected a valid callback for parameter 'success'."); - } - - Ajax.api(this, data, success, failure); - } - }; -}); diff --git a/wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Popover.ts b/wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Popover.ts new file mode 100644 index 0000000000..545ccea6a3 --- /dev/null +++ b/wcfsetup/install/files/ts/WoltLabSuite/Core/Controller/Popover.ts @@ -0,0 +1,475 @@ +/** + * Versatile popover manager. + * + * @author Alexander Ebert + * @copyright 2001-2019 WoltLab GmbH + * @license GNU Lesser General Public License + * @module WoltLabSuite/Core/Controller/Popover + */ + +import * as Ajax from "../Ajax"; +import DomChangeListener from "../Dom/Change/Listener"; +import DomUtil from "../Dom/Util"; +import * as Environment from "../Environment"; +import * as UiAlignment from "../Ui/Alignment"; +import { AjaxCallbackObject, AjaxCallbackSetup, CallbackFailure, CallbackSuccess, RequestPayload } from "../Ajax/Data"; + +const enum State { + None, + Loading, + Ready, +} + +const enum Delay { + Hide = 500, + Show = 800, +} + +type CallbackLoad = (objectId: number | string, popover: ControllerPopover, element: HTMLElement) => void; + +interface PopoverOptions { + attributeName: string; + className: string; + dboAction: string; + identifier: string; + legacy: boolean; + loadCallback: CallbackLoad; +} + +interface HandlerData { + attributeName: string; + dboAction: string; + legacy: boolean; + loadCallback: CallbackLoad; + selector: string; +} + +interface ElementData { + element: HTMLElement; + identifier: string; + objectId: number | string; +} + +interface CacheData { + content: DocumentFragment | null; + state: State; +} + +class ControllerPopover implements AjaxCallbackObject { + private activeId = ""; + private readonly cache = new Map(); + private readonly elements = new Map(); + private readonly handlers = new Map(); + private hoverId = ""; + private readonly popover: HTMLDivElement; + private readonly popoverContent: HTMLDivElement; + private suspended = false; + private timerEnter?: number = undefined; + private timerLeave?: number = undefined; + + /** + * Builds popover DOM elements and binds event listeners. + */ + constructor() { + this.popover = document.createElement("div"); + this.popover.className = "popover forceHide"; + + this.popoverContent = document.createElement("div"); + this.popoverContent.className = "popoverContent"; + this.popover.appendChild(this.popoverContent); + + const pointer = document.createElement("span"); + pointer.className = "elementPointer"; + pointer.appendChild(document.createElement("span")); + this.popover.appendChild(pointer); + + document.body.appendChild(this.popover); + + // event listener + this.popover.addEventListener("mouseenter", this.popoverMouseEnter.bind(this)); + this.popover.addEventListener("mouseleave", () => this.mouseLeave()); + + this.popover.addEventListener("animationend", this.clearContent.bind(this)); + + window.addEventListener("beforeunload", () => { + this.suspended = true; + + if (this.timerEnter) { + window.clearTimeout(this.timerEnter); + this.timerEnter = undefined; + } + + this.hidePopover(); + }); + + DomChangeListener.add("WoltLabSuite/Core/Controller/Popover", (identifier) => this.initHandler(identifier)); + } + + /** + * Initializes a popover handler. + * + * Usage: + * + * ControllerPopover.init({ + * attributeName: 'data-object-id', + * className: 'fooLink', + * identifier: 'com.example.bar.foo', + * loadCallback: (objectId, popover) => { + * // request data for object id (e.g. via WoltLabSuite/Core/Ajax) + * + * // then call this to set the content + * popover.setContent('com.example.bar.foo', objectId, htmlTemplateString); + * } + * }); + */ + init(options: PopoverOptions): void { + if (Environment.platform() !== "desktop") { + return; + } + + options.attributeName = options.attributeName || "data-object-id"; + options.legacy = (options.legacy as unknown) === true; + + if (this.handlers.has(options.identifier)) { + return; + } + + // Legacy implementations provided a selector for `className`. + const selector = options.legacy ? options.className : `.${options.className}`; + + this.handlers.set(options.identifier, { + attributeName: options.attributeName, + dboAction: options.dboAction, + legacy: options.legacy, + loadCallback: options.loadCallback, + selector, + }); + + this.initHandler(options.identifier); + } + + /** + * Initializes a popover handler. + */ + private initHandler(identifier?: string): void { + if (typeof identifier === "string" && identifier.length) { + this.initElements(this.handlers.get(identifier)!, identifier); + } else { + this.handlers.forEach((value, key) => { + this.initElements(value, key); + }); + } + } + + /** + * Binds event listeners for popover-enabled elements. + */ + private initElements(options: HandlerData, identifier: string): void { + document.querySelectorAll(options.selector).forEach((element: HTMLElement) => { + const id = DomUtil.identify(element); + if (this.cache.has(id)) { + return; + } + + // Skip elements that are located inside a popover. + if (element.closest(".popover") !== null) { + this.cache.set(id, { + content: null, + state: State.None, + }); + + return; + } + + const objectId = options.legacy ? id : ~~element.getAttribute(options.attributeName)!; + if (objectId === 0) { + return; + } + + element.addEventListener("mouseenter", (ev) => this.mouseEnter(ev)); + element.addEventListener("mouseleave", () => this.mouseLeave()); + + if (element instanceof HTMLAnchorElement && element.href) { + element.addEventListener("click", () => this.hidePopover()); + } + + const cacheId = `${identifier}-${objectId}`; + element.dataset.cacheId = cacheId; + + this.elements.set(id, { + element, + identifier, + objectId: objectId.toString(), + }); + + if (!this.cache.has(cacheId)) { + this.cache.set(cacheId, { + content: null, + state: State.None, + }); + } + }); + } + + /** + * Sets the content for given identifier and object id. + */ + setContent(identifier: string, objectId: number | string, content: string): void { + const cacheId = `${identifier}-${objectId}`; + const data = this.cache.get(cacheId); + if (data === undefined) { + throw new Error(`Unable to find element for object id '${objectId}' (identifier: '${identifier}').`); + } + + let fragment = DomUtil.createFragmentFromHtml(content); + if (!fragment.childElementCount) { + fragment = DomUtil.createFragmentFromHtml("

" + content + "

"); + } + + data.content = fragment; + data.state = State.Ready; + + if (this.activeId) { + const activeElement = this.elements.get(this.activeId)!.element; + + if (activeElement.dataset.cacheId === cacheId) { + this.show(); + } + } + } + + /** + * Handles the mouse start hovering the popover-enabled element. + */ + private mouseEnter(event: MouseEvent): void { + if (this.suspended) { + return; + } + + if (this.timerEnter) { + window.clearTimeout(this.timerEnter); + this.timerEnter = undefined; + } + + const id = DomUtil.identify(event.currentTarget as HTMLElement); + if (this.activeId === id && this.timerLeave) { + window.clearTimeout(this.timerLeave); + this.timerLeave = undefined; + } + + this.hoverId = id; + + this.timerEnter = window.setTimeout(() => { + this.timerEnter = undefined; + + if (this.hoverId === id) { + this.show(); + } + }, Delay.Show); + } + + /** + * Handles the mouse leaving the popover-enabled element or the popover itself. + */ + private mouseLeave(): void { + this.hoverId = ""; + + if (this.timerLeave) { + return; + } + + this.timerLeave = window.setTimeout(() => this.hidePopover(), Delay.Hide); + } + + /** + * Handles the mouse start hovering the popover element. + */ + private popoverMouseEnter(): void { + if (this.timerLeave) { + window.clearTimeout(this.timerLeave); + this.timerLeave = undefined; + } + } + + /** + * Shows the popover and loads content on-the-fly. + */ + private show(): void { + if (this.timerLeave) { + window.clearTimeout(this.timerLeave); + this.timerLeave = undefined; + } + + let forceHide = false; + if (this.popover.classList.contains("active")) { + if (this.activeId !== this.hoverId) { + this.hidePopover(); + + forceHide = true; + } + } else if (this.popoverContent.childElementCount) { + forceHide = true; + } + + if (forceHide) { + this.popover.classList.add("forceHide"); + + // force layout + //noinspection BadExpressionStatementJS + this.popover.offsetTop; + + this.clearContent(); + + this.popover.classList.remove("forceHide"); + } + + this.activeId = this.hoverId; + + const elementData = this.elements.get(this.activeId); + // check if source element is already gone + if (elementData === undefined) { + return; + } + + const cacheId = elementData.element.dataset.cacheId!; + const data = this.cache.get(cacheId)!; + + if (data.state === State.Ready) { + this.popoverContent.appendChild(data.content!); + + this.rebuild(); + } else if (data.state === State.None) { + data.state = State.Loading; + + const handler = this.handlers.get(elementData.identifier)!; + if (handler.loadCallback) { + handler.loadCallback(elementData.objectId, this, elementData.element); + } else if (handler.dboAction) { + const callback = (data) => { + this.setContent(elementData.identifier, elementData.objectId, data.returnValues.template); + + return true; + }; + + this.ajaxApi( + { + actionName: "getPopover", + className: handler.dboAction, + interfaceName: "wcf\\data\\IPopoverAction", + objectIDs: [elementData.objectId], + }, + callback, + callback, + ); + } + } + } + + /** + * Hides the popover element. + */ + private hidePopover(): void { + if (this.timerLeave) { + window.clearTimeout(this.timerLeave); + this.timerLeave = undefined; + } + + this.popover.classList.remove("active"); + } + + /** + * Clears popover content by moving it back into the cache. + */ + private clearContent(): void { + if (this.activeId && this.popoverContent.childElementCount && !this.popover.classList.contains("active")) { + const cacheId = this.elements.get(this.activeId)!.element.dataset.cacheId!; + const activeElData = this.cache.get(cacheId)!; + while (this.popoverContent.childNodes.length) { + activeElData.content!.appendChild(this.popoverContent.childNodes[0]); + } + } + } + + /** + * Rebuilds the popover. + */ + private rebuild(): void { + if (this.popover.classList.contains("active")) { + return; + } + + this.popover.classList.remove("forceHide"); + this.popover.classList.add("active"); + + UiAlignment.set(this.popover, this.elements.get(this.activeId)!.element, { + pointer: true, + vertical: "top", + }); + } + + _ajaxSuccess() { + // This class was designed in a strange way without utilizing this method. + } + + _ajaxSetup(): ReturnType { + return { + silent: true, + }; + } + + /** + * Sends an AJAX requests to the server, simple wrapper to reuse the request object. + */ + ajaxApi(data: RequestPayload, success: CallbackSuccess, failure: CallbackFailure): void { + if (typeof success !== "function") { + throw new TypeError("Expected a valid callback for parameter 'success'."); + } + + Ajax.api(this, data, success, failure); + } +} + +let controllerPopover: ControllerPopover; + +function getControllerPopover(): ControllerPopover { + if (!controllerPopover) { + controllerPopover = new ControllerPopover(); + } + + return controllerPopover; +} + +/** + * Initializes a popover handler. + * + * Usage: + * + * ControllerPopover.init({ + * attributeName: 'data-object-id', + * className: 'fooLink', + * identifier: 'com.example.bar.foo', + * loadCallback: function(objectId, popover) { + * // request data for object id (e.g. via WoltLabSuite/Core/Ajax) + * + * // then call this to set the content + * popover.setContent('com.example.bar.foo', objectId, htmlTemplateString); + * } + * }); + */ +export function init(options: PopoverOptions): void { + getControllerPopover().init(options); +} + +/** + * Sets the content for given identifier and object id. + */ +export function setContent(identifier: string, objectId: number, content: string): void { + getControllerPopover().setContent(identifier, objectId, content); +} + +/** + * Sends an AJAX requests to the server, simple wrapper to reuse the request object. + */ +export function ajaxApi(data: RequestPayload, success: CallbackSuccess, failure: CallbackFailure): void { + getControllerPopover().ajaxApi(data, success, failure); +} -- 2.20.1