mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 19:06:18 +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> | 	</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> | 	<div> | ||||||
| 		<h1>ComboMarkdownEditor</h1> | 		<h1>ComboMarkdownEditor</h1> | ||||||
| 		<div>ps: no JS code attached, so just a layout</div> | 		<div>ps: no JS code attached, so just a layout</div> | ||||||
| @@ -201,7 +192,5 @@ | |||||||
| 	<div> | 	<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> | 		<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> | 	</div> | ||||||
|  |  | ||||||
| 	<script src="{{AssetUrlPrefix}}/js/devtest.js?v={{AssetVersion}}"></script> |  | ||||||
| </div> | </div> | ||||||
| {{template "base/footer" .}} | {{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% { |   0% { | ||||||
|     transform: scale(1); |     transform: scale(1); | ||||||
|   } |   } | ||||||
|   50% { |   50% { | ||||||
|     transform: scale(1.8); |     transform: scale(1.5); | ||||||
|   } |   } | ||||||
|   100% { |   100% { | ||||||
|     transform: scale(1); |     transform: scale(1); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| .pulse { | /* pulse animation for scale(1.5) in 200ms */ | ||||||
|   animation: pulse 2s linear; | .pulse-1p5-200 { | ||||||
|  |   animation: pulse-1p5 200ms linear; | ||||||
| } | } | ||||||
|  |  | ||||||
| .ui.modal, | .ui.modal, | ||||||
|   | |||||||
| @@ -22,17 +22,31 @@ | |||||||
|   overflow-wrap: anywhere; |   overflow-wrap: anywhere; | ||||||
| } | } | ||||||
|  |  | ||||||
| .toast-close, | .toast-close { | ||||||
| .toast-icon { |  | ||||||
|   color: currentcolor; |  | ||||||
|   border-radius: var(--border-radius); |   border-radius: var(--border-radius); | ||||||
|   background: transparent; |  | ||||||
|   border: none; |  | ||||||
|   display: flex; |  | ||||||
|   width: 30px; |   width: 30px; | ||||||
|   height: 30px; |   height: 30px; | ||||||
|   justify-content: center; |   justify-content: center; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .toast-icon { | ||||||
|  |   display: inline-flex; | ||||||
|  |   width: 30px; | ||||||
|  |   height: 30px; | ||||||
|   align-items: center; |   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 { | .toast-close:hover { | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.js'; | |||||||
| import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles, initExpandAndCollapseFilesButton} from './pull-view-file.js'; | import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles, initExpandAndCollapseFilesButton} from './pull-view-file.js'; | ||||||
| import {initImageDiff} from './imagediff.js'; | import {initImageDiff} from './imagediff.js'; | ||||||
| import {showErrorToast} from '../modules/toast.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'; | import {POST, GET} from '../modules/fetch.js'; | ||||||
|  |  | ||||||
| const {pageData, i18n} = window.config; | const {pageData, i18n} = window.config; | ||||||
| @@ -26,11 +26,7 @@ function initRepoDiffReviewButton() { | |||||||
|       const num = parseInt(counter.getAttribute('data-pending-comment-number')) + 1 || 1; |       const num = parseInt(counter.getAttribute('data-pending-comment-number')) + 1 || 1; | ||||||
|       counter.setAttribute('data-pending-comment-number', num); |       counter.setAttribute('data-pending-comment-number', num); | ||||||
|       counter.textContent = num; |       counter.textContent = num; | ||||||
|  |       animateOnce(reviewBox, 'pulse-1p5-200'); | ||||||
|       reviewBox.classList.remove('pulse'); |  | ||||||
|       requestAnimationFrame(() => { |  | ||||||
|         reviewBox.classList.add('pulse'); |  | ||||||
|       }); |  | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import {htmlEscape} from 'escape-goat'; | import {htmlEscape} from 'escape-goat'; | ||||||
| import {svg} from '../svg.js'; | 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 | 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 = { | const levels = { | ||||||
| @@ -21,13 +22,28 @@ const levels = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| // See https://github.com/apvarun/toastify-js#api for options | // 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 {icon, background, duration: levelDuration} = levels[level ?? 'info']; | ||||||
|   const toast = Toastify({ |   const toast = Toastify({ | ||||||
|     text: ` |     text: ` | ||||||
|       <div class='toast-icon'>${svg(icon)}</div> |       <div class='toast-icon'>${svg(icon)}</div> | ||||||
|       <div class='toast-body'>${useHtmlBody ? message : htmlEscape(message)}</div> |       <div class='toast-body'><span class="toast-duplicate-number tw-hidden">1</span>${body}</div> | ||||||
|       <button class='toast-close'>${svg('octicon-x')}</button> |       <button class='btn toast-close'>${svg('octicon-x')}</button> | ||||||
|     `, |     `, | ||||||
|     escapeMarkup: false, |     escapeMarkup: false, | ||||||
|     gravity: gravity ?? 'top', |     gravity: gravity ?? 'top', | ||||||
| @@ -39,6 +55,7 @@ function showToast(message, level, {gravity, position, duration, useHtmlBody, .. | |||||||
|  |  | ||||||
|   toast.showToast(); |   toast.showToast(); | ||||||
|   toast.toastElement.querySelector('.toast-close').addEventListener('click', () => toast.hideToast()); |   toast.toastElement.querySelector('.toast-close').addEventListener('click', () => toast.hideToast()); | ||||||
|  |   toast.toastElement.setAttribute('data-toast-unique-key', key); | ||||||
|   return toast; |   return toast; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,11 +1,14 @@ | |||||||
| import {showInfoToast, showWarningToast, showErrorToast} from '../modules/toast.js'; | import {showInfoToast, showWarningToast, showErrorToast} from '../modules/toast.js'; | ||||||
|  |  | ||||||
| document.querySelector('#info-toast').addEventListener('click', () => { | function initDevtestToast() { | ||||||
|   showInfoToast('success 😀'); |   const levelMap = {info: showInfoToast, warning: showWarningToast, error: showErrorToast}; | ||||||
| }); |   for (const el of document.querySelectorAll('.toast-test-button')) { | ||||||
| document.querySelector('#warning-toast').addEventListener('click', () => { |     el.addEventListener('click', () => { | ||||||
|   showWarningToast('warning 😐'); |       const level = el.getAttribute('data-toast-level'); | ||||||
| }); |       const message = el.getAttribute('data-toast-message'); | ||||||
| document.querySelector('#error-toast').addEventListener('click', () => { |       levelMap[level](message); | ||||||
|   showErrorToast('error 🙁'); |     }); | ||||||
| }); |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | initDevtestToast(); | ||||||
|   | |||||||
| @@ -306,3 +306,14 @@ export function createElementFromAttrs(tagName, attrs) { | |||||||
|   } |   } | ||||||
|   return el; |   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