* @copyright 2001-2024 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @since 6.2
+ * @woltlabExcludeBundle tiny
*/
+
import * as Core from "WoltLabSuite/Core/Core";
import { listenToCkeditor } from "WoltLabSuite/Core/Component/Ckeditor/Event";
import type { CKEditor } from "WoltLabSuite/Core/Component/Ckeditor";
import { getTabMenu } from "WoltLabSuite/Core/Component/Message/MessageTabMenu";
import { getPhrase } from "WoltLabSuite/Core/Language";
+import { setActiveEditor } from "WoltLabSuite/Core/Component/Quote/Message";
export const STORAGE_KEY = Core.getStoragePrefix() + "quotes";
const quoteLists = new Map<string, QuoteList>();
listenToCkeditor(editor).ready(({ ckeditor }) => {
if (ckeditor.features.quoteBlock) {
quoteLists.set(editorId, new QuoteList(editorId, ckeditor));
+
+ setActiveEditor(ckeditor, true);
+ } else {
+ setActiveEditor(ckeditor, false);
}
+
+ //TODO handle active editor changed
});
}
--- /dev/null
+/**
+ * Handles quotes selection in messages.
+ *
+ * @author Olaf Braun
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ * @woltlabExcludeBundle tiny
+ */
+
+import DomUtil from "WoltLabSuite/Core/Dom/Util";
+import { getPhrase } from "WoltLabSuite/Core/Language";
+import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector";
+import { set as setAlignment } from "WoltLabSuite/Core/Ui/Alignment";
+import { CKEditor } from "WoltLabSuite/Core/Component/Ckeditor";
+
+interface Container {
+ element: HTMLElement;
+ messageBodySelector: string;
+}
+
+const containers = new Map<string, Container>();
+let activeMessageId = "";
+let message = "";
+let activeEditor: CKEditor | undefined = undefined;
+let timerSelectionChange: number | undefined = undefined;
+let isMouseDown = false;
+let objectId: number | undefined = undefined;
+const copyQuote = document.createElement("div");
+
+export function registerContainer(containerSelector: string, messageBodySelector: string): void {
+ wheneverFirstSeen(containerSelector, (container: HTMLElement) => {
+ const id = DomUtil.identify(container);
+ containers.set(id, {
+ element: container,
+ messageBodySelector: messageBodySelector,
+ });
+
+ if (container.classList.contains("jsInvalidQuoteTarget")) {
+ return;
+ }
+
+ container.addEventListener("mousedown", (event) => onMouseDown(event));
+ container.classList.add("jsQuoteMessageContainer");
+
+ container.querySelector(".jsQuoteMessage")?.addEventListener("click", () => {
+ //TODO
+ });
+ });
+}
+
+export function setActiveEditor(editor: CKEditor, supportDirectInsert: boolean) {
+ copyQuote.querySelector<HTMLButtonElement>(".jsQuoteManagerQuoteAndInsert")!.hidden = !supportDirectInsert;
+
+ activeEditor = editor;
+}
+
+function setup() {
+ copyQuote.classList.add("balloonTooltip", "interactive", "quoteManagerCopy");
+
+ const buttonSaveQuote = document.createElement("button");
+ buttonSaveQuote.type = "button";
+ buttonSaveQuote.classList.add("jsQuoteManagerStore");
+ buttonSaveQuote.textContent = getPhrase("wcf.message.quote.quoteSelected");
+ buttonSaveQuote.addEventListener("click", () => {
+ //TODO
+ });
+ copyQuote.appendChild(buttonSaveQuote);
+ const buttonSaveAndInsertQuote = document.createElement("button");
+ buttonSaveAndInsertQuote.type = "button";
+ buttonSaveAndInsertQuote.hidden = true;
+ buttonSaveAndInsertQuote.classList.add("jsQuoteManagerQuoteAndInsert");
+ buttonSaveAndInsertQuote.textContent = getPhrase("wcf.message.quote.quoteAndReply");
+ buttonSaveAndInsertQuote.addEventListener("click", () => {
+ //TODO
+ });
+ copyQuote.appendChild(buttonSaveAndInsertQuote);
+
+ document.body.appendChild(copyQuote);
+
+ document.addEventListener("mouseup", (event) => onMouseUp(event));
+ document.addEventListener("selectionchange", () => onSelectionchange());
+
+ // Prevent the tooltip from being selectable while the touch pointer is being moved.
+ document.addEventListener(
+ "touchstart",
+ (event) => {
+ const target = event.target as HTMLElement;
+ if (target !== copyQuote && !copyQuote.contains(target)) {
+ copyQuote.classList.add("touchForceInaccessible");
+
+ document.addEventListener(
+ "touchend",
+ () => {
+ copyQuote.classList.remove("touchForceInaccessible");
+ },
+ { once: true, passive: false },
+ );
+ }
+ },
+ { passive: false },
+ );
+
+ window.addEventListener(
+ "resize",
+ () => {
+ copyQuote.classList.remove("active");
+ },
+ { passive: true },
+ );
+}
+
+setup();
+
+function getSelectedText(): string {
+ const selection = window.getSelection()!;
+ if (selection.rangeCount) {
+ return getNodeText(selection.getRangeAt(0).cloneContents());
+ }
+
+ return "";
+}
+
+/**
+ * Returns the text of a node and its children.
+ */
+function 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.substring(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;
+}
+
+function normalizeTextForComparison(text: string): string {
+ return text
+ .replace(/\r?\n|\r/g, "\n")
+ .replace(/\s/g, " ")
+ .replace(/\s{2,}/g, " ");
+}
+
+function onSelectionchange(): void {
+ if (isMouseDown) {
+ return;
+ }
+
+ if (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;
+ }
+ }
+
+ activeMessageId = startContainer.id;
+ }
+ }
+
+ if (timerSelectionChange) {
+ window.clearTimeout(timerSelectionChange);
+ }
+
+ timerSelectionChange = window.setTimeout(() => onMouseUp(), 100);
+}
+
+function onMouseDown(event: MouseEvent): void {
+ // hide copy quote
+ copyQuote.classList.remove("active");
+
+ const message = event.currentTarget as HTMLElement;
+ activeMessageId = message.classList.contains("jsInvalidQuoteTarget") ? "" : message.id;
+
+ if (timerSelectionChange) {
+ window.clearTimeout(timerSelectionChange);
+ timerSelectionChange = undefined;
+ }
+
+ isMouseDown = true;
+}
+
+function onMouseUp(event?: MouseEvent): void {
+ if (event instanceof Event) {
+ if (timerSelectionChange) {
+ // Prevent collisions of the `selectionchange` and the `mouseup` event.
+ window.clearTimeout(timerSelectionChange);
+ timerSelectionChange = undefined;
+ }
+
+ isMouseDown = false;
+ }
+
+ // ignore event
+ if (activeMessageId === "") {
+ copyQuote.classList.remove("active");
+
+ return;
+ }
+
+ const selection = window.getSelection()!;
+ if (selection.rangeCount !== 1 || selection.isCollapsed) {
+ copyQuote.classList.remove("active");
+
+ return;
+ }
+
+ const container = containers.get(activeMessageId);
+ if (container === undefined) {
+ // Since 5.4 we listen for global mouse events, because those are much
+ // more reliable on mobile devices. However, this can cause conflicts
+ // if two or more types of message types with quote support coexist on
+ // the same page.
+ return;
+ }
+
+ const content = container.messageBodySelector
+ ? (container.element.querySelector(container.messageBodySelector) as HTMLElement)
+ : container;
+
+ let anchorNode = selection.anchorNode;
+ while (anchorNode) {
+ if (anchorNode === content) {
+ break;
+ }
+
+ anchorNode = anchorNode.parentNode;
+ }
+
+ // selection spans unrelated nodes
+ if (anchorNode !== content) {
+ copyQuote.classList.remove("active");
+
+ return;
+ }
+
+ const selectedText = getSelectedText();
+ const text = selectedText.trim();
+ if (text === "") {
+ 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")) {
+ copyQuote.classList.remove("active");
+
+ return;
+ }
+
+ // compare selection with message text of given container
+ const messageText = getNodeText(content);
+
+ // selected text is not part of $messageText or contains text from unrelated nodes
+ if (!normalizeTextForComparison(messageText).includes(normalizeTextForComparison(text))) {
+ return;
+ }
+
+ copyQuote.classList.add("active");
+ const wasInaccessible = copyQuote.classList.contains("touchForceInaccessible");
+ if (wasInaccessible) {
+ copyQuote.classList.remove("touchForceInaccessible");
+ }
+
+ setAlignment(copyQuote, endContainer);
+
+ copyQuote.classList.remove("active");
+ if (wasInaccessible) {
+ copyQuote.classList.add("touchForceInaccessible");
+ }
+
+ if (!timerSelectionChange) {
+ // reset containerID
+ activeMessageId = "";
+ } else {
+ window.clearTimeout(timerSelectionChange);
+ timerSelectionChange = undefined;
+ }
+
+ // show element after a delay, to prevent display if text was unmarked again (clicking into marked text)
+ window.setTimeout(() => {
+ const text = getSelectedText().trim();
+ if (text !== "") {
+ copyQuote.classList.add("active");
+ message = text;
+ objectId = ~~container.element.dataset.objectId!;
+ }
+ }, 10);
+}
/**
* @woltlabExcludeBundle tiny
+ *
+ * @deprecated 6.2 use `WoltLabSuite/Core/Component/Quote/Message` instead
*/
-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;
-}
+import { registerContainer } from "WoltLabSuite/Core/Component/Quote/Message";
// see WCF.Message.Quote.Manager
export interface WCFMessageQuoteManager {
updateCount: (number, object) => void;
}
-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: WCFMessageQuoteManager;
-
+export class UiMessageQuote {
/**
* Initializes the quote handler for given object type.
*/
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) => {
- 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: false },
- );
- }
- },
- { passive: false },
- );
-
- window.addEventListener(
- "resize",
- () => {
- this.copyQuote.classList.remove("active");
- },
- { 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);
- if (container === undefined) {
- // Since 5.4 we listen for global mouse events, because those are much
- // more reliable on mobile devices. However, this can cause conflicts
- // if two or more types of message types with quote support coexist on
- // the same page.
- return;
- }
-
- 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 wasInaccessible = this.copyQuote.classList.contains("touchForceInaccessible");
- if (wasInaccessible) {
- this.copyQuote.classList.remove("touchForceInaccessible");
- }
-
- 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 (wasInaccessible) {
- this.copyQuote.classList.add("touchForceInaccessible");
- }
-
- 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 {
- this.copyQuote.classList.add("balloonTooltip", "interactive", "quoteManagerCopy");
-
- 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: HTMLUListElement | null = listItem.closest(".buttonGroupNavigation");
- if (navigationList && 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");
- }
- });
+ registerContainer(containerSelector, messageBodySelector);
}
}
-define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/Core/Component/Ckeditor/Event", "WoltLabSuite/Core/Component/Message/MessageTabMenu", "WoltLabSuite/Core/Language"], function (require, exports, tslib_1, Core, Event_1, MessageTabMenu_1, Language_1) {
+/**
+ * Handles quotes for CKEditor 5 message fields.
+ *
+ * @author Olaf Braun
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ * @woltlabExcludeBundle tiny
+ */
+define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/Core/Component/Ckeditor/Event", "WoltLabSuite/Core/Component/Message/MessageTabMenu", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Component/Quote/Message"], function (require, exports, tslib_1, Core, Event_1, MessageTabMenu_1, Language_1, Message_1) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.STORAGE_KEY = void 0;
(0, Event_1.listenToCkeditor)(editor).ready(({ ckeditor }) => {
if (ckeditor.features.quoteBlock) {
quoteLists.set(editorId, new QuoteList(editorId, ckeditor));
+ (0, Message_1.setActiveEditor)(ckeditor, true);
}
+ else {
+ (0, Message_1.setActiveEditor)(ckeditor, false);
+ }
+ //TODO handle active editor changed
});
}
});
--- /dev/null
+/**
+ * Handles quotes selection in messages.
+ *
+ * @author Olaf Braun
+ * @copyright 2001-2024 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @since 6.2
+ * @woltlabExcludeBundle tiny
+ */
+define(["require", "exports", "tslib", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Helper/Selector", "WoltLabSuite/Core/Ui/Alignment"], function (require, exports, tslib_1, Util_1, Language_1, Selector_1, Alignment_1) {
+ "use strict";
+ Object.defineProperty(exports, "__esModule", { value: true });
+ exports.registerContainer = registerContainer;
+ exports.setActiveEditor = setActiveEditor;
+ Util_1 = tslib_1.__importDefault(Util_1);
+ const containers = new Map();
+ let activeMessageId = "";
+ let message = "";
+ let activeEditor = undefined;
+ let timerSelectionChange = undefined;
+ let isMouseDown = false;
+ let objectId = undefined;
+ const copyQuote = document.createElement("div");
+ function registerContainer(containerSelector, messageBodySelector) {
+ (0, Selector_1.wheneverFirstSeen)(containerSelector, (container) => {
+ const id = Util_1.default.identify(container);
+ containers.set(id, {
+ element: container,
+ messageBodySelector: messageBodySelector,
+ });
+ if (container.classList.contains("jsInvalidQuoteTarget")) {
+ return;
+ }
+ container.addEventListener("mousedown", (event) => onMouseDown(event));
+ container.classList.add("jsQuoteMessageContainer");
+ container.querySelector(".jsQuoteMessage")?.addEventListener("click", () => {
+ //TODO
+ });
+ });
+ }
+ function setActiveEditor(editor, supportDirectInsert) {
+ copyQuote.querySelector(".jsQuoteManagerQuoteAndInsert").hidden = !supportDirectInsert;
+ activeEditor = editor;
+ }
+ function setup() {
+ copyQuote.classList.add("balloonTooltip", "interactive", "quoteManagerCopy");
+ const buttonSaveQuote = document.createElement("button");
+ buttonSaveQuote.type = "button";
+ buttonSaveQuote.classList.add("jsQuoteManagerStore");
+ buttonSaveQuote.textContent = (0, Language_1.getPhrase)("wcf.message.quote.quoteSelected");
+ buttonSaveQuote.addEventListener("click", () => {
+ //TODO
+ });
+ copyQuote.appendChild(buttonSaveQuote);
+ const buttonSaveAndInsertQuote = document.createElement("button");
+ buttonSaveAndInsertQuote.type = "button";
+ buttonSaveAndInsertQuote.hidden = true;
+ buttonSaveAndInsertQuote.classList.add("jsQuoteManagerQuoteAndInsert");
+ buttonSaveAndInsertQuote.textContent = (0, Language_1.getPhrase)("wcf.message.quote.quoteAndReply");
+ buttonSaveAndInsertQuote.addEventListener("click", () => {
+ //TODO
+ });
+ copyQuote.appendChild(buttonSaveAndInsertQuote);
+ document.body.appendChild(copyQuote);
+ document.addEventListener("mouseup", (event) => onMouseUp(event));
+ document.addEventListener("selectionchange", () => onSelectionchange());
+ // Prevent the tooltip from being selectable while the touch pointer is being moved.
+ document.addEventListener("touchstart", (event) => {
+ const target = event.target;
+ if (target !== copyQuote && !copyQuote.contains(target)) {
+ copyQuote.classList.add("touchForceInaccessible");
+ document.addEventListener("touchend", () => {
+ copyQuote.classList.remove("touchForceInaccessible");
+ }, { once: true, passive: false });
+ }
+ }, { passive: false });
+ window.addEventListener("resize", () => {
+ copyQuote.classList.remove("active");
+ }, { passive: true });
+ }
+ setup();
+ function getSelectedText() {
+ const selection = window.getSelection();
+ if (selection.rangeCount) {
+ return getNodeText(selection.getRangeAt(0).cloneContents());
+ }
+ return "";
+ }
+ /**
+ * Returns the text of a node and its children.
+ */
+ function 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.substring(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;
+ }
+ function normalizeTextForComparison(text) {
+ return text
+ .replace(/\r?\n|\r/g, "\n")
+ .replace(/\s/g, " ")
+ .replace(/\s{2,}/g, " ");
+ }
+ function onSelectionchange() {
+ if (isMouseDown) {
+ return;
+ }
+ if (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;
+ }
+ }
+ activeMessageId = startContainer.id;
+ }
+ }
+ if (timerSelectionChange) {
+ window.clearTimeout(timerSelectionChange);
+ }
+ timerSelectionChange = window.setTimeout(() => onMouseUp(), 100);
+ }
+ function onMouseDown(event) {
+ // hide copy quote
+ copyQuote.classList.remove("active");
+ const message = event.currentTarget;
+ activeMessageId = message.classList.contains("jsInvalidQuoteTarget") ? "" : message.id;
+ if (timerSelectionChange) {
+ window.clearTimeout(timerSelectionChange);
+ timerSelectionChange = undefined;
+ }
+ isMouseDown = true;
+ }
+ function onMouseUp(event) {
+ if (event instanceof Event) {
+ if (timerSelectionChange) {
+ // Prevent collisions of the `selectionchange` and the `mouseup` event.
+ window.clearTimeout(timerSelectionChange);
+ timerSelectionChange = undefined;
+ }
+ isMouseDown = false;
+ }
+ // ignore event
+ if (activeMessageId === "") {
+ copyQuote.classList.remove("active");
+ return;
+ }
+ const selection = window.getSelection();
+ if (selection.rangeCount !== 1 || selection.isCollapsed) {
+ copyQuote.classList.remove("active");
+ return;
+ }
+ const container = containers.get(activeMessageId);
+ if (container === undefined) {
+ // Since 5.4 we listen for global mouse events, because those are much
+ // more reliable on mobile devices. However, this can cause conflicts
+ // if two or more types of message types with quote support coexist on
+ // the same page.
+ return;
+ }
+ const content = container.messageBodySelector
+ ? container.element.querySelector(container.messageBodySelector)
+ : container;
+ let anchorNode = selection.anchorNode;
+ while (anchorNode) {
+ if (anchorNode === content) {
+ break;
+ }
+ anchorNode = anchorNode.parentNode;
+ }
+ // selection spans unrelated nodes
+ if (anchorNode !== content) {
+ copyQuote.classList.remove("active");
+ return;
+ }
+ const selectedText = getSelectedText();
+ const text = selectedText.trim();
+ if (text === "") {
+ 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")) {
+ copyQuote.classList.remove("active");
+ return;
+ }
+ // compare selection with message text of given container
+ const messageText = getNodeText(content);
+ // selected text is not part of $messageText or contains text from unrelated nodes
+ if (!normalizeTextForComparison(messageText).includes(normalizeTextForComparison(text))) {
+ return;
+ }
+ copyQuote.classList.add("active");
+ const wasInaccessible = copyQuote.classList.contains("touchForceInaccessible");
+ if (wasInaccessible) {
+ copyQuote.classList.remove("touchForceInaccessible");
+ }
+ (0, Alignment_1.set)(copyQuote, endContainer);
+ copyQuote.classList.remove("active");
+ if (wasInaccessible) {
+ copyQuote.classList.add("touchForceInaccessible");
+ }
+ if (!timerSelectionChange) {
+ // reset containerID
+ activeMessageId = "";
+ }
+ else {
+ window.clearTimeout(timerSelectionChange);
+ timerSelectionChange = undefined;
+ }
+ // show element after a delay, to prevent display if text was unmarked again (clicking into marked text)
+ window.setTimeout(() => {
+ const text = getSelectedText().trim();
+ if (text !== "") {
+ copyQuote.classList.add("active");
+ message = text;
+ objectId = ~~container.element.dataset.objectId;
+ }
+ }, 10);
+ }
+});
/**
* @woltlabExcludeBundle tiny
+ *
+ * @deprecated 6.2 use `WoltLabSuite/Core/Component/Quote/Message` instead
*/
-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) {
+define(["require", "exports", "WoltLabSuite/Core/Component/Quote/Message"], function (require, exports, Message_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 {
- activeMessageId = "";
- className;
- containers = new Map();
- containerSelector = "";
- copyQuote = document.createElement("div");
- message = "";
- messageBodySelector;
- objectId = 0;
- objectType = "";
- timerSelectionChange = undefined;
- isMouseDown = false;
- quoteManager;
/**
* Initializes the quote handler for given object type.
*/
constructor(quoteManager, className, objectType, containerSelector, messageBodySelector, messageContentSelector, supportDirectInsert) {
- 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) => {
- 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: false });
- }
- }, { passive: false });
- window.addEventListener("resize", () => {
- this.copyQuote.classList.remove("active");
- }, { passive: true });
- }
- /**
- * Initializes message containers.
- */
- initContainers() {
- document.querySelectorAll(this.containerSelector).forEach((container) => {
- 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");
- container
- .querySelector(".jsQuoteMessage")
- ?.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);
- if (container === undefined) {
- // Since 5.4 we listen for global mouse events, because those are much
- // more reliable on mobile devices. However, this can cause conflicts
- // if two or more types of message types with quote support coexist on
- // the same page.
- return;
- }
- 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 wasInaccessible = this.copyQuote.classList.contains("touchForceInaccessible");
- if (wasInaccessible) {
- this.copyQuote.classList.remove("touchForceInaccessible");
- }
- 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 (wasInaccessible) {
- this.copyQuote.classList.add("touchForceInaccessible");
- }
- 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) {
- this.copyQuote.classList.add("balloonTooltip", "interactive", "quoteManagerCopy");
- 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 && navigationList.classList.contains("jsMobileButtonGroupNavigation")) {
- const dropDownLabel = navigationList.querySelector(".dropdownLabel");
- dropDownLabel.click();
- }
- }
- saveQuote(event, 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");
- }
- }
- 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");
- }
- });
+ (0, Message_1.registerContainer)(containerSelector, messageBodySelector);
}
}
exports.UiMessageQuote = UiMessageQuote;
&.interactive {
pointer-events: all;
- > span {
+ > span, button {
cursor: pointer;
&:not(:first-child) {