mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 10:56:10 +01:00 
			
		
		
		
	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>
This commit is contained in:
		| @@ -182,15 +182,6 @@ | ||||
| 		</div> | ||||
| 	</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> | ||||
| @@ -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" .}} | ||||
|   | ||||
							
								
								
									
										15
									
								
								templates/devtest/toast.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								templates/devtest/toast.tmpl
									
									
									
									
									
										Normal file
									
								
							| @@ -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" .}} | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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'); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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); | ||||
|   }); | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user