Mark `WoltLabSuite/Core/Ui/Message/Quote` as deprecated 6.2-quote-tab
authorCyperghost <olaf_schmitz_1@t-online.de>
Wed, 18 Dec 2024 12:52:45 +0000 (13:52 +0100)
committerCyperghost <olaf_schmitz_1@t-online.de>
Wed, 18 Dec 2024 12:52:45 +0000 (13:52 +0100)
ts/WoltLabSuite/Core/Component/Quote/List.ts
ts/WoltLabSuite/Core/Component/Quote/Message.ts [new file with mode: 0644]
ts/WoltLabSuite/Core/Ui/Message/Quote.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js [new file with mode: 0644]
wcfsetup/install/files/js/WoltLabSuite/Core/Ui/Message/Quote.js
wcfsetup/install/files/style/ui/tooltip.scss

index 2af4d50bc0335c868af275caaf723cfffadce048..a83d633b2cc842ca23ef11bc2569427dddaef096 100644 (file)
@@ -5,12 +5,15 @@
  * @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>();
@@ -68,6 +71,12 @@ export function setup(editorId: string): void {
   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
   });
 }
diff --git a/ts/WoltLabSuite/Core/Component/Quote/Message.ts b/ts/WoltLabSuite/Core/Component/Quote/Message.ts
new file mode 100644 (file)
index 0000000..a8c2665
--- /dev/null
@@ -0,0 +1,393 @@
+/**
+ * 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 === &hellip;
+      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);
+}
index 0487723d93dfaf1d68903bb4d68c232eb192839d..8a2ee184586e712c7f50987187dbb2baf1b9ddec 100644 (file)
@@ -1,31 +1,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 {
@@ -33,31 +12,7 @@ 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.
    */
@@ -70,510 +25,7 @@ export class UiMessageQuote implements AjaxCallbackObject {
     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 === &hellip;
-        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);
   }
 }
 
index 39105e436a20af298cd8302ceb364248c3d1551a..179fa611efe376f67411b37b391e4ac2de6b0289 100644 (file)
@@ -1,4 +1,13 @@
-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;
@@ -49,7 +58,12 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C
         (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
         });
     }
 });
diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Message.js
new file mode 100644 (file)
index 0000000..3068f6a
--- /dev/null
@@ -0,0 +1,311 @@
+/**
+ * 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 === &hellip;
+                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);
+    }
+});
index b0940925609449c4ea7aed85e63c532837380213..fa5784e7305a504d5ab7f38ad91d20f2fbd7a613 100644 (file)
 /**
  * @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 === &hellip;
-                    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;
index 202c3db064b2d1181ffe0258a87993b31409e202..ef2ccf7a1a32c0a74f6b7f176451981ac6790b34 100644 (file)
@@ -26,7 +26,7 @@
        &.interactive {
                pointer-events: all;
 
-               > span {
+               > span, button {
                        cursor: pointer;
 
                        &:not(:first-child) {