Present stored quotes using the existing UI design
authorAlexander Ebert <ebert@woltlab.com>
Mon, 6 Jan 2025 15:10:29 +0000 (16:10 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 8 Jan 2025 16:26:37 +0000 (17:26 +0100)
com.woltlab.wcf/templates/__messageFormQuote.tpl
com.woltlab.wcf/templates/__messageFormQuoteInline.tpl
ts/WoltLabSuite/Core/Component/Quote/List.ts
ts/WoltLabSuite/Core/Component/Quote/Storage.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/List.js
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Quote/Storage.js
wcfsetup/install/files/style/bbcode/quote.scss

index d4b635e1060efdf5799f807aaaed163eea056039..16c489f614065638234ec86bc57bf82c43f86a25 100644 (file)
@@ -1,11 +1,8 @@
-{* TODO *}
-
-<div id="quotes_{if $wysiwygSelector|isset}{$wysiwygSelector}{else}text{/if}" class="messageTabMenuContent">
-
-</div>
+<div id="quotes_{if $wysiwygSelector|isset}{$wysiwygSelector}{else}text{/if}"
+       class="messageTabMenuContent messageTabMenuContent--quotes"></div>
 
 <script data-relocate="true">
-  require(["WoltLabSuite/Core/Component/Quote/List"], ({ setup }) => {
+require(["WoltLabSuite/Core/Component/Quote/List"], ({ setup }) => {
        setup("{if $wysiwygSelector|isset}{$wysiwygSelector}{else}text{/if}");
-  });
-</script>
+});
+</script>
\ No newline at end of file
index d4b635e1060efdf5799f807aaaed163eea056039..16c489f614065638234ec86bc57bf82c43f86a25 100644 (file)
@@ -1,11 +1,8 @@
-{* TODO *}
-
-<div id="quotes_{if $wysiwygSelector|isset}{$wysiwygSelector}{else}text{/if}" class="messageTabMenuContent">
-
-</div>
+<div id="quotes_{if $wysiwygSelector|isset}{$wysiwygSelector}{else}text{/if}"
+       class="messageTabMenuContent messageTabMenuContent--quotes"></div>
 
 <script data-relocate="true">
-  require(["WoltLabSuite/Core/Component/Quote/List"], ({ setup }) => {
+require(["WoltLabSuite/Core/Component/Quote/List"], ({ setup }) => {
        setup("{if $wysiwygSelector|isset}{$wysiwygSelector}{else}text{/if}");
-  });
-</script>
+});
+</script>
\ No newline at end of file
index 680f905f7fbc12caef94d2bbd713bc2ac3cd4cae..fa2c558a0bfea43f8b9421e53c81a04e74b51a3b 100644 (file)
@@ -12,8 +12,9 @@ import { listenToCkeditor, dispatchToCkeditor } from "WoltLabSuite/Core/Componen
 import { getTabMenu } from "WoltLabSuite/Core/Component/Message/MessageTabMenu";
 import { getPhrase } from "WoltLabSuite/Core/Language";
 import { setActiveEditor } from "WoltLabSuite/Core/Component/Quote/Message";
-import { getQuotes, getMessage } from "WoltLabSuite/Core/Component/Quote/Storage";
+import { getQuotes, getMessage, removeQuote } from "WoltLabSuite/Core/Component/Quote/Storage";
 import DomUtil from "WoltLabSuite/Core/Dom/Util";
+import { escapeHTML } from "WoltLabSuite/Core/StringUtil";
 
 const quoteLists = new Map<string, QuoteList>();
 
@@ -43,67 +44,46 @@ class QuoteList {
     let quotesCount = 0;
     for (const [key, quotes] of getQuotes()) {
       const message = getMessage(key)!;
-      quotesCount += quotes.size;
-
-      // TODO escape values
-      // TODO create web components???
-      const fragment = DomUtil.createFragmentFromHtml(`<article class="message messageReduced jsInvalidQuoteTarget">
-  <div class="messageContent">
-    <header class="messageHeader">
-      <div class="box32 messageHeaderWrapper">
-        <!-- TODO load real avatar -->
-        <span><img src="${window.WCF_PATH}images/avatars/avatar-default.svg" alt="" class="userAvatarImage" style="width: 32px; height: 32px"></span>
-        <div class="messageHeaderBox">
-          <h2 class="messageTitle">
-            <a href="${message.link}">${message.title}</a>
-          </h2>
-          <ul class="messageHeaderMetaData">
-            <!-- TODO add link to author profile -->
-            <li><span class="username">${message.author}</span></li>
-            <li><span class="messagePublicationTime"><woltlab-core-date-time date="${message.time}">${message.time}</woltlab-core-date-time></span></li>
-          </ul>
-        </div>
-      </div>
-    </header>
-    <div class="messageBody">
-      <div class="messageText">
-        <ul class="messageQuoteItemList">
-        ${Array.from(quotes)
-          .map(
-            (quote) => `<li>
-  <span>
-    <input type="checkbox" value="1" class="jsCheckbox">
-    <button type="button" class="jsTooltip jsInsertQuote" title="${getPhrase("wcf.message.quote.insertQuote")}">
-        <fa-icon name="plus"></fa-icon>
+      quotesCount += quotes.length;
+
+      quotes.forEach((quote, index) => {
+        const fragment = DomUtil.createFragmentFromHtml(`
+<div class="quoteBox quoteBox--tabMenu">
+  <div class="quoteBoxIcon">
+    <img src="${escapeHTML(message.avatar)}" alt="" class="userAvatarImage" height="24" width="24">
+  </div>
+  <div class="quoteBoxTitle">
+    <a href="${escapeHTML(message.link)}" target="_blank">${escapeHTML(message.author)}</a>
+  </div>
+  <div class="quoteBoxButtons">
+    <button type="button" class="button small jsTooltip" title="${getPhrase("wcf.global.button.delete")}" data-action="delete">
+      <fa-icon name="times"></fa-icon>
+    </button>
+    <button type="button" class="button buttonPrimary small jsTooltip" title="${getPhrase("wcf.message.quote.insertQuote")}" data-action="insert">
+      <fa-icon name="paste"></fa-icon>
     </button>
-  </span>
-  
-  <div class="jsQuote">
-    ${quote.message}
   </div>
-</li>`,
-          )
-          .join("")}
-        </ul>
-      </div>
-    </div>
+  <div class="quoteBoxContent">
+    ${quote.rawMessage === undefined ? quote.message : quote.rawMessage}
   </div>
-</article>`);
+</div>
+        `);
 
-      // TODO dont query the DOM
-      fragment.querySelectorAll<HTMLButtonElement>(".jsInsertQuote").forEach((button) => {
-        button.addEventListener("click", () => {
-          // TODO use rawMessage to insert if available otherwise use message
+        fragment.querySelector('button[data-action="insert"]')!.addEventListener("click", () => {
           dispatchToCkeditor(this.#editor).insertQuote({
             author: message.author,
-            content: button.closest("li")!.querySelector(".jsQuote")!.innerHTML,
-            isText: false,
+            content: quote.rawMessage === undefined ? quote.message : quote.rawMessage,
+            isText: quote.rawMessage === undefined,
             link: message.link,
           });
         });
-      });
 
-      this.#container.append(fragment);
+        fragment.querySelector('button[data-action="delete"]')!.addEventListener("click", () => {
+          removeQuote(key, index);
+        });
+
+        this.#container.append(fragment);
+      });
     }
 
     if (quotesCount > 0) {
index 5a9413a1145b734616a42400b3aca56630c8fa38..0cc2e255bf0302706e0d0d3c7a91365771ed5de8 100644 (file)
@@ -29,7 +29,7 @@ interface Quote {
 }
 
 interface StorageData {
-  quotes: Map<string, Set<Quote>>;
+  quotes: Map<string, Quote[]>;
   messages: Map<string, Message>;
 }
 
@@ -76,7 +76,7 @@ export async function saveFullQuote(objectType: string, objectClassName: string,
   refreshQuoteLists();
 }
 
-export function getQuotes(): Map<string, Set<Quote>> {
+export function getQuotes(): Map<string, Quote[]> {
   return getStorage().quotes;
 }
 
@@ -86,17 +86,15 @@ export function getMessage(objectType: string, objectId?: number): Message | und
   return getStorage().messages.get(key);
 }
 
-export function removeQuote(objectType: string, objectId: number, quote: Quote): void {
+export function removeQuote(key: string, index: number): void {
   const storage = getStorage();
-
-  const key = getKey(objectType, objectId);
   if (!storage.quotes.has(key)) {
     return;
   }
 
-  storage.quotes.get(key)!.delete(quote);
+  storage.quotes.get(key)!.splice(index, 1);
 
-  if (storage.quotes.get(key)!.size === 0) {
+  if (storage.quotes.get(key)!.length === 0) {
     storage.quotes.delete(key);
     storage.messages.delete(key);
   }
@@ -111,18 +109,11 @@ function storeQuote(objectType: string, message: Message, quote: Quote): void {
 
   const key = getKey(objectType, message.objectID);
   if (!storage.quotes.has(key)) {
-    storage.quotes.set(key, new Set());
+    storage.quotes.set(key, []);
   }
 
   storage.messages.set(key, message);
-
-  if (
-    !Array.from(storage.quotes.get(key)!)
-      .map((q) => q.message)
-      .includes(quote.message)
-  ) {
-    storage.quotes.get(key)!.add(quote);
-  }
+  storage.quotes.get(key)!.push(quote);
 
   saveStorage(storage);
 }
@@ -137,11 +128,7 @@ function getStorage(): StorageData {
   } else {
     return JSON.parse(data, (key, value) => {
       if (key === "quotes") {
-        const result = new Map<string, Set<string>>(value);
-        for (const [key, setValue] of result) {
-          result.set(key, new Set(setValue));
-        }
-        return result;
+        return new Map<string, Quote[]>(value);
       } else if (key === "messages") {
         return new Map<string, Message>(value);
       }
@@ -158,7 +145,7 @@ function getKey(objectType: string, objectId: number): string {
 function saveStorage(data: StorageData) {
   window.localStorage.setItem(
     STORAGE_KEY,
-    JSON.stringify(data, (key, value) => {
+    JSON.stringify(data, (_key, value) => {
       if (value instanceof Map) {
         return Array.from(value.entries());
       } else if (value instanceof Set) {
index d2dd115fc7d7aad0174f3688ffa409989fd6a186..6d12ac1fb2b334f7c22a4c81b8fc5d0ec7d913cc 100644 (file)
@@ -7,7 +7,7 @@
  * @since 6.2
  * @woltlabExcludeBundle tiny
  */
-define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Event", "WoltLabSuite/Core/Component/Message/MessageTabMenu", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Component/Quote/Message", "WoltLabSuite/Core/Component/Quote/Storage", "WoltLabSuite/Core/Dom/Util"], function (require, exports, tslib_1, Event_1, MessageTabMenu_1, Language_1, Message_1, Storage_1, Util_1) {
+define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Event", "WoltLabSuite/Core/Component/Message/MessageTabMenu", "WoltLabSuite/Core/Language", "WoltLabSuite/Core/Component/Quote/Message", "WoltLabSuite/Core/Component/Quote/Storage", "WoltLabSuite/Core/Dom/Util", "WoltLabSuite/Core/StringUtil"], function (require, exports, tslib_1, Event_1, MessageTabMenu_1, Language_1, Message_1, Storage_1, Util_1, StringUtil_1) {
     "use strict";
     Object.defineProperty(exports, "__esModule", { value: true });
     exports.getQuoteList = getQuoteList;
@@ -36,62 +36,42 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Component/Ckeditor/Eve
             let quotesCount = 0;
             for (const [key, quotes] of (0, Storage_1.getQuotes)()) {
                 const message = (0, Storage_1.getMessage)(key);
-                quotesCount += quotes.size;
-                // TODO escape values
-                // TODO create web components???
-                const fragment = Util_1.default.createFragmentFromHtml(`<article class="message messageReduced jsInvalidQuoteTarget">
-  <div class="messageContent">
-    <header class="messageHeader">
-      <div class="box32 messageHeaderWrapper">
-        <!-- TODO load real avatar -->
-        <span><img src="${window.WCF_PATH}images/avatars/avatar-default.svg" alt="" class="userAvatarImage" style="width: 32px; height: 32px"></span>
-        <div class="messageHeaderBox">
-          <h2 class="messageTitle">
-            <a href="${message.link}">${message.title}</a>
-          </h2>
-          <ul class="messageHeaderMetaData">
-            <!-- TODO add link to author profile -->
-            <li><span class="username">${message.author}</span></li>
-            <li><span class="messagePublicationTime"><woltlab-core-date-time date="${message.time}">${message.time}</woltlab-core-date-time></span></li>
-          </ul>
-        </div>
-      </div>
-    </header>
-    <div class="messageBody">
-      <div class="messageText">
-        <ul class="messageQuoteItemList">
-        ${Array.from(quotes)
-                    .map((quote) => `<li>
-  <span>
-    <input type="checkbox" value="1" class="jsCheckbox">
-    <button type="button" class="jsTooltip jsInsertQuote" title="${(0, Language_1.getPhrase)("wcf.message.quote.insertQuote")}">
-        <fa-icon name="plus"></fa-icon>
+                quotesCount += quotes.length;
+                quotes.forEach((quote, index) => {
+                    const fragment = Util_1.default.createFragmentFromHtml(`
+<div class="quoteBox quoteBox--tabMenu">
+  <div class="quoteBoxIcon">
+    <img src="${(0, StringUtil_1.escapeHTML)(message.avatar)}" alt="" class="userAvatarImage" height="24" width="24">
+  </div>
+  <div class="quoteBoxTitle">
+    <a href="${(0, StringUtil_1.escapeHTML)(message.link)}" target="_blank">${(0, StringUtil_1.escapeHTML)(message.author)}</a>
+  </div>
+  <div class="quoteBoxButtons">
+    <button type="button" class="button small jsTooltip" title="${(0, Language_1.getPhrase)("wcf.global.button.delete")}" data-action="delete">
+      <fa-icon name="times"></fa-icon>
+    </button>
+    <button type="button" class="button buttonPrimary small jsTooltip" title="${(0, Language_1.getPhrase)("wcf.message.quote.insertQuote")}" data-action="insert">
+      <fa-icon name="paste"></fa-icon>
     </button>
-  </span>
-  
-  <div class="jsQuote">
-    ${quote.message}
   </div>
-</li>`)
-                    .join("")}
-        </ul>
-      </div>
-    </div>
+  <div class="quoteBoxContent">
+    ${quote.rawMessage === undefined ? quote.message : quote.rawMessage}
   </div>
-</article>`);
-                fragment.querySelectorAll(".jsInsertQuote").forEach((button) => {
-                    button.addEventListener("click", () => {
-                        // TODO dont query the DOM
-                        // TODO use rawMessage to insert if available otherwise use message
+</div>
+        `);
+                    fragment.querySelector('button[data-action="insert"]').addEventListener("click", () => {
                         (0, Event_1.dispatchToCkeditor)(this.#editor).insertQuote({
                             author: message.author,
-                            content: button.closest("li").querySelector(".jsQuote").innerHTML,
-                            isText: false,
+                            content: quote.rawMessage === undefined ? quote.message : quote.rawMessage,
+                            isText: quote.rawMessage === undefined,
                             link: message.link,
                         });
                     });
+                    fragment.querySelector('button[data-action="delete"]').addEventListener("click", () => {
+                        (0, Storage_1.removeQuote)(key, index);
+                    });
+                    this.#container.append(fragment);
                 });
-                this.#container.append(fragment);
             }
             if (quotesCount > 0) {
                 (0, MessageTabMenu_1.getTabMenu)(this.#editorId)?.showTab("quotes", (0, Language_1.getPhrase)("wcf.message.quote.showQuotes", {
index b71b4f69bed386fd2bca76a801ecffc6b27125b3..c5cece8d3b8ff8278afed6438336b2255e1a8ef9 100644 (file)
@@ -55,14 +55,13 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C
         const key = objectId ? getKey(objectType, objectId) : objectType;
         return getStorage().messages.get(key);
     }
-    function removeQuote(objectType, objectId, quote) {
+    function removeQuote(key, index) {
         const storage = getStorage();
-        const key = getKey(objectType, objectId);
         if (!storage.quotes.has(key)) {
             return;
         }
-        storage.quotes.get(key).delete(quote);
-        if (storage.quotes.get(key).size === 0) {
+        storage.quotes.get(key).splice(index, 1);
+        if (storage.quotes.get(key).length === 0) {
             storage.quotes.delete(key);
             storage.messages.delete(key);
         }
@@ -73,14 +72,10 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C
         const storage = getStorage();
         const key = getKey(objectType, message.objectID);
         if (!storage.quotes.has(key)) {
-            storage.quotes.set(key, new Set());
+            storage.quotes.set(key, []);
         }
         storage.messages.set(key, message);
-        if (!Array.from(storage.quotes.get(key))
-            .map((q) => q.message)
-            .includes(quote.message)) {
-            storage.quotes.get(key).add(quote);
-        }
+        storage.quotes.get(key).push(quote);
         saveStorage(storage);
     }
     function getStorage() {
@@ -94,11 +89,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C
         else {
             return JSON.parse(data, (key, value) => {
                 if (key === "quotes") {
-                    const result = new Map(value);
-                    for (const [key, setValue] of result) {
-                        result.set(key, new Set(setValue));
-                    }
-                    return result;
+                    return new Map(value);
                 }
                 else if (key === "messages") {
                     return new Map(value);
@@ -111,7 +102,7 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Core", "WoltLabSuite/C
         return `${objectType}:${objectId}`;
     }
     function saveStorage(data) {
-        window.localStorage.setItem(STORAGE_KEY, JSON.stringify(data, (key, value) => {
+        window.localStorage.setItem(STORAGE_KEY, JSON.stringify(data, (_key, value) => {
             if (value instanceof Map) {
                 return Array.from(value.entries());
             }
index c2e2490cd704da99ad619bc94b44f6ceaf7ddc92..944b6ee8ffee6383f2b8c4b1628004ca6da6392b 100644 (file)
@@ -8,7 +8,7 @@
        display: grid;
        font-style: normal;
        grid-template-areas:
-               "icon title"
+               "icon    title"
                "content content";
        grid-template-columns: 24px auto;
        margin: 2em 0 1em 0;
                margin-bottom: 0 !important;
        }
 }
+
+.quoteBox.quoteBox--tabMenu {
+       grid-template-areas:
+               "icon    title   buttons"
+               "content content content";
+       grid-template-columns: 24px auto min-content;
+       margin: 0;
+}
+
+.quoteBox.quoteBox--tabMenu + .quoteBox.quoteBox--tabMenu {
+       margin-top: 10px;
+}
+
+.quoteBoxButtons {
+       align-self: center;
+       column-gap: 5px;
+       display: flex;
+       grid-area: buttons;
+       white-space: nowrap;
+}
+
+.quoteBox.quoteBox--tabMenu :is(.quoteBoxIcon, .quoteBoxTitle) {
+       align-self: center;
+}
+
+.quoteBox.quoteBox--tabMenu .quoteBoxContent {
+       pointer-events: none !important;
+}
+
+@include screen-xs {
+       .messageTabMenu:not(.messageTabMenuContent) > .messageTabMenuContent.messageTabMenuContent--quotes.active {
+               padding-left: 10px;
+               padding-right: 10px;
+       }
+}