-{* 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
-{* 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
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>();
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) {
}
interface StorageData {
- quotes: Map<string, Set<Quote>>;
+ quotes: Map<string, Quote[]>;
messages: Map<string, Message>;
}
refreshQuoteLists();
}
-export function getQuotes(): Map<string, Set<Quote>> {
+export function getQuotes(): Map<string, Quote[]> {
return getStorage().quotes;
}
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);
}
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);
}
} 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);
}
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) {
* @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;
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", {
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);
}
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() {
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);
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());
}
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;
+ }
+}