if (COMPILER_TARGET_DEFAULT) {
/**
* Handles message quotes.
+ *
+ * @deprecated 5.4 Use `WoltLabSuite/Core/Ui/Message/Quote` instead
*/
WCF.Message.Quote.Handler = Class.extend({
- /**
- * active container id
- * @var string
- */
- _activeContainerID: '',
-
- /**
- * action class name
- * @var string
- */
- _className: '',
-
- /**
- * list of message containers
- * @var object
- */
- _containers: {},
-
- /**
- * container selector
- * @var string
- */
- _containerSelector: '',
-
- /**
- * 'copy quote' overlay
- * @var jQuery
- */
- _copyQuote: null,
-
- /**
- * marked message
- * @var string
- */
- _message: '',
-
- /**
- * message body selector
- * @var string
- */
- _messageBodySelector: '',
-
- /**
- * object id
- * @var {int}
- */
- _objectID: 0,
-
- /**
- * object type name
- * @var string
- */
- _objectType: '',
-
- /**
- * action proxy
- * @var WCF.Action.Proxy
- */
- _proxy: null,
-
- /**
- * quote manager
- * @var WCF.Message.Quote.Manager
- */
- _quoteManager: null,
-
- /**
- * @var {?int}
- */
- _selectionChangeTimer: null,
-
- /**
- * @var {boolean}
- */
- _isMouseDown: false,
-
- /**
- * Initializes the quote handler for given object type.
- *
- * @param {WCF.Message.Quote.Manager} quoteManager
- * @param {string} className
- * @param {string} objectType
- * @param {string} containerSelector
- * @param {string} messageBodySelector
- * @param {string} messageContentSelector
- * @param {boolean} supportDirectInsert
- */
init: function (quoteManager, className, objectType, containerSelector, messageBodySelector, messageContentSelector, supportDirectInsert) {
- this._className = className;
- if (this._className === '') {
- console.debug("[WCF.Message.QuoteManager] Empty class name given, aborting.");
- return;
- }
-
- this._objectType = objectType;
- if (this._objectType === '') {
- console.debug("[WCF.Message.QuoteManager] Empty object type name given, aborting.");
- return;
- }
-
- this._containerSelector = containerSelector;
- this._message = '';
- this._messageBodySelector = messageBodySelector;
- this._objectID = 0;
- this._proxy = new WCF.Action.Proxy({
- success: $.proxy(this._success, this)
- });
- this._selectionChangeTimer = null;
- this._isMouseDown = false;
-
- this._initContainers();
-
- supportDirectInsert = (supportDirectInsert && quoteManager.supportPaste());
- this._initCopyQuote(supportDirectInsert);
-
- $(document).mouseup($.proxy(this._mouseUp, this));
- document.addEventListener('selectionchange', this._selectionchange.bind(this));
-
- // register with quote manager
- this._quoteManager = quoteManager;
- this._quoteManager.register(this._objectType, this);
-
- // register with DOMNodeInsertedHandler
- WCF.DOMNodeInsertedHandler.addCallback('WCF.Message.Quote.Handler' + objectType.hashCode(), $.proxy(this._initContainers, this));
-
- // Prevent the tooltip from being selectable while the touch pointer is being moved.
- var tooltip = this._copyQuote[0];
- document.addEventListener("touchstart", function (event) {
- if (tooltip.classList.contains("active")) {
- var target = event.target;
- if (target !== tooltip && !tooltip.contains(target)) {
- tooltip.classList.add("touchForceInaccessible");
-
- document.addEventListener("touchend", function () {
- tooltip.classList.remove("touchForceInaccessible");
- }, {once: true});
- }
- }
- }, {passive: true});
- },
-
- /**
- * Initializes message containers.
- */
- _initContainers: function () {
- var self = this;
- $(this._containerSelector).each(function (index, container) {
- var $container = $(container);
- var $containerID = $container.wcfIdentify();
-
- if (!self._containers[$containerID]) {
- self._containers[$containerID] = $container;
- if ($container.hasClass('jsInvalidQuoteTarget')) {
- return true;
- }
-
- if (self._messageBodySelector) {
- $container.data('body', $container.find(self._messageBodySelector).data('containerID', $containerID));
- }
-
- $container.mousedown($.proxy(self._mouseDown, self));
- $container[0].classList.add('jsQuoteMessageContainer');
-
- // bind event to quote whole message
- self._containers[$containerID].find('.jsQuoteMessage').click($.proxy(self._saveFullQuote, self));
- }
+ require(["WoltLabSuite/Core/Ui/Message/Quote"], (UiMessageQuote) => {
+ new UiMessageQuote.default(
+ quoteManager,
+ className,
+ objectType,
+ containerSelector,
+ messageBodySelector,
+ messageContentSelector,
+ supportDirectInsert,
+ );
});
},
-
- _selectionchange: function () {
- if (this._isMouseDown) {
- return;
- }
-
- if (this._activeContainerID === '') {
- // check if the selection is non-empty and is entirely contained
- // inside a single message container that is registered for quoting
- var selection = window.getSelection();
- if (selection.rangeCount !== 1 || selection.isCollapsed) {
- return;
- }
-
- var range = selection.getRangeAt(0);
- var startContainer = elClosest(range.startContainer, '.jsQuoteMessageContainer');
- var endContainer = elClosest(range.endContainer, '.jsQuoteMessageContainer');
- if (startContainer && startContainer === endContainer && !startContainer.classList.contains('jsInvalidQuoteTarget')) {
- // Check if the selection is visible, such as text marked inside containers with an
- // active overflow handling attached to it. This can be a side effect of the browser
- // search which modifies the text selection, but cannot be distinguished from manual
- // selections initiated by the user.
- var commonAncestor = range.commonAncestorContainer;
- if (commonAncestor.nodeType !== Node.ELEMENT_NODE) {
- commonAncestor = commonAncestor.parentNode;
- }
-
- var offsetParent = commonAncestor.offsetParent;
- if (startContainer.contains(offsetParent)) {
- if (offsetParent.scrollTop + offsetParent.clientHeight < commonAncestor.offsetTop) {
- // The selected text is not visible to the user.
- return;
- }
- }
-
- this._activeContainerID = startContainer.id;
- }
- }
-
- if (this._selectionChangeTimer !== null) {
- window.clearTimeout(this._selectionChangeTimer);
- }
-
- this._selectionChangeTimer = window.setTimeout(this._mouseUp.bind(this), 100);
- },
-
- /**
- * Handles mouse down event.
- *
- * @param {Event} event
- */
- _mouseDown: function (event) {
- // hide copy quote
- this._copyQuote.removeClass('active');
-
- this._activeContainerID = (event.currentTarget.classList.contains('jsInvalidQuoteTarget')) ? '' : event.currentTarget.id;
-
- if (this._selectionChangeTimer !== null) {
- window.clearTimeout(this._selectionChangeTimer);
- this._selectionChangeTimer = null;
- }
-
- this._isMouseDown = true;
- },
-
- /**
- * Returns the text of a node and its children.
- *
- * @param {Node} node
- * @return {string}
- */
- _getNodeText: function (node) {
- // work-around for IE, see http://stackoverflow.com/a/5983176
- var $nodeFilter = function (node) {
- switch (node.tagName) {
- case 'BLOCKQUOTE':
- case 'SCRIPT':
- return NodeFilter.FILTER_REJECT;
-
- case 'IMG':
- if (!node.classList.contains('smiley') || node.alt.length === 0) {
- return NodeFilter.FILTER_REJECT;
- }
- // fallthrough
-
- //noinspection FallthroughInSwitchStatementJS
- default:
- return NodeFilter.FILTER_ACCEPT;
- }
- };
- $nodeFilter.acceptNode = $nodeFilter;
-
- var $walker = document.createTreeWalker(
- node,
- NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
- $nodeFilter,
- true
- );
-
- var $text = '', ignoreLinks = [], value;
- while ($walker.nextNode()) {
- var $node = $walker.currentNode;
-
- if ($node.nodeType === Node.ELEMENT_NODE) {
- switch ($node.tagName) {
- case 'A':
- // \u2026 === …
- value = $node.textContent;
- if (value.indexOf('\u2026') > 0) {
- var tmp = value.split(/\u2026/);
- if (tmp.length === 2) {
- var href = $node.href;
- if (href.indexOf(tmp[0]) === 0 && href.substr(tmp[1].length * -1) === tmp[1]) {
- // truncated url, use original href to preserve link
- $text += href;
- ignoreLinks.push($node);
- }
- }
- }
- break;
-
- case 'BR':
- case 'LI':
- case 'UL':
- $text += "\n";
- break;
-
- case 'TD':
- if (!$.browser.msie) {
- $text += "\n";
- }
- break;
-
- case 'P':
- $text += "\n\n";
- break;
-
- // smilies
- case 'IMG':
- $text += " " + $node.alt + " ";
- break;
-
- // Code listing
- case 'DIV':
- if ($node.classList.contains('codeBoxHeadline') || $node.classList.contains('codeBoxLine')) {
- $text += "\n";
- }
- break;
- }
- }
- else {
- if ($node.parentNode.nodeName === 'A' && ignoreLinks.indexOf($node.parentNode) !== -1) {
- // ignore text content of links that have already been captured
- continue;
- }
-
- // Firefox loves to arbitrarily wrap pasted text at weird line lengths, causing
- // pointless linebreaks to be inserted. Replacing them with a simple space will
- // preserve the spacing between words that would otherwise be lost.
- $text += $node.nodeValue.replace(/\n/g, ' ');
- }
-
- }
-
- return $text;
- },
-
- /**
- * Handles the mouse up event.
- *
- * @param {?$.Event} event
- */
- _mouseUp: function (event) {
- if (event && event.originalEvent instanceof Event) {
- if (this._selectionChangeTimer !== null) {
- // prevent collisions of the `selectionchange` and the `mouseup` event
- window.clearTimeout(this._selectionChangeTimer);
- this._selectionChangeTimer = null;
- }
-
- this._isMouseDown = false;
- }
-
- // ignore event
- if (this._activeContainerID === '') {
- this._copyQuote.removeClass('active');
- return;
- }
-
- var selection = window.getSelection();
- if (selection.rangeCount !== 1 || selection.isCollapsed) {
- this._copyQuote.removeClass('active');
- return;
- }
-
- var $container = this._containers[this._activeContainerID];
- var $objectID = $container.data('objectID');
- $container = $container.data('body') || $container;
-
- var anchorNode = selection.anchorNode;
- while (anchorNode) {
- if (anchorNode === $container[0]) {
- break;
- }
-
- anchorNode = anchorNode.parentNode;
- }
-
- // selection spans unrelated nodes
- if (anchorNode !== $container[0]) {
- this._copyQuote.removeClass('active');
- return;
- }
-
- var $selection = this._getSelectedText();
- var $text = $.trim($selection);
- if ($text === '') {
- this._copyQuote.removeClass('active');
-
- return;
- }
-
- // check if mousedown/mouseup took place inside a blockquote
- var range = selection.getRangeAt(0);
- var startContainer = (range.startContainer.nodeType === Node.TEXT_NODE) ? range.startContainer.parentNode : range.startContainer;
- var endContainer = (range.endContainer.nodeType === Node.TEXT_NODE) ? range.endContainer.parentNode : range.endContainer;
- if (startContainer.closest('blockquote') || endContainer.closest('blockquote')) {
- this._copyQuote.removeClass('active');
-
- return;
- }
-
- // compare selection with message text of given container
- var $messageText = this._getNodeText($container[0]);
-
- // selected text is not part of $messageText or contains text from unrelated nodes
- if (this._normalize($messageText).indexOf(this._normalize($text)) === -1) {
- return;
- }
- this._copyQuote.addClass('active');
-
- var $coordinates = this._getBoundingRectangle($container, window.getSelection());
- var $dimensions = this._copyQuote.getDimensions('outer');
- var $left = ($coordinates.right - $coordinates.left) / 2 - ($dimensions.width / 2) + $coordinates.left;
-
- // Prevent the overlay from overflowing the left or right boundary of the container.
- var containerBoundaries = $container[0].getBoundingClientRect();
- if ($left < containerBoundaries.left) {
- $left = containerBoundaries.left;
- }
- else if ($left + $dimensions.width > containerBoundaries.right) {
- $left = containerBoundaries.right - $dimensions.width;
- }
-
- this._copyQuote.css({
- top: $coordinates.bottom + 7 + 'px',
- left: $left + 'px'
- });
- this._copyQuote.removeClass('active');
-
- if (this._selectionChangeTimer === null) {
- // reset containerID
- this._activeContainerID = '';
- }
- else {
- window.clearTimeout(this._selectionChangeTimer);
- this._selectionChangeTimer = null;
- }
-
- // show element after a delay, to prevent display if text was unmarked again (clicking into marked text)
- var self = this;
- window.setTimeout(function () {
- var $text = $.trim(self._getSelectedText());
- if ($text !== '') {
- self._copyQuote.addClass('active');
- self._message = $text;
- self._objectID = $objectID;
- }
- }, 10);
- },
-
- /**
- * Normalizes a text for comparison.
- *
- * @param {string} text
- * @return {string}
- */
- _normalize: function (text) {
- return text.replace(/\r?\n|\r/g, "\n").replace(/\s/g, ' ').replace(/\s{2,}/g, ' ');
- },
-
- /**
- * Returns the offsets of the selection's bounding rectangle.
- *
- * @return {Object}
- */
- _getBoundingRectangle: function (container, selection) {
- var $coordinates = null;
-
- if (selection.rangeCount > 0) {
- // the coordinates returned by getBoundingClientRect() are relative to the viewport, not the document!
- var $rect = selection.getRangeAt(0).getBoundingClientRect();
-
- var scrollTop = $(document).scrollTop();
- $coordinates = {
- bottom: $rect.bottom + scrollTop,
- left: $rect.left,
- right: $rect.right,
- top: $rect.top + scrollTop
- };
- }
-
- return $coordinates;
- },
-
- /**
- * Initializes the 'copy quote' element.
- *
- * @param {boolean} supportDirectInsert
- */
- _initCopyQuote: function (supportDirectInsert) {
- this._copyQuote = $('#quoteManagerCopy');
- if (!this._copyQuote.length) {
- this._copyQuote = $('<div id="quoteManagerCopy" class="balloonTooltip interactive"><span class="jsQuoteManagerStore">' + WCF.Language.get('wcf.message.quote.quoteSelected') + '</span></div>').appendTo(document.body);
- var $storeQuote = this._copyQuote.children('span.jsQuoteManagerStore').click($.proxy(this._saveQuote, this));
- if (supportDirectInsert) {
- $('<span class="jsQuoteManagerQuoteAndInsert">' + WCF.Language.get('wcf.message.quote.quoteAndReply') + '</span>').click($.proxy(this._saveAndInsertQuote, this)).insertAfter($storeQuote);
- }
- }
- },
-
- /**
- * Returns the text selection.
- *
- * @return string
- */
- _getSelectedText: function () {
- var $selection = window.getSelection();
- if ($selection.rangeCount) {
- return this._getNodeText($selection.getRangeAt(0).cloneContents());
- }
-
- return '';
- },
-
- /**
- * Saves a full quote.
- *
- * @param {Event} event
- */
- _saveFullQuote: function (event) {
- event.preventDefault();
-
- var $listItem = $(event.currentTarget);
-
- this._proxy.setOption('data', {
- actionName: 'saveFullQuote',
- className: this._className,
- interfaceName: 'wcf\\data\\IMessageQuoteAction',
- objectIDs: [$listItem.data('objectID')]
- });
- this._proxy.sendRequest();
-
- // mark element as quoted
- if ($listItem.data('isQuoted')) {
- $listItem.data('isQuoted', false).children('a').removeClass('active');
- }
- else {
- $listItem.data('isQuoted', true).children('a').addClass('active');
- }
-
- // close navigation on mobile
- var $navigationList = $listItem.parents('.buttonGroupNavigation');
- if ($navigationList.hasClass('jsMobileButtonGroupNavigation')) {
- $navigationList.children('.dropdownLabel').trigger('click');
- }
- },
-
- /**
- * Saves a quote.
- *
- * @param {boolean} renderQuote
- */
- _saveQuote: function (renderQuote) {
- this._proxy.setOption('data', {
- actionName: 'saveQuote',
- className: this._className,
- interfaceName: 'wcf\\data\\IMessageQuoteAction',
- objectIDs: [this._objectID],
- parameters: {
- message: this._message,
- renderQuote: (renderQuote === true)
- }
- });
- this._proxy.sendRequest();
-
- var selection = window.getSelection();
- if (selection.rangeCount) {
- selection.removeAllRanges();
- this._copyQuote[0].classList.remove("active");
- }
- },
-
- /**
- * Saves a quote and directly inserts it.
- */
- _saveAndInsertQuote: function () {
- this._saveQuote(true);
- },
-
- /**
- * Handles successful AJAX requests.
- *
- * @param {Object} data
- */
- _success: function (data) {
- if (data.returnValues.count !== undefined) {
- if (data.returnValues.fullQuoteMessageIDs !== undefined) {
- data.returnValues.fullQuoteObjectIDs = data.returnValues.fullQuoteMessageIDs;
- }
-
- var $fullQuoteObjectIDs = (data.returnValues.fullQuoteObjectIDs !== undefined) ? data.returnValues.fullQuoteObjectIDs : {};
- this._quoteManager.updateCount(data.returnValues.count, $fullQuoteObjectIDs);
- }
-
- switch (data.actionName) {
- case 'saveQuote':
- case 'saveFullQuote':
- if (data.returnValues.renderedQuote) {
- WCF.System.Event.fireEvent('com.woltlab.wcf.message.quote', 'insert', {
- forceInsert: (data.actionName === 'saveQuote'),
- quote: data.returnValues.renderedQuote
- });
- }
- break;
- }
- },
-
- /**
- * Updates the full quote data for all matching objects.
- *
- * @param array<integer> $objectIDs
- */
- updateFullQuoteObjectIDs: function (objectIDs) {
- for (var $containerID in this._containers) {
- this._containers[$containerID].find('.jsQuoteMessage').each(function (index, button) {
- // reset all markings
- var $button = $(button).data('isQuoted', 0);
- $button.children('a').removeClass('active');
-
- // mark as active
- if (WCF.inArray($button.data('objectID'), objectIDs)) {
- $button.data('isQuoted', 1).children('a').addClass('active');
- }
- });
- }
- }
});
/**
}
return innerError;
},
+ /**
+ * Finds the closest element that matches the provided selector. This is a helper
+ * function because `closest()` does exist on elements only, for example, it is
+ * missing on text nodes.
+ */
+ closest(node, selector) {
+ const element = node instanceof HTMLElement ? node : node.parentElement;
+ return element.closest(selector);
+ },
+ /**
+ * Returns the `node` if it is an element or its parent. This is useful when working
+ * with the range of a text selection.
+ */
+ getClosestElement(node) {
+ return node instanceof HTMLElement ? node : node.parentElement;
+ },
};
// expose on window object for backward compatibility
window.bc_wcfDomUtil = DomUtil;
--- /dev/null
+define(["require", "exports", "tslib", "../../Ajax", "../../Core", "../../Event/Handler", "../../Language", "../../Dom/Change/Listener", "../../Dom/Util"], function (require, exports, tslib_1, Ajax, Core, EventHandler, Language, Listener_1, Util_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.UiMessageQuote = void 0;
+ Ajax = tslib_1.__importStar(Ajax);
+ Core = tslib_1.__importStar(Core);
+ EventHandler = tslib_1.__importStar(EventHandler);
+ Language = tslib_1.__importStar(Language);
+ Listener_1 = tslib_1.__importDefault(Listener_1);
+ Util_1 = tslib_1.__importDefault(Util_1);
+ class UiMessageQuote {
+ /**
+ * Initializes the quote handler for given object type.
+ */
+ constructor(quoteManager, // TODO
+ className, objectType, containerSelector, messageBodySelector, messageContentSelector, supportDirectInsert) {
+ this.activeMessageId = "";
+ this.containers = new Map();
+ this.containerSelector = "";
+ this.copyQuote = document.createElement("div");
+ this.message = "";
+ this.objectId = 0;
+ this.objectType = "";
+ this.timerSelectionChange = undefined;
+ this.isMouseDown = false;
+ this.className = className;
+ this.objectType = objectType;
+ this.containerSelector = containerSelector;
+ this.messageBodySelector = messageBodySelector;
+ this.initContainers();
+ supportDirectInsert = supportDirectInsert && quoteManager.supportPaste();
+ this.quoteManager = quoteManager;
+ this.initCopyQuote(supportDirectInsert);
+ document.addEventListener("mouseup", (event) => this.onMouseUp(event));
+ document.addEventListener("selectionchange", () => this.onSelectionchange());
+ Listener_1.default.add("UiMessageQuote", () => this.initContainers());
+ // Prevent the tooltip from being selectable while the touch pointer is being moved.
+ document.addEventListener("touchstart", (event) => {
+ if (this.copyQuote.classList.contains("active")) {
+ const target = event.target;
+ if (target !== this.copyQuote && !this.copyQuote.contains(target)) {
+ this.copyQuote.classList.add("touchForceInaccessible");
+ document.addEventListener("touchend", () => {
+ this.copyQuote.classList.remove("touchForceInaccessible");
+ }, { once: true });
+ }
+ }
+ }, { passive: true });
+ }
+ /**
+ * Initializes message containers.
+ */
+ initContainers() {
+ document.querySelectorAll(this.containerSelector).forEach((container) => {
+ var _a;
+ const id = Util_1.default.identify(container);
+ if (this.containers.has(id)) {
+ return;
+ }
+ this.containers.set(id, container);
+ if (container.classList.contains("jsInvalidQuoteTarget")) {
+ return;
+ }
+ container.addEventListener("mousedown", (event) => this.onMouseDown(event));
+ container.classList.add("jsQuoteMessageContainer");
+ (_a = container
+ .querySelector(".jsQuoteMessage")) === null || _a === void 0 ? void 0 : _a.addEventListener("click", (event) => this.saveFullQuote(event));
+ });
+ }
+ onSelectionchange() {
+ if (this.isMouseDown) {
+ return;
+ }
+ if (this.activeMessageId === "") {
+ // check if the selection is non-empty and is entirely contained
+ // inside a single message container that is registered for quoting
+ const selection = window.getSelection();
+ if (selection.rangeCount !== 1 || selection.isCollapsed) {
+ return;
+ }
+ const range = selection.getRangeAt(0);
+ const startContainer = Util_1.default.closest(range.startContainer, ".jsQuoteMessageContainer");
+ const endContainer = Util_1.default.closest(range.endContainer, ".jsQuoteMessageContainer");
+ if (startContainer &&
+ startContainer === endContainer &&
+ !startContainer.classList.contains("jsInvalidQuoteTarget")) {
+ // Check if the selection is visible, such as text marked inside containers with an
+ // active overflow handling attached to it. This can be a side effect of the browser
+ // search which modifies the text selection, but cannot be distinguished from manual
+ // selections initiated by the user.
+ let commonAncestor = range.commonAncestorContainer;
+ if (commonAncestor.nodeType !== Node.ELEMENT_NODE) {
+ commonAncestor = commonAncestor.parentElement;
+ }
+ const offsetParent = commonAncestor.offsetParent;
+ if (startContainer.contains(offsetParent)) {
+ if (offsetParent.scrollTop + offsetParent.clientHeight < commonAncestor.offsetTop) {
+ // The selected text is not visible to the user.
+ return;
+ }
+ }
+ this.activeMessageId = startContainer.id;
+ }
+ }
+ if (this.timerSelectionChange) {
+ window.clearTimeout(this.timerSelectionChange);
+ }
+ this.timerSelectionChange = window.setTimeout(() => this.onMouseUp(), 100);
+ }
+ onMouseDown(event) {
+ // hide copy quote
+ this.copyQuote.classList.remove("active");
+ const message = event.currentTarget;
+ this.activeMessageId = message.classList.contains("jsInvalidQuoteTarget") ? "" : message.id;
+ if (this.timerSelectionChange) {
+ window.clearTimeout(this.timerSelectionChange);
+ this.timerSelectionChange = undefined;
+ }
+ this.isMouseDown = true;
+ }
+ /**
+ * Returns the text of a node and its children.
+ */
+ getNodeText(node) {
+ const treeWalker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, {
+ acceptNode(node) {
+ if (node.nodeName === "BLOCKQUOTE" || node.nodeName === "SCRIPT") {
+ return NodeFilter.FILTER_REJECT;
+ }
+ if (node instanceof HTMLImageElement) {
+ // Skip any image that is not a smiley or contains no alt text.
+ if (!node.classList.contains("smiley") || !node.alt) {
+ return NodeFilter.FILTER_REJECT;
+ }
+ }
+ return NodeFilter.FILTER_ACCEPT;
+ },
+ });
+ let text = "";
+ const ignoreLinks = [];
+ while (treeWalker.nextNode()) {
+ const node = treeWalker.currentNode;
+ if (node instanceof Text) {
+ const parent = node.parentElement;
+ if (parent instanceof HTMLAnchorElement && ignoreLinks.includes(parent)) {
+ // ignore text content of links that have already been captured
+ continue;
+ }
+ // Firefox loves to arbitrarily wrap pasted text at weird line lengths, causing
+ // pointless linebreaks to be inserted. Replacing them with a simple space will
+ // preserve the spacing between words that would otherwise be lost.
+ text += node.nodeValue.replace(/\n/g, " ");
+ continue;
+ }
+ if (node instanceof HTMLAnchorElement) {
+ // \u2026 === …
+ const value = node.textContent;
+ if (value.indexOf("\u2026") > 0) {
+ const tmp = value.split(/\u2026/);
+ if (tmp.length === 2) {
+ const href = node.href;
+ if (href.indexOf(tmp[0]) === 0 && href.substr(tmp[1].length * -1) === tmp[1]) {
+ // This is a truncated url, use the original href instead to preserve the link.
+ text += href;
+ ignoreLinks.push(node);
+ }
+ }
+ }
+ }
+ switch (node.nodeName) {
+ case "BR":
+ case "LI":
+ case "TD":
+ case "UL":
+ text += "\n";
+ break;
+ case "P":
+ text += "\n\n";
+ break;
+ // smilies
+ case "IMG": {
+ const img = node;
+ text += ` ${img.alt} `;
+ break;
+ }
+ // Code listing
+ case "DIV":
+ if (node.classList.contains("codeBoxHeadline") || node.classList.contains("codeBoxLine")) {
+ text += "\n";
+ }
+ break;
+ }
+ }
+ return text;
+ }
+ onMouseUp(event) {
+ if (event instanceof Event) {
+ if (this.timerSelectionChange) {
+ // Prevent collisions of the `selectionchange` and the `mouseup` event.
+ window.clearTimeout(this.timerSelectionChange);
+ this.timerSelectionChange = undefined;
+ }
+ this.isMouseDown = false;
+ }
+ // ignore event
+ if (this.activeMessageId === "") {
+ this.copyQuote.classList.remove("active");
+ return;
+ }
+ const selection = window.getSelection();
+ if (selection.rangeCount !== 1 || selection.isCollapsed) {
+ this.copyQuote.classList.remove("active");
+ return;
+ }
+ const container = this.containers.get(this.activeMessageId);
+ const objectId = ~~container.dataset.objectId;
+ const content = this.messageBodySelector
+ ? container.querySelector(this.messageBodySelector)
+ : container;
+ let anchorNode = selection.anchorNode;
+ while (anchorNode) {
+ if (anchorNode === content) {
+ break;
+ }
+ anchorNode = anchorNode.parentNode;
+ }
+ // selection spans unrelated nodes
+ if (anchorNode !== content) {
+ this.copyQuote.classList.remove("active");
+ return;
+ }
+ const selectedText = this.getSelectedText();
+ const text = selectedText.trim();
+ if (text === "") {
+ this.copyQuote.classList.remove("active");
+ return;
+ }
+ // check if mousedown/mouseup took place inside a blockquote
+ const range = selection.getRangeAt(0);
+ const startContainer = Util_1.default.getClosestElement(range.startContainer);
+ const endContainer = Util_1.default.getClosestElement(range.endContainer);
+ if (startContainer.closest("blockquote") || endContainer.closest("blockquote")) {
+ this.copyQuote.classList.remove("active");
+ return;
+ }
+ // compare selection with message text of given container
+ const messageText = this.getNodeText(content);
+ // selected text is not part of $messageText or contains text from unrelated nodes
+ if (!this.normalizeTextForComparison(messageText).includes(this.normalizeTextForComparison(text))) {
+ return;
+ }
+ this.copyQuote.classList.add("active");
+ const coordinates = this.getElementBoundaries(selection);
+ const dimensions = { height: this.copyQuote.offsetHeight, width: this.copyQuote.offsetWidth };
+ let left = (coordinates.right - coordinates.left) / 2 - dimensions.width / 2 + coordinates.left;
+ // Prevent the overlay from overflowing the left or right boundary of the container.
+ const containerBoundaries = content.getBoundingClientRect();
+ if (left < containerBoundaries.left) {
+ left = containerBoundaries.left;
+ }
+ else if (left + dimensions.width > containerBoundaries.right) {
+ left = containerBoundaries.right - dimensions.width;
+ }
+ this.copyQuote.style.setProperty("top", `${coordinates.bottom + 7}px`);
+ this.copyQuote.style.setProperty("left", `${left}px`);
+ this.copyQuote.classList.remove("active");
+ if (!this.timerSelectionChange) {
+ // reset containerID
+ this.activeMessageId = "";
+ }
+ else {
+ window.clearTimeout(this.timerSelectionChange);
+ this.timerSelectionChange = undefined;
+ }
+ // show element after a delay, to prevent display if text was unmarked again (clicking into marked text)
+ window.setTimeout(() => {
+ const text = this.getSelectedText().trim();
+ if (text !== "") {
+ this.copyQuote.classList.add("active");
+ this.message = text;
+ this.objectId = objectId;
+ }
+ }, 10);
+ }
+ normalizeTextForComparison(text) {
+ return text
+ .replace(/\r?\n|\r/g, "\n")
+ .replace(/\s/g, " ")
+ .replace(/\s{2,}/g, " ");
+ }
+ getElementBoundaries(selection) {
+ let coordinates = null;
+ if (selection.rangeCount > 0) {
+ // The coordinates returned by getBoundingClientRect() are relative to the
+ // viewport, not the document.
+ const rect = selection.getRangeAt(0).getBoundingClientRect();
+ const scrollTop = window.pageYOffset;
+ coordinates = {
+ bottom: rect.bottom + scrollTop,
+ left: rect.left,
+ right: rect.right,
+ top: rect.top + scrollTop,
+ };
+ }
+ return coordinates;
+ }
+ initCopyQuote(supportDirectInsert) {
+ const copyQuote = document.getElementById("quoteManagerCopy");
+ copyQuote === null || copyQuote === void 0 ? void 0 : copyQuote.remove();
+ this.copyQuote.id = "quoteManagerCopy";
+ this.copyQuote.classList.add("balloonTooltip", "interactive");
+ const buttonSaveQuote = document.createElement("span");
+ buttonSaveQuote.classList.add("jsQuoteManagerStore");
+ buttonSaveQuote.textContent = Language.get("wcf.message.quote.quoteSelected");
+ buttonSaveQuote.addEventListener("click", (event) => this.saveQuote(event));
+ this.copyQuote.appendChild(buttonSaveQuote);
+ if (supportDirectInsert) {
+ const buttonSaveAndInsertQuote = document.createElement("span");
+ buttonSaveAndInsertQuote.classList.add("jsQuoteManagerQuoteAndInsert");
+ buttonSaveAndInsertQuote.textContent = Language.get("wcf.message.quote.quoteAndReply");
+ buttonSaveAndInsertQuote.addEventListener("click", (event) => this.saveAndInsertQuote(event));
+ this.copyQuote.appendChild(buttonSaveAndInsertQuote);
+ }
+ document.body.appendChild(this.copyQuote);
+ }
+ getSelectedText() {
+ const selection = window.getSelection();
+ if (selection.rangeCount) {
+ return this.getNodeText(selection.getRangeAt(0).cloneContents());
+ }
+ return "";
+ }
+ saveFullQuote(event) {
+ event.preventDefault();
+ const listItem = event.currentTarget;
+ Ajax.api(this, {
+ actionName: "saveFullQuote",
+ objectIDs: [listItem.dataset.objectId],
+ });
+ // mark element as quoted
+ const quoteLink = listItem.querySelector("a");
+ if (Core.stringToBool(listItem.dataset.isQuoted || "")) {
+ listItem.dataset.isQuoted = "false";
+ quoteLink.classList.remove("active");
+ }
+ else {
+ listItem.dataset.isQuoted = "true";
+ quoteLink.classList.add("active");
+ }
+ // close navigation on mobile
+ const navigationList = listItem.closest(".buttonGroupNavigation");
+ if (navigationList.classList.contains("jsMobileButtonGroupNavigation")) {
+ const dropDownLabel = navigationList.querySelector(".dropdownLabel");
+ dropDownLabel.click();
+ }
+ }
+ saveQuote(event, renderQuote = false) {
+ event === null || event === void 0 ? void 0 : event.preventDefault();
+ Ajax.api(this, {
+ actionName: "saveQuote",
+ objectIDs: [this.objectId],
+ parameters: {
+ message: this.message,
+ renderQuote,
+ },
+ });
+ const selection = window.getSelection();
+ if (selection.rangeCount) {
+ selection.removeAllRanges();
+ this.copyQuote.classList.remove("active");
+ }
+ }
+ saveAndInsertQuote(event) {
+ event.preventDefault();
+ this.saveQuote(undefined, true);
+ }
+ _ajaxSuccess(data) {
+ if (data.returnValues.count !== undefined) {
+ if (data.returnValues.fullQuoteMessageIDs !== undefined) {
+ data.returnValues.fullQuoteObjectIDs = data.returnValues.fullQuoteMessageIDs;
+ }
+ const fullQuoteObjectIDs = data.returnValues.fullQuoteObjectIDs || {};
+ this.quoteManager.updateCount(data.returnValues.count, fullQuoteObjectIDs);
+ }
+ switch (data.actionName) {
+ case "saveQuote":
+ case "saveFullQuote":
+ if (data.returnValues.renderedQuote) {
+ EventHandler.fire("com.woltlab.wcf.message.quote", "insert", {
+ forceInsert: data.actionName === "saveQuote",
+ quote: data.returnValues.renderedQuote,
+ });
+ }
+ break;
+ }
+ }
+ _ajaxSetup() {
+ return {
+ data: {
+ className: this.className,
+ interfaceName: "wcf\\data\\IMessageQuoteAction",
+ },
+ };
+ }
+ /**
+ * Updates the full quote data for all matching objects.
+ */
+ updateFullQuoteObjectIDs(objectIds) {
+ this.containers.forEach((message) => {
+ const quoteButton = message.querySelector(".jsQuoteMessage");
+ quoteButton.dataset.isQuoted = "false";
+ const quoteButtonLink = quoteButton.querySelector("a");
+ quoteButton.classList.remove("active");
+ const objectId = ~~quoteButton.dataset.objectID;
+ if (objectIds.includes(objectId)) {
+ quoteButton.dataset.isQuoted = "true";
+ quoteButtonLink.classList.add("active");
+ }
+ });
+ }
+ }
+ exports.UiMessageQuote = UiMessageQuote;
+ exports.default = UiMessageQuote;
+});
return innerError as HTMLElement | null;
},
+
+ /**
+ * Finds the closest element that matches the provided selector. This is a helper
+ * function because `closest()` does exist on elements only, for example, it is
+ * missing on text nodes.
+ */
+ closest(node: Node, selector: string): HTMLElement | null {
+ const element = node instanceof HTMLElement ? node : node.parentElement!;
+ return element.closest(selector);
+ },
+
+ /**
+ * Returns the `node` if it is an element or its parent. This is useful when working
+ * with the range of a text selection.
+ */
+ getClosestElement(node: Node): HTMLElement {
+ return node instanceof HTMLElement ? node : node.parentElement!;
+ },
};
interface Dimensions {
--- /dev/null
+import * as Ajax from "../../Ajax";
+import * as Core from "../../Core";
+import * as EventHandler from "../../Event/Handler";
+import * as Language from "../../Language";
+import DomChangeListener from "../../Dom/Change/Listener";
+import DomUtil from "../../Dom/Util";
+import { AjaxCallbackObject, AjaxCallbackSetup } from "../../Ajax/Data";
+
+interface AjaxResponse {
+ actionName: string;
+ returnValues: {
+ count?: number;
+ fullQuoteMessageIDs?: unknown;
+ fullQuoteObjectIDs?: unknown;
+ renderedQuote?: string;
+ };
+}
+
+interface ElementBoundaries {
+ bottom: number;
+ left: number;
+ right: number;
+ top: number;
+}
+
+export class UiMessageQuote implements AjaxCallbackObject {
+ private activeMessageId = "";
+
+ private readonly className: string;
+
+ private containers = new Map<string, HTMLElement>();
+
+ private containerSelector = "";
+
+ private readonly copyQuote = document.createElement("div");
+
+ private message = "";
+
+ private readonly messageBodySelector: string;
+
+ private objectId = 0;
+
+ private objectType = "";
+
+ private timerSelectionChange?: number = undefined;
+
+ private isMouseDown = false;
+
+ private readonly quoteManager: any;
+
+ /**
+ * Initializes the quote handler for given object type.
+ */
+ constructor(
+ quoteManager: any, // TODO
+ className: string,
+ objectType: string,
+ containerSelector: string,
+ messageBodySelector: string,
+ messageContentSelector: string,
+ supportDirectInsert: boolean,
+ ) {
+ this.className = className;
+ this.objectType = objectType;
+ this.containerSelector = containerSelector;
+ this.messageBodySelector = messageBodySelector;
+
+ this.initContainers();
+
+ supportDirectInsert = supportDirectInsert && quoteManager.supportPaste();
+ this.quoteManager = quoteManager;
+ this.initCopyQuote(supportDirectInsert);
+
+ document.addEventListener("mouseup", (event) => this.onMouseUp(event));
+ document.addEventListener("selectionchange", () => this.onSelectionchange());
+
+ DomChangeListener.add("UiMessageQuote", () => this.initContainers());
+
+ // Prevent the tooltip from being selectable while the touch pointer is being moved.
+ document.addEventListener(
+ "touchstart",
+ (event) => {
+ if (this.copyQuote.classList.contains("active")) {
+ const target = event.target as HTMLElement;
+ if (target !== this.copyQuote && !this.copyQuote.contains(target)) {
+ this.copyQuote.classList.add("touchForceInaccessible");
+
+ document.addEventListener(
+ "touchend",
+ () => {
+ this.copyQuote.classList.remove("touchForceInaccessible");
+ },
+ { once: true },
+ );
+ }
+ }
+ },
+ { passive: true },
+ );
+ }
+
+ /**
+ * Initializes message containers.
+ */
+ private initContainers(): void {
+ document.querySelectorAll(this.containerSelector).forEach((container: HTMLElement) => {
+ const id = DomUtil.identify(container);
+ if (this.containers.has(id)) {
+ return;
+ }
+
+ this.containers.set(id, container);
+ if (container.classList.contains("jsInvalidQuoteTarget")) {
+ return;
+ }
+
+ container.addEventListener("mousedown", (event) => this.onMouseDown(event));
+ container.classList.add("jsQuoteMessageContainer");
+
+ container
+ .querySelector(".jsQuoteMessage")
+ ?.addEventListener("click", (event: MouseEvent) => this.saveFullQuote(event));
+ });
+ }
+
+ private onSelectionchange(): void {
+ if (this.isMouseDown) {
+ return;
+ }
+
+ if (this.activeMessageId === "") {
+ // check if the selection is non-empty and is entirely contained
+ // inside a single message container that is registered for quoting
+ const selection = window.getSelection()!;
+ if (selection.rangeCount !== 1 || selection.isCollapsed) {
+ return;
+ }
+
+ const range = selection.getRangeAt(0);
+ const startContainer = DomUtil.closest(range.startContainer, ".jsQuoteMessageContainer");
+ const endContainer = DomUtil.closest(range.endContainer, ".jsQuoteMessageContainer");
+ if (
+ startContainer &&
+ startContainer === endContainer &&
+ !startContainer.classList.contains("jsInvalidQuoteTarget")
+ ) {
+ // Check if the selection is visible, such as text marked inside containers with an
+ // active overflow handling attached to it. This can be a side effect of the browser
+ // search which modifies the text selection, but cannot be distinguished from manual
+ // selections initiated by the user.
+ let commonAncestor = range.commonAncestorContainer as HTMLElement;
+ if (commonAncestor.nodeType !== Node.ELEMENT_NODE) {
+ commonAncestor = commonAncestor.parentElement!;
+ }
+
+ const offsetParent = commonAncestor.offsetParent!;
+ if (startContainer.contains(offsetParent)) {
+ if (offsetParent.scrollTop + offsetParent.clientHeight < commonAncestor.offsetTop) {
+ // The selected text is not visible to the user.
+ return;
+ }
+ }
+
+ this.activeMessageId = startContainer.id;
+ }
+ }
+
+ if (this.timerSelectionChange) {
+ window.clearTimeout(this.timerSelectionChange);
+ }
+
+ this.timerSelectionChange = window.setTimeout(() => this.onMouseUp(), 100);
+ }
+
+ private onMouseDown(event: MouseEvent): void {
+ // hide copy quote
+ this.copyQuote.classList.remove("active");
+
+ const message = event.currentTarget as HTMLElement;
+ this.activeMessageId = message.classList.contains("jsInvalidQuoteTarget") ? "" : message.id;
+
+ if (this.timerSelectionChange) {
+ window.clearTimeout(this.timerSelectionChange);
+ this.timerSelectionChange = undefined;
+ }
+
+ this.isMouseDown = true;
+ }
+
+ /**
+ * Returns the text of a node and its children.
+ */
+ private getNodeText(node: Node): string {
+ const treeWalker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, {
+ acceptNode(node: Node): number {
+ if (node.nodeName === "BLOCKQUOTE" || node.nodeName === "SCRIPT") {
+ return NodeFilter.FILTER_REJECT;
+ }
+
+ if (node instanceof HTMLImageElement) {
+ // Skip any image that is not a smiley or contains no alt text.
+ if (!node.classList.contains("smiley") || !node.alt) {
+ return NodeFilter.FILTER_REJECT;
+ }
+ }
+
+ return NodeFilter.FILTER_ACCEPT;
+ },
+ });
+
+ let text = "";
+ const ignoreLinks: HTMLAnchorElement[] = [];
+ while (treeWalker.nextNode()) {
+ const node = treeWalker.currentNode as HTMLElement | Text;
+
+ if (node instanceof Text) {
+ const parent = node.parentElement!;
+ if (parent instanceof HTMLAnchorElement && ignoreLinks.includes(parent)) {
+ // ignore text content of links that have already been captured
+ continue;
+ }
+
+ // Firefox loves to arbitrarily wrap pasted text at weird line lengths, causing
+ // pointless linebreaks to be inserted. Replacing them with a simple space will
+ // preserve the spacing between words that would otherwise be lost.
+ text += node.nodeValue!.replace(/\n/g, " ");
+
+ continue;
+ }
+
+ if (node instanceof HTMLAnchorElement) {
+ // \u2026 === …
+ const value = node.textContent!;
+ if (value.indexOf("\u2026") > 0) {
+ const tmp = value.split(/\u2026/);
+ if (tmp.length === 2) {
+ const href = node.href;
+ if (href.indexOf(tmp[0]) === 0 && href.substr(tmp[1].length * -1) === tmp[1]) {
+ // This is a truncated url, use the original href instead to preserve the link.
+ text += href;
+ ignoreLinks.push(node);
+ }
+ }
+ }
+ }
+
+ switch (node.nodeName) {
+ case "BR":
+ case "LI":
+ case "TD":
+ case "UL":
+ text += "\n";
+ break;
+
+ case "P":
+ text += "\n\n";
+ break;
+
+ // smilies
+ case "IMG": {
+ const img = node as HTMLImageElement;
+ text += ` ${img.alt} `;
+ break;
+ }
+
+ // Code listing
+ case "DIV":
+ if (node.classList.contains("codeBoxHeadline") || node.classList.contains("codeBoxLine")) {
+ text += "\n";
+ }
+ break;
+ }
+ }
+
+ return text;
+ }
+
+ private onMouseUp(event?: MouseEvent): void {
+ if (event instanceof Event) {
+ if (this.timerSelectionChange) {
+ // Prevent collisions of the `selectionchange` and the `mouseup` event.
+ window.clearTimeout(this.timerSelectionChange);
+ this.timerSelectionChange = undefined;
+ }
+
+ this.isMouseDown = false;
+ }
+
+ // ignore event
+ if (this.activeMessageId === "") {
+ this.copyQuote.classList.remove("active");
+
+ return;
+ }
+
+ const selection = window.getSelection()!;
+ if (selection.rangeCount !== 1 || selection.isCollapsed) {
+ this.copyQuote.classList.remove("active");
+
+ return;
+ }
+
+ const container = this.containers.get(this.activeMessageId)!;
+ const objectId = ~~container.dataset.objectId!;
+ const content = this.messageBodySelector
+ ? (container.querySelector(this.messageBodySelector)! as HTMLElement)
+ : container;
+
+ let anchorNode = selection.anchorNode;
+ while (anchorNode) {
+ if (anchorNode === content) {
+ break;
+ }
+
+ anchorNode = anchorNode.parentNode;
+ }
+
+ // selection spans unrelated nodes
+ if (anchorNode !== content) {
+ this.copyQuote.classList.remove("active");
+
+ return;
+ }
+
+ const selectedText = this.getSelectedText();
+ const text = selectedText.trim();
+ if (text === "") {
+ this.copyQuote.classList.remove("active");
+
+ return;
+ }
+
+ // check if mousedown/mouseup took place inside a blockquote
+ const range = selection.getRangeAt(0);
+ const startContainer = DomUtil.getClosestElement(range.startContainer);
+ const endContainer = DomUtil.getClosestElement(range.endContainer);
+ if (startContainer.closest("blockquote") || endContainer.closest("blockquote")) {
+ this.copyQuote.classList.remove("active");
+
+ return;
+ }
+
+ // compare selection with message text of given container
+ const messageText = this.getNodeText(content);
+
+ // selected text is not part of $messageText or contains text from unrelated nodes
+ if (!this.normalizeTextForComparison(messageText).includes(this.normalizeTextForComparison(text))) {
+ return;
+ }
+
+ this.copyQuote.classList.add("active");
+
+ const coordinates = this.getElementBoundaries(selection)!;
+ const dimensions = { height: this.copyQuote.offsetHeight, width: this.copyQuote.offsetWidth };
+ let left = (coordinates.right - coordinates.left) / 2 - dimensions.width / 2 + coordinates.left;
+
+ // Prevent the overlay from overflowing the left or right boundary of the container.
+ const containerBoundaries = content.getBoundingClientRect();
+ if (left < containerBoundaries.left) {
+ left = containerBoundaries.left;
+ } else if (left + dimensions.width > containerBoundaries.right) {
+ left = containerBoundaries.right - dimensions.width;
+ }
+
+ this.copyQuote.style.setProperty("top", `${coordinates.bottom + 7}px`);
+ this.copyQuote.style.setProperty("left", `${left}px`);
+ this.copyQuote.classList.remove("active");
+
+ if (!this.timerSelectionChange) {
+ // reset containerID
+ this.activeMessageId = "";
+ } else {
+ window.clearTimeout(this.timerSelectionChange);
+ this.timerSelectionChange = undefined;
+ }
+
+ // show element after a delay, to prevent display if text was unmarked again (clicking into marked text)
+ window.setTimeout(() => {
+ const text = this.getSelectedText().trim();
+ if (text !== "") {
+ this.copyQuote.classList.add("active");
+ this.message = text;
+ this.objectId = objectId;
+ }
+ }, 10);
+ }
+
+ private normalizeTextForComparison(text: string): string {
+ return text
+ .replace(/\r?\n|\r/g, "\n")
+ .replace(/\s/g, " ")
+ .replace(/\s{2,}/g, " ");
+ }
+
+ private getElementBoundaries(selection: Selection): ElementBoundaries | null {
+ let coordinates: ElementBoundaries | null = null;
+
+ if (selection.rangeCount > 0) {
+ // The coordinates returned by getBoundingClientRect() are relative to the
+ // viewport, not the document.
+ const rect = selection.getRangeAt(0).getBoundingClientRect();
+
+ const scrollTop = window.pageYOffset;
+ coordinates = {
+ bottom: rect.bottom + scrollTop,
+ left: rect.left,
+ right: rect.right,
+ top: rect.top + scrollTop,
+ };
+ }
+
+ return coordinates;
+ }
+
+ private initCopyQuote(supportDirectInsert: boolean): void {
+ const copyQuote = document.getElementById("quoteManagerCopy");
+ copyQuote?.remove();
+
+ this.copyQuote.id = "quoteManagerCopy";
+ this.copyQuote.classList.add("balloonTooltip", "interactive");
+
+ const buttonSaveQuote = document.createElement("span");
+ buttonSaveQuote.classList.add("jsQuoteManagerStore");
+ buttonSaveQuote.textContent = Language.get("wcf.message.quote.quoteSelected");
+ buttonSaveQuote.addEventListener("click", (event) => this.saveQuote(event));
+ this.copyQuote.appendChild(buttonSaveQuote);
+
+ if (supportDirectInsert) {
+ const buttonSaveAndInsertQuote = document.createElement("span");
+ buttonSaveAndInsertQuote.classList.add("jsQuoteManagerQuoteAndInsert");
+ buttonSaveAndInsertQuote.textContent = Language.get("wcf.message.quote.quoteAndReply");
+ buttonSaveAndInsertQuote.addEventListener("click", (event) => this.saveAndInsertQuote(event));
+ this.copyQuote.appendChild(buttonSaveAndInsertQuote);
+ }
+
+ document.body.appendChild(this.copyQuote);
+ }
+
+ private getSelectedText(): string {
+ const selection = window.getSelection()!;
+ if (selection.rangeCount) {
+ return this.getNodeText(selection.getRangeAt(0).cloneContents());
+ }
+
+ return "";
+ }
+
+ private saveFullQuote(event: MouseEvent): void {
+ event.preventDefault();
+
+ const listItem = event.currentTarget as HTMLElement;
+
+ Ajax.api(this, {
+ actionName: "saveFullQuote",
+ objectIDs: [listItem.dataset.objectId],
+ });
+
+ // mark element as quoted
+ const quoteLink = listItem.querySelector("a")!;
+ if (Core.stringToBool(listItem.dataset.isQuoted || "")) {
+ listItem.dataset.isQuoted = "false";
+ quoteLink.classList.remove("active");
+ } else {
+ listItem.dataset.isQuoted = "true";
+ quoteLink.classList.add("active");
+ }
+
+ // close navigation on mobile
+ const navigationList = listItem.closest(".buttonGroupNavigation") as HTMLUListElement;
+ if (navigationList.classList.contains("jsMobileButtonGroupNavigation")) {
+ const dropDownLabel = navigationList.querySelector(".dropdownLabel") as HTMLElement;
+ dropDownLabel.click();
+ }
+ }
+
+ private saveQuote(event?: MouseEvent, renderQuote = false) {
+ event?.preventDefault();
+
+ Ajax.api(this, {
+ actionName: "saveQuote",
+ objectIDs: [this.objectId],
+ parameters: {
+ message: this.message,
+ renderQuote,
+ },
+ });
+
+ const selection = window.getSelection()!;
+ if (selection.rangeCount) {
+ selection.removeAllRanges();
+ this.copyQuote.classList.remove("active");
+ }
+ }
+
+ private saveAndInsertQuote(event: MouseEvent) {
+ event.preventDefault();
+
+ this.saveQuote(undefined, true);
+ }
+
+ _ajaxSuccess(data: AjaxResponse): void {
+ if (data.returnValues.count !== undefined) {
+ if (data.returnValues.fullQuoteMessageIDs !== undefined) {
+ data.returnValues.fullQuoteObjectIDs = data.returnValues.fullQuoteMessageIDs;
+ }
+
+ const fullQuoteObjectIDs = data.returnValues.fullQuoteObjectIDs || {};
+ this.quoteManager.updateCount(data.returnValues.count, fullQuoteObjectIDs);
+ }
+
+ switch (data.actionName) {
+ case "saveQuote":
+ case "saveFullQuote":
+ if (data.returnValues.renderedQuote) {
+ EventHandler.fire("com.woltlab.wcf.message.quote", "insert", {
+ forceInsert: data.actionName === "saveQuote",
+ quote: data.returnValues.renderedQuote,
+ });
+ }
+ break;
+ }
+ }
+
+ _ajaxSetup(): ReturnType<AjaxCallbackSetup> {
+ return {
+ data: {
+ className: this.className,
+ interfaceName: "wcf\\data\\IMessageQuoteAction",
+ },
+ };
+ }
+
+ /**
+ * Updates the full quote data for all matching objects.
+ */
+ updateFullQuoteObjectIDs(objectIds: number[]): void {
+ this.containers.forEach((message) => {
+ const quoteButton = message.querySelector(".jsQuoteMessage") as HTMLLIElement;
+ quoteButton.dataset.isQuoted = "false";
+
+ const quoteButtonLink = quoteButton.querySelector("a")!;
+ quoteButton.classList.remove("active");
+
+ const objectId = ~~quoteButton.dataset.objectID!;
+ if (objectIds.includes(objectId)) {
+ quoteButton.dataset.isQuoted = "true";
+ quoteButtonLink.classList.add("active");
+ }
+ });
+ }
+}
+
+export default UiMessageQuote;