aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorwxiaoguang <wxiaoguang@gmail.com>2024-06-27 21:58:38 +0800
committerGitHub <noreply@github.com>2024-06-27 13:58:38 +0000
commitc1fe6fbcc36bd29549019274d39f273b3e37755d (patch)
tree22938fd15047666a750608f8aaf485a55d152014
parent9bc5552c11f6aca08c8c873a0561882b3e099350 (diff)
downloadgitea-c1fe6fbcc36bd29549019274d39f273b3e37755d.tar.gz
gitea-c1fe6fbcc36bd29549019274d39f273b3e37755d.zip
Make toast support preventDuplicates (#31501)
make preventDuplicates default to true, users get a clear UI feedback and know that "a new message appears". Fixes: https://github.com/go-gitea/gitea/issues/26651 --------- Co-authored-by: silverwind <me@silverwind.io>
-rw-r--r--templates/devtest/gitea-ui.tmpl11
-rw-r--r--templates/devtest/toast.tmpl15
-rw-r--r--web_src/css/modules/animations.css10
-rw-r--r--web_src/css/modules/toast.css26
-rw-r--r--web_src/js/features/repo-diff.js8
-rw-r--r--web_src/js/modules/toast.js23
-rw-r--r--web_src/js/standalone/devtest.js21
-rw-r--r--web_src/js/utils/dom.js11
8 files changed, 86 insertions, 39 deletions
diff --git a/templates/devtest/gitea-ui.tmpl b/templates/devtest/gitea-ui.tmpl
index ea293fd3b4..06d0e36569 100644
--- a/templates/devtest/gitea-ui.tmpl
+++ b/templates/devtest/gitea-ui.tmpl
@@ -183,15 +183,6 @@
</div>
<div>
- <h1>Toast</h1>
- <div>
- <button class="ui button" id="info-toast">Show Info Toast</button>
- <button class="ui button" id="warning-toast">Show Warning Toast</button>
- <button class="ui button" id="error-toast">Show Error Toast</button>
- </div>
- </div>
-
- <div>
<h1>ComboMarkdownEditor</h1>
<div>ps: no JS code attached, so just a layout</div>
{{template "shared/combomarkdowneditor" .}}
@@ -201,7 +192,5 @@
<div>
<button class="{{if true}}tw-bg-red{{end}} tw-p-5 tw-border tw-rounded hover:tw-bg-blue active:tw-bg-yellow">Button</button>
</div>
-
- <script src="{{AssetUrlPrefix}}/js/devtest.js?v={{AssetVersion}}"></script>
</div>
{{template "base/footer" .}}
diff --git a/templates/devtest/toast.tmpl b/templates/devtest/toast.tmpl
new file mode 100644
index 0000000000..412f23964a
--- /dev/null
+++ b/templates/devtest/toast.tmpl
@@ -0,0 +1,15 @@
+{{template "base/head" .}}
+
+<div>
+ <h1>Toast</h1>
+ <div>
+ <button class="ui button toast-test-button" data-toast-level="info" data-toast-message="test info">Show Info Toast</button>
+ <button class="ui button toast-test-button" data-toast-level="warning" data-toast-message="test warning">Show Warning Toast</button>
+ <button class="ui button toast-test-button" data-toast-level="error" data-toast-message="test error">Show Error Toast</button>
+ <button class="ui button toast-test-button" data-toast-level="error" data-toast-message="very looooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong message">Show Error Toast (long)</button>
+ </div>
+</div>
+
+<script src="{{AssetUrlPrefix}}/js/devtest.js?v={{AssetVersion}}"></script>
+
+{{template "base/footer" .}}
diff --git a/web_src/css/modules/animations.css b/web_src/css/modules/animations.css
index a86c9234aa..481e997d4f 100644
--- a/web_src/css/modules/animations.css
+++ b/web_src/css/modules/animations.css
@@ -92,20 +92,22 @@ code.language-math.is-loading::after {
}
}
-@keyframes pulse {
+/* 1p5 means 1-point-5. it can't use "pulse" here, otherwise the animation is not right (maybe due to some conflicts */
+@keyframes pulse-1p5 {
0% {
transform: scale(1);
}
50% {
- transform: scale(1.8);
+ transform: scale(1.5);
}
100% {
transform: scale(1);
}
}
-.pulse {
- animation: pulse 2s linear;
+/* pulse animation for scale(1.5) in 200ms */
+.pulse-1p5-200 {
+ animation: pulse-1p5 200ms linear;
}
.ui.modal,
diff --git a/web_src/css/modules/toast.css b/web_src/css/modules/toast.css
index 2a9f78e017..1145f3b1b5 100644
--- a/web_src/css/modules/toast.css
+++ b/web_src/css/modules/toast.css
@@ -22,17 +22,31 @@
overflow-wrap: anywhere;
}
-.toast-close,
-.toast-icon {
- color: currentcolor;
+.toast-close {
border-radius: var(--border-radius);
- background: transparent;
- border: none;
- display: flex;
width: 30px;
height: 30px;
justify-content: center;
+}
+
+.toast-icon {
+ display: inline-flex;
+ width: 30px;
+ height: 30px;
align-items: center;
+ justify-content: center;
+}
+
+.toast-duplicate-number::before {
+ content: "(";
+}
+.toast-duplicate-number {
+ display: inline-block;
+ margin-right: 5px;
+ user-select: none;
+}
+.toast-duplicate-number::after {
+ content: ")";
}
.toast-close:hover {
diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js
index cd01232a7e..279f6da757 100644
--- a/web_src/js/features/repo-diff.js
+++ b/web_src/js/features/repo-diff.js
@@ -7,7 +7,7 @@ import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.js';
import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles, initExpandAndCollapseFilesButton} from './pull-view-file.js';
import {initImageDiff} from './imagediff.js';
import {showErrorToast} from '../modules/toast.js';
-import {submitEventSubmitter, queryElemSiblings, hideElem, showElem} from '../utils/dom.js';
+import {submitEventSubmitter, queryElemSiblings, hideElem, showElem, animateOnce} from '../utils/dom.js';
import {POST, GET} from '../modules/fetch.js';
const {pageData, i18n} = window.config;
@@ -26,11 +26,7 @@ function initRepoDiffReviewButton() {
const num = parseInt(counter.getAttribute('data-pending-comment-number')) + 1 || 1;
counter.setAttribute('data-pending-comment-number', num);
counter.textContent = num;
-
- reviewBox.classList.remove('pulse');
- requestAnimationFrame(() => {
- reviewBox.classList.add('pulse');
- });
+ animateOnce(reviewBox, 'pulse-1p5-200');
});
});
}
diff --git a/web_src/js/modules/toast.js b/web_src/js/modules/toast.js
index d12d203718..627e24a1ff 100644
--- a/web_src/js/modules/toast.js
+++ b/web_src/js/modules/toast.js
@@ -1,5 +1,6 @@
import {htmlEscape} from 'escape-goat';
import {svg} from '../svg.js';
+import {animateOnce, showElem} from '../utils/dom.js';
import Toastify from 'toastify-js'; // don't use "async import", because when network error occurs, the "async import" also fails and nothing is shown
const levels = {
@@ -21,13 +22,28 @@ const levels = {
};
// See https://github.com/apvarun/toastify-js#api for options
-function showToast(message, level, {gravity, position, duration, useHtmlBody, ...other} = {}) {
+function showToast(message, level, {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other} = {}) {
+ const body = useHtmlBody ? String(message) : htmlEscape(message);
+ const key = `${level}-${body}`;
+
+ // prevent showing duplicate toasts with same level and message, and give a visual feedback for end users
+ if (preventDuplicates) {
+ const toastEl = document.querySelector(`.toastify[data-toast-unique-key="${CSS.escape(key)}"]`);
+ if (toastEl) {
+ const toastDupNumEl = toastEl.querySelector('.toast-duplicate-number');
+ showElem(toastDupNumEl);
+ toastDupNumEl.textContent = String(Number(toastDupNumEl.textContent) + 1);
+ animateOnce(toastDupNumEl, 'pulse-1p5-200');
+ return;
+ }
+ }
+
const {icon, background, duration: levelDuration} = levels[level ?? 'info'];
const toast = Toastify({
text: `
<div class='toast-icon'>${svg(icon)}</div>
- <div class='toast-body'>${useHtmlBody ? message : htmlEscape(message)}</div>
- <button class='toast-close'>${svg('octicon-x')}</button>
+ <div class='toast-body'><span class="toast-duplicate-number tw-hidden">1</span>${body}</div>
+ <button class='btn toast-close'>${svg('octicon-x')}</button>
`,
escapeMarkup: false,
gravity: gravity ?? 'top',
@@ -39,6 +55,7 @@ function showToast(message, level, {gravity, position, duration, useHtmlBody, ..
toast.showToast();
toast.toastElement.querySelector('.toast-close').addEventListener('click', () => toast.hideToast());
+ toast.toastElement.setAttribute('data-toast-unique-key', key);
return toast;
}
diff --git a/web_src/js/standalone/devtest.js b/web_src/js/standalone/devtest.js
index 8dbba554ac..d3e3e13a87 100644
--- a/web_src/js/standalone/devtest.js
+++ b/web_src/js/standalone/devtest.js
@@ -1,11 +1,14 @@
import {showInfoToast, showWarningToast, showErrorToast} from '../modules/toast.js';
-document.querySelector('#info-toast').addEventListener('click', () => {
- showInfoToast('success 😀');
-});
-document.querySelector('#warning-toast').addEventListener('click', () => {
- showWarningToast('warning 😐');
-});
-document.querySelector('#error-toast').addEventListener('click', () => {
- showErrorToast('error 🙁');
-});
+function initDevtestToast() {
+ const levelMap = {info: showInfoToast, warning: showWarningToast, error: showErrorToast};
+ for (const el of document.querySelectorAll('.toast-test-button')) {
+ el.addEventListener('click', () => {
+ const level = el.getAttribute('data-toast-level');
+ const message = el.getAttribute('data-toast-message');
+ levelMap[level](message);
+ });
+ }
+}
+
+initDevtestToast();
diff --git a/web_src/js/utils/dom.js b/web_src/js/utils/dom.js
index 6a38968899..9bdb233236 100644
--- a/web_src/js/utils/dom.js
+++ b/web_src/js/utils/dom.js
@@ -306,3 +306,14 @@ export function createElementFromAttrs(tagName, attrs) {
}
return el;
}
+
+export function animateOnce(el, animationClassName) {
+ return new Promise((resolve) => {
+ el.addEventListener('animationend', function onAnimationEnd() {
+ el.classList.remove(animationClassName);
+ el.removeEventListener('animationend', onAnimationEnd);
+ resolve();
+ }, {once: true});
+ el.classList.add(animationClassName);
+ });
+}