Add support for counters for message tab menus
authorAlexander Ebert <ebert@woltlab.com>
Wed, 8 Jan 2025 16:17:19 +0000 (17:17 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 8 Jan 2025 16:17:19 +0000 (17:17 +0100)
com.woltlab.wcf/templates/messageFormTabs.tpl
com.woltlab.wcf/templates/messageFormTabsInline.tpl
ts/WoltLabSuite/Core/Component/Attachment/List.ts
ts/WoltLabSuite/Core/Component/Message/MessageTabMenu.ts
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Attachment/List.js
wcfsetup/install/files/js/WoltLabSuite/Core/Component/Message/MessageTabMenu.js
wcfsetup/install/files/style/ui/tabMenuMessage.scss

index 1aa77e10811cf1e4e0b5916b207fedf134f74d88..bfc4726dc0d9841e34adbb8d80410f58278fb119 100644 (file)
@@ -1,26 +1,56 @@
 {* the settings template does not generate direct ouput anymore, but captures it content *}
 {include file='messageFormSettings'}
 
-<div class="messageTabMenu" data-preselect="{if $preselectTabMenu|isset}{$preselectTabMenu}{else}true{/if}" data-wysiwyg-container-id="{if $wysiwygContainerID|isset}{$wysiwygContainerID}{else}text{/if}">
+<div class="messageTabMenu" data-preselect="{if $preselectTabMenu|isset}{$preselectTabMenu}{else}true{/if}"
+       data-wysiwyg-container-id="{if $wysiwygContainerID|isset}{$wysiwygContainerID}{else}text{/if}">
        <nav class="messageTabMenuNavigation jsOnly">
                <ul>
-                       {if MODULE_SMILEY && !$smileyCategories|empty}<li data-name="smilies"><button type="button">{icon name='face-smile'} <span>{lang}wcf.message.smilies{/lang}</span></button></li>{/if}
+                       {if MODULE_SMILEY && !$smileyCategories|empty}
+                               <li data-name="smilies">
+                                       <button type="button">
+                                               {icon name='face-smile'}
+                                               <span>{lang}wcf.message.smilies{/lang}</span>
+                                       </button>
+                               </li>
+                       {/if}
                        {if !$attachmentHandler|empty && $attachmentHandler->canUpload()}
-                               <li data-name="attachments"><button type="button">{icon name='paperclip'} <span>{lang}wcf.attachment.attachments{/lang}</span></button></li>
+                               <li data-name="attachments">
+                                       <button type="button">
+                                               {icon name='paperclip'}
+                                               <span>{lang}wcf.attachment.attachments{/lang}</span>
+                                               {if $attachmentHandler->count() > 0}
+                                                       <span class="badge badgeUpdate">{#$attachmentHandler->count()}</span>
+                                               {/if}
+                                       </button>
+                               </li>
+                       {/if}
+                       {if $__messageFormSettings}
+                               <li data-name="settings">
+                                       <button type="button">
+                                               {icon name='gear'}
+                                               <span>{lang}wcf.message.settings{/lang}</span>
+                                       </button>
+                               </li>
+                       {/if}
+                       {if $__showPoll|isset && $__showPoll}
+                               <li data-name="poll">
+                                       <button type="button">
+                                               {icon name='chart-bar'}
+                                               <span>{lang}wcf.poll.management{/lang}</span>
+                                       </button>
+                               </li>
                        {/if}
-                       {if $__messageFormSettings}<li data-name="settings"><button type="button">{icon name='gear'} <span>{lang}wcf.message.settings{/lang}</span></button></li>{/if}
-                       {if $__showPoll|isset && $__showPoll}<li data-name="poll"><button type="button">{icon name='chart-bar'} <span>{lang}wcf.poll.management{/lang}</span></button></li>{/if}
                        {event name='tabMenuTabs'}
                </ul>
        </nav>
-       
+
        {if MODULE_SMILEY && !$smileyCategories|empty}{include file='shared_messageFormSmileyTab'}{/if}
        {if !$attachmentHandler|empty && $attachmentHandler->canUpload()}
                {include file='shared_messageFormAttachments'}
        {/if}
-       
+
        {if $__messageFormSettings}{unsafe:$__messageFormSettings}{/if}
        {include file='__messageFormPoll'}
-       
+
        {event name='tabMenuContents'}
-</div>
+</div>
\ No newline at end of file
index 9f81bf37f8e63ec1f758c6f9afce14e27d22b08c..71c23dbcb20b99819a719e52117d395e2bc5426a 100644 (file)
@@ -5,27 +5,56 @@
 {capture assign='__messageFormSettingsInlineContent'}{include file='messageFormSettingsInline'}{/capture}
 {assign var='__messageFormSettingsInlineContent' value=$__messageFormSettingsInlineContent|trim}
 
-<div class="messageTabMenu"{if $preselectTabMenu|isset} data-preselect="{$preselectTabMenu}"{/if} data-wysiwyg-container-id="{$wysiwygSelector}">
+<div class="messageTabMenu" {if $preselectTabMenu|isset} data-preselect="{$preselectTabMenu}" {/if}
+       data-wysiwyg-container-id="{$wysiwygSelector}">
        <nav class="messageTabMenuNavigation jsOnly">
                <ul>
-                       {if MODULE_SMILEY && !$smileyCategories|empty}<li data-name="smilies"><button type="button">{icon name='face-smile'} <span>{lang}wcf.message.smilies{/lang}</span></button></li>{/if}
+                       {if MODULE_SMILEY && !$smileyCategories|empty}
+                               <li data-name="smilies">
+                                       <button type="button">
+                                               {icon name='face-smile'}
+                                               <span>{lang}wcf.message.smilies{/lang}</span>
+                                       </button>
+                               </li>
+                       {/if}
                        {if !$attachmentHandler|empty && $attachmentHandler->canUpload()}
-                               <li data-name="attachments"><button type="button">{icon name='paperclip'} <span>{lang}wcf.attachment.attachments{/lang}</span></button></li>
+                               <li data-name="attachments">
+                                       <button type="button">
+                                               {icon name='paperclip'}
+                                               <span>{lang}wcf.attachment.attachments{/lang}</span>
+                                               {if $attachmentHandler->count() > 0}
+                                                       <span class="badge badgeUpdate">{#$attachmentHandler->count()}</span>
+                                               {/if}
+                                       </button>
+                               </li>
+                       {/if}
+                       {if $__messageFormSettingsInlineContent}
+                               <li data-name="settings"><button type="button">
+                                               {icon name='gear'}
+                                               <span>{lang}wcf.message.settings{/lang}</span>
+                                       </button>
+                               </li>
+                       {/if}
+                       {if $__showPoll|isset && $__showPoll}
+                               <li data-name="poll">
+                                       <button type="button">
+                                               {icon name='chart-bar'}
+                                               <span>{lang}wcf.poll.management{/lang}</span>
+                                       </button>
+                               </li>
                        {/if}
-                       {if $__messageFormSettingsInlineContent}<li data-name="settings"><button type="button">{icon name='gear'} <span>{lang}wcf.message.settings{/lang}</span></button></li>{/if}
-                       {if $__showPoll|isset && $__showPoll}<li data-name="poll"><button type="button">{icon name='chart-bar'} <span>{lang}wcf.poll.management{/lang}</span></button></li>{/if}
                        {event name='tabMenuTabs'}
                </ul>
        </nav>
-       
+
        {if MODULE_SMILEY && !$smileyCategories|empty}{include file='shared_messageFormSmileyTab'}{/if}
        {if !$attachmentHandler|empty && $attachmentHandler->canUpload()}
                {include file='shared_messageFormAttachments'}
        {/if}
-       
+
        {if $__messageFormSettingsInlineContent}{@$__messageFormSettingsInlineContent}{/if}
-       
+
        {include file='__messageFormPollInline'}
-       
+
        {event name='tabMenuContents'}
-</div>
+</div>
\ No newline at end of file
index 46a84b5715e7589ba49b57f85a445a30333fa409..2fdf779c155bcd469dc7cb24c822cffc96b8946e 100644 (file)
@@ -18,6 +18,11 @@ export function setup(editorId: string): void {
     throw new Error(`The attachments container for '${editorId}' does not exist.`);
   }
 
+  const tabMenu = getTabMenu(editorId);
+  if (tabMenu === undefined) {
+    throw new Error("Unable to find the corresponding tab menu.");
+  }
+
   const editor = document.getElementById(editorId);
   if (editor === null) {
     throw new Error(`The editor element for '${editorId}' does not exist.`);
@@ -85,4 +90,20 @@ export function setup(editorId: string): void {
 
     existingFiles.remove();
   }
+
+  const files = fileList.getElementsByTagName("woltlab-core-file");
+  const observer = new MutationObserver(() => {
+    let counter = 0;
+    for (const file of files) {
+      if (!file.isFailedUpload()) {
+        counter++;
+      }
+    }
+
+    tabMenu.setTabCounter("attachments", counter);
+  });
+  observer.observe(fileList, {
+    childList: true,
+    subtree: true,
+  });
 }
index 0c87f8f37032acd29b361e6b543c86f045a0b7e8..0d3d4d7579647afda5c2657141256f53a368db18 100644 (file)
@@ -65,6 +65,28 @@ class TabMenu {
     this.#activeTabName = tabName;
   }
 
+  setTabCounter(tabName: string, value: number): void {
+    const tab = this.#tabs.find((element) => element.dataset.name === tabName);
+    if (tab === undefined) {
+      throw new Error(`Unknown tab '${tabName}'.`);
+    }
+
+    let badgeUpdate = tab.querySelector(".badgeUpdate");
+    if (value === 0) {
+      badgeUpdate?.remove();
+
+      return;
+    }
+
+    if (badgeUpdate === null) {
+      badgeUpdate = document.createElement("span");
+      badgeUpdate.classList.add("badge", "badgeUpdate");
+      tab.querySelector("a, button")!.append(badgeUpdate);
+    }
+
+    badgeUpdate.textContent = value.toString();
+  }
+
   #init(): void {
     for (let i = 0; i < this.#tabs.length; i++) {
       const tab = this.#tabs[i];
@@ -142,9 +164,19 @@ function initTabMenu(tabMenu: HTMLElement): void {
 }
 
 export function getTabMenu(identifier: string): TabMenu | undefined {
+  setup();
+
   return tabMenus.get(identifier);
 }
 
+let initialized = false;
+
 export function setup(): void {
+  if (initialized) {
+    return;
+  }
+
+  initialized = true;
+
   wheneverFirstSeen(".messageTabMenu", (tabMenu) => initTabMenu(tabMenu));
 }
index 189c882cf686dda9ef562f8292f54a5a0949e8de..74ef52916a1a2a1e3898be9a1aa8fad9b95ddc9f 100644 (file)
@@ -10,6 +10,10 @@ define(["require", "exports", "./Entry", "../Ckeditor/Event", "../Message/Messag
         if (container === null) {
             throw new Error(`The attachments container for '${editorId}' does not exist.`);
         }
+        const tabMenu = (0, MessageTabMenu_1.getTabMenu)(editorId);
+        if (tabMenu === undefined) {
+            throw new Error("Unable to find the corresponding tab menu.");
+        }
         const editor = document.getElementById(editorId);
         if (editor === null) {
             throw new Error(`The editor element for '${editorId}' does not exist.`);
@@ -69,5 +73,19 @@ define(["require", "exports", "./Entry", "../Ckeditor/Event", "../Message/Messag
             });
             existingFiles.remove();
         }
+        const files = fileList.getElementsByTagName("woltlab-core-file");
+        const observer = new MutationObserver(() => {
+            let counter = 0;
+            for (const file of files) {
+                if (!file.isFailedUpload()) {
+                    counter++;
+                }
+            }
+            tabMenu.setTabCounter("attachments", counter);
+        });
+        observer.observe(fileList, {
+            childList: true,
+            subtree: true,
+        });
     }
 });
index 84ec5d2ddc97e9f3c59d7d0f7a073218714fa09c..fcbfb6d4fe1398528ce42de718636cdcf995504e 100644 (file)
@@ -53,6 +53,23 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol
             this.#tabContainers[tabIndex].classList.add("active");
             this.#activeTabName = tabName;
         }
+        setTabCounter(tabName, value) {
+            const tab = this.#tabs.find((element) => element.dataset.name === tabName);
+            if (tab === undefined) {
+                throw new Error(`Unknown tab '${tabName}'.`);
+            }
+            let badgeUpdate = tab.querySelector(".badgeUpdate");
+            if (value === 0) {
+                badgeUpdate?.remove();
+                return;
+            }
+            if (badgeUpdate === null) {
+                badgeUpdate = document.createElement("span");
+                badgeUpdate.classList.add("badge", "badgeUpdate");
+                tab.querySelector("a, button").append(badgeUpdate);
+            }
+            badgeUpdate.textContent = value.toString();
+        }
         #init() {
             for (let i = 0; i < this.#tabs.length; i++) {
                 const tab = this.#tabs[i];
@@ -114,9 +131,15 @@ define(["require", "exports", "tslib", "WoltLabSuite/Core/Helper/Selector", "Wol
         }
     }
     function getTabMenu(identifier) {
+        setup();
         return tabMenus.get(identifier);
     }
+    let initialized = false;
     function setup() {
+        if (initialized) {
+            return;
+        }
+        initialized = true;
         (0, Selector_1.wheneverFirstSeen)(".messageTabMenu", (tabMenu) => initTabMenu(tabMenu));
     }
 });
index 5ca84fa87ec42dded0b93657f22e32769977673c..ab4a8b355ea2672bdf19bbcbb7c4b0e83cc0a833 100644 (file)
 
                        > a,
                        > button {
+                               align-items: center;
                                color: inherit;
-                               display: block;
+                               /* 0.2em matches the width of a space */
+                               column-gap: 0.2em;
+                               display: flex;
                                padding: 10px 20px;
 
                                @include userSelectNone;
                                @include wcfFontDefault;
 
+                               .badgeUpdate {
+                                       font-variant: tabular-nums;
+                                       margin-left: 0.3em;
+                               }
+
                                @include screen-md-up {
                                        > .icon {
                                                display: none;
                                                display: block;
                                        }
 
-                                       > span:not(.icon) {
+                                       > span:not(.badgeUpdate) {
                                                display: none;
                                        }
                                }