/**
* Versatile popover manager.
*
- * @author Alexander Ebert
- * @copyright 2001-2019 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @module WoltLabSuite/Core/Controller/Popover
+ * @author Alexander Ebert
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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.
*
* 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("<p>" + content + "</p>");
}
- var fragment = DomUtil.createFragmentFromHtml(content);
- if (!fragment.childElementCount)
- fragment = DomUtil.createFragmentFromHtml('<p>' + content + '</p>');
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;
});
| any[];
}
+// Return `false` to suppress the error message.
export type CallbackFailure = (
data: ResponseData,
responseText: string,
+++ /dev/null
-/**
- * Versatile popover manager.
- *
- * @author Alexander Ebert
- * @copyright 2001-2019 WoltLab GmbH
- * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @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('<p>' + content + '</p>');
- 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);
- }
- };
-});
--- /dev/null
+/**
+ * Versatile popover manager.
+ *
+ * @author Alexander Ebert
+ * @copyright 2001-2019 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @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<string, CacheData>();
+ private readonly elements = new Map<string, ElementData>();
+ private readonly handlers = new Map<string, HandlerData>();
+ 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("<p>" + content + "</p>");
+ }
+
+ 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<AjaxCallbackSetup> {
+ 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);
+}