mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 10:56:10 +01:00 
			
		
		
		
	Rework and fix stopwatch (#30732)
Fixes https://github.com/go-gitea/gitea/issues/30721 and overhauls the stopwatch. Time is now shown inside the "dot" icon and on both mobile and desktop. All rendering is now done by `<relative-time>`, the `pretty-ms` dependency is dropped. Desktop: <img width="557" alt="Screenshot 2024-04-29 at 22 33 27" src="https://github.com/go-gitea/gitea/assets/115237/3a46cdbf-6af2-4bf9-b07f-021348badaac"> Mobile: <img width="640" alt="Screenshot 2024-04-29 at 22 34 19" src="https://github.com/go-gitea/gitea/assets/115237/8a2beea7-bd5d-473f-8fff-66f63fd50877"> Note for tippy: Previously, tippy instances defaulted to "menu" theme, but that theme is really only meant for `.ui.menu`, so it was not optimal for the stopwatch popover. This introduces a unopinionated `default` theme that has no padding and should be suitable for all content. I reviewed all existing uses and explicitely set the desired `theme` on all of them.
This commit is contained in:
		
							
								
								
									
										26
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										26
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -42,7 +42,6 @@ | |||||||
|         "postcss": "8.4.38", |         "postcss": "8.4.38", | ||||||
|         "postcss-loader": "8.1.1", |         "postcss-loader": "8.1.1", | ||||||
|         "postcss-nesting": "12.1.2", |         "postcss-nesting": "12.1.2", | ||||||
|         "pretty-ms": "9.0.0", |  | ||||||
|         "sortablejs": "1.15.2", |         "sortablejs": "1.15.2", | ||||||
|         "swagger-ui-dist": "5.17.2", |         "swagger-ui-dist": "5.17.2", | ||||||
|         "tailwindcss": "3.4.3", |         "tailwindcss": "3.4.3", | ||||||
| @@ -9170,17 +9169,6 @@ | |||||||
|         "url": "https://github.com/sponsors/sindresorhus" |         "url": "https://github.com/sponsors/sindresorhus" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/parse-ms": { |  | ||||||
|       "version": "4.0.0", |  | ||||||
|       "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", |  | ||||||
|       "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", |  | ||||||
|       "engines": { |  | ||||||
|         "node": ">=18" |  | ||||||
|       }, |  | ||||||
|       "funding": { |  | ||||||
|         "url": "https://github.com/sponsors/sindresorhus" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/path-exists": { |     "node_modules/path-exists": { | ||||||
|       "version": "4.0.0", |       "version": "4.0.0", | ||||||
|       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", |       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", | ||||||
| @@ -9772,20 +9760,6 @@ | |||||||
|         "url": "https://github.com/chalk/ansi-styles?sponsor=1" |         "url": "https://github.com/chalk/ansi-styles?sponsor=1" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/pretty-ms": { |  | ||||||
|       "version": "9.0.0", |  | ||||||
|       "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.0.0.tgz", |  | ||||||
|       "integrity": "sha512-E9e9HJ9R9NasGOgPaPE8VMeiPKAyWR5jcFpNnwIejslIhWqdqOrb2wShBsncMPUb+BcCd2OPYfh7p2W6oemTng==", |  | ||||||
|       "dependencies": { |  | ||||||
|         "parse-ms": "^4.0.0" |  | ||||||
|       }, |  | ||||||
|       "engines": { |  | ||||||
|         "node": ">=18" |  | ||||||
|       }, |  | ||||||
|       "funding": { |  | ||||||
|         "url": "https://github.com/sponsors/sindresorhus" |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "node_modules/printable-characters": { |     "node_modules/printable-characters": { | ||||||
|       "version": "1.0.42", |       "version": "1.0.42", | ||||||
|       "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", |       "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", | ||||||
|   | |||||||
| @@ -41,7 +41,6 @@ | |||||||
|     "postcss": "8.4.38", |     "postcss": "8.4.38", | ||||||
|     "postcss-loader": "8.1.1", |     "postcss-loader": "8.1.1", | ||||||
|     "postcss-nesting": "12.1.2", |     "postcss-nesting": "12.1.2", | ||||||
|     "pretty-ms": "9.0.0", |  | ||||||
|     "sortablejs": "1.15.2", |     "sortablejs": "1.15.2", | ||||||
|     "swagger-ui-dist": "5.17.2", |     "swagger-ui-dist": "5.17.2", | ||||||
|     "tailwindcss": "3.4.3", |     "tailwindcss": "3.4.3", | ||||||
|   | |||||||
| @@ -12,6 +12,14 @@ | |||||||
|  |  | ||||||
| 		<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column --> | 		<!-- mobile right menu, it must be here because in mobile view, each item is a flex column, the first item is a full row column --> | ||||||
| 		<div class="ui secondary menu item navbar-mobile-right only-mobile"> | 		<div class="ui secondary menu item navbar-mobile-right only-mobile"> | ||||||
|  | 			{{if and .IsSigned EnableTimetracking .ActiveStopwatch}} | ||||||
|  | 			<a id="mobile-stopwatch-icon" class="active-stopwatch item tw-mx-0" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{.ActiveStopwatch.Seconds}}"> | ||||||
|  | 				<div class="tw-relative"> | ||||||
|  | 					{{svg "octicon-stopwatch"}} | ||||||
|  | 					<span class="header-stopwatch-dot"></span> | ||||||
|  | 				</div> | ||||||
|  | 			</a> | ||||||
|  | 			{{end}} | ||||||
| 			{{if .IsSigned}} | 			{{if .IsSigned}} | ||||||
| 			<a id="mobile-notifications-icon" class="item tw-w-auto tw-p-2" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}"> | 			<a id="mobile-notifications-icon" class="item tw-w-auto tw-p-2" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}"> | ||||||
| 				<div class="tw-relative"> | 				<div class="tw-relative"> | ||||||
| @@ -74,41 +82,13 @@ | |||||||
| 				</div><!-- end content avatar menu --> | 				</div><!-- end content avatar menu --> | ||||||
| 			</div><!-- end dropdown avatar menu --> | 			</div><!-- end dropdown avatar menu --> | ||||||
| 		{{else if .IsSigned}} | 		{{else if .IsSigned}} | ||||||
| 			{{if EnableTimetracking}} | 			{{if and EnableTimetracking .ActiveStopwatch}} | ||||||
| 			<a class="active-stopwatch-trigger item tw-mx-0{{if not .ActiveStopwatch}} tw-hidden{{end}}" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}"> | 			<a class="item not-mobile active-stopwatch tw-mx-0" href="{{.ActiveStopwatch.IssueLink}}" title="{{ctx.Locale.Tr "active_stopwatch"}}" data-seconds="{{.ActiveStopwatch.Seconds}}"> | ||||||
| 				<div class="tw-relative"> | 				<div class="tw-relative"> | ||||||
| 					{{svg "octicon-stopwatch"}} | 					{{svg "octicon-stopwatch"}} | ||||||
| 					<span class="header-stopwatch-dot"></span> | 					<span class="header-stopwatch-dot"></span> | ||||||
| 				</div> | 				</div> | ||||||
| 				<span class="only-mobile tw-ml-2">{{ctx.Locale.Tr "active_stopwatch"}}</span> |  | ||||||
| 			</a> | 			</a> | ||||||
| 			<div class="active-stopwatch-popup item tippy-target tw-p-2"> |  | ||||||
| 				<div class="tw-flex tw-items-center"> |  | ||||||
| 					<a class="stopwatch-link tw-flex tw-items-center" href="{{.ActiveStopwatch.IssueLink}}"> |  | ||||||
| 						{{svg "octicon-issue-opened" 16 "tw-mr-2"}} |  | ||||||
| 						<span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span> |  | ||||||
| 						<span class="ui primary label stopwatch-time tw-my-0 tw-mx-4" data-seconds="{{.ActiveStopwatch.Seconds}}"> |  | ||||||
| 							{{if .ActiveStopwatch}}{{Sec2Time .ActiveStopwatch.Seconds}}{{end}} |  | ||||||
| 						</span> |  | ||||||
| 					</a> |  | ||||||
| 					<form class="stopwatch-commit" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/toggle"> |  | ||||||
| 						{{.CsrfTokenHtml}} |  | ||||||
| 						<button |  | ||||||
| 							type="submit" |  | ||||||
| 							class="ui button mini compact basic icon" |  | ||||||
| 							data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}" |  | ||||||
| 						>{{svg "octicon-square-fill"}}</button> |  | ||||||
| 					</form> |  | ||||||
| 					<form class="stopwatch-cancel" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/cancel"> |  | ||||||
| 						{{.CsrfTokenHtml}} |  | ||||||
| 						<button |  | ||||||
| 							type="submit" |  | ||||||
| 							class="ui button mini compact basic icon" |  | ||||||
| 							data-tooltip-content="{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}" |  | ||||||
| 						>{{svg "octicon-trash"}}</button> |  | ||||||
| 					</form> |  | ||||||
| 				</div> |  | ||||||
| 			</div> |  | ||||||
| 			{{end}} | 			{{end}} | ||||||
|  |  | ||||||
| 			<a class="item not-mobile tw-mx-0" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}"> | 			<a class="item not-mobile tw-mx-0" href="{{AppSubUrl}}/notifications" data-tooltip-content="{{ctx.Locale.Tr "notifications"}}" aria-label="{{ctx.Locale.Tr "notifications"}}"> | ||||||
| @@ -202,4 +182,33 @@ | |||||||
| 			</a> | 			</a> | ||||||
| 		{{end}} | 		{{end}} | ||||||
| 	</div><!-- end full right menu --> | 	</div><!-- end full right menu --> | ||||||
|  |  | ||||||
|  | 	{{if and .IsSigned EnableTimetracking .ActiveStopwatch}} | ||||||
|  | 		<div class="active-stopwatch-popup tippy-target"> | ||||||
|  | 			<div class="tw-flex tw-items-center tw-gap-2 tw-p-3"> | ||||||
|  | 				<a class="stopwatch-link tw-flex tw-items-center tw-gap-2 muted" href="{{.ActiveStopwatch.IssueLink}}"> | ||||||
|  | 					{{svg "octicon-issue-opened" 16}} | ||||||
|  | 					<span class="stopwatch-issue">{{.ActiveStopwatch.RepoSlug}}#{{.ActiveStopwatch.IssueIndex}}</span> | ||||||
|  | 				</a> | ||||||
|  | 				<div class="tw-flex tw-gap-1"> | ||||||
|  | 					<form class="stopwatch-commit" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/toggle"> | ||||||
|  | 						{{.CsrfTokenHtml}} | ||||||
|  | 						<button | ||||||
|  | 							type="submit" | ||||||
|  | 							class="ui button mini compact basic icon tw-mr-0" | ||||||
|  | 							data-tooltip-content="{{ctx.Locale.Tr "repo.issues.stop_tracking"}}" | ||||||
|  | 						>{{svg "octicon-square-fill"}}</button> | ||||||
|  | 					</form> | ||||||
|  | 					<form class="stopwatch-cancel" method="post" action="{{.ActiveStopwatch.IssueLink}}/times/stopwatch/cancel"> | ||||||
|  | 						{{.CsrfTokenHtml}} | ||||||
|  | 						<button | ||||||
|  | 							type="submit" | ||||||
|  | 							class="ui button mini compact basic icon tw-mr-0" | ||||||
|  | 							data-tooltip-content="{{ctx.Locale.Tr "repo.issues.cancel_tracking"}}" | ||||||
|  | 						>{{svg "octicon-trash"}}</button> | ||||||
|  | 					</form> | ||||||
|  | 				</div> | ||||||
|  | 			</div> | ||||||
|  | 		</div> | ||||||
|  | 	{{end}} | ||||||
| </nav> | </nav> | ||||||
|   | |||||||
| @@ -103,19 +103,12 @@ | |||||||
|     width: 50%; |     width: 50%; | ||||||
|     min-height: 48px; |     min-height: 48px; | ||||||
|   } |   } | ||||||
|  |   #navbar #mobile-stopwatch-icon, | ||||||
|   #navbar #mobile-notifications-icon { |   #navbar #mobile-notifications-icon { | ||||||
|     margin-right: 6px !important; |     margin-right: 6px !important; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| #navbar a.item .notification_count { |  | ||||||
|   color: var(--color-nav-bg); |  | ||||||
|   padding: 0 3.75px; |  | ||||||
|   font-size: 12px; |  | ||||||
|   line-height: 12px; |  | ||||||
|   font-weight: var(--font-weight-bold); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #navbar a.item:hover .notification_count, | #navbar a.item:hover .notification_count, | ||||||
| #navbar a.item:hover .header-stopwatch-dot { | #navbar a.item:hover .header-stopwatch-dot { | ||||||
|   border-color: var(--color-nav-hover-bg); |   border-color: var(--color-nav-hover-bg); | ||||||
| @@ -123,6 +116,11 @@ | |||||||
|  |  | ||||||
| #navbar a.item .notification_count, | #navbar a.item .notification_count, | ||||||
| #navbar a.item .header-stopwatch-dot { | #navbar a.item .header-stopwatch-dot { | ||||||
|  |   color: var(--color-nav-bg); | ||||||
|  |   padding: 0 3.75px; | ||||||
|  |   font-size: 12px; | ||||||
|  |   line-height: 12px; | ||||||
|  |   font-weight: var(--font-weight-bold); | ||||||
|   background: var(--color-primary); |   background: var(--color-primary); | ||||||
|   border: 2px solid var(--color-nav-bg); |   border: 2px solid var(--color-nav-bg); | ||||||
|   position: absolute; |   position: absolute; | ||||||
| @@ -135,6 +133,8 @@ | |||||||
|   align-items: center; |   align-items: center; | ||||||
|   justify-content: center; |   justify-content: center; | ||||||
|   z-index: 1; /* prevent menu button background from overlaying icon */ |   z-index: 1; /* prevent menu button background from overlaying icon */ | ||||||
|  |   user-select: none; | ||||||
|  |   white-space: nowrap; | ||||||
| } | } | ||||||
|  |  | ||||||
| .secondary-nav { | .secondary-nav { | ||||||
|   | |||||||
| @@ -16,8 +16,8 @@ | |||||||
|  |  | ||||||
| .tippy-box { | .tippy-box { | ||||||
|   position: relative; |   position: relative; | ||||||
|   background-color: var(--color-body); |   background-color: var(--color-menu); | ||||||
|   color: var(--color-secondary-dark-6); |   color: var(--color-text); | ||||||
|   border: 1px solid var(--color-secondary); |   border: 1px solid var(--color-secondary); | ||||||
|   border-radius: var(--border-radius); |   border-radius: var(--border-radius); | ||||||
|   font-size: 1rem; |   font-size: 1rem; | ||||||
| @@ -25,7 +25,6 @@ | |||||||
|  |  | ||||||
| .tippy-content { | .tippy-content { | ||||||
|   position: relative; |   position: relative; | ||||||
|   padding: 1rem; /* if you need different padding, use different data-theme */ |  | ||||||
|   z-index: 1; |   z-index: 1; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -166,5 +165,5 @@ | |||||||
| } | } | ||||||
|  |  | ||||||
| .tippy-svg-arrow-inner { | .tippy-svg-arrow-inner { | ||||||
|   fill: var(--color-body); |   fill: var(--color-menu); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -18,6 +18,7 @@ export function attachRefIssueContextPopup(refIssues) { | |||||||
|     if (!owner) return; |     if (!owner) return; | ||||||
|  |  | ||||||
|     const el = document.createElement('div'); |     const el = document.createElement('div'); | ||||||
|  |     el.classList.add('tw-p-3'); | ||||||
|     refIssue.parentNode.insertBefore(el, refIssue.nextSibling); |     refIssue.parentNode.insertBefore(el, refIssue.nextSibling); | ||||||
|  |  | ||||||
|     const view = createApp(ContextPopup); |     const view = createApp(ContextPopup); | ||||||
| @@ -30,6 +31,7 @@ export function attachRefIssueContextPopup(refIssues) { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     createTippy(refIssue, { |     createTippy(refIssue, { | ||||||
|  |       theme: 'default', | ||||||
|       content: el, |       content: el, | ||||||
|       placement: 'top-start', |       placement: 'top-start', | ||||||
|       interactive: true, |       interactive: true, | ||||||
|   | |||||||
| @@ -113,6 +113,7 @@ function showLineButton() { | |||||||
|   btn.closest('.code-view').append(menu.cloneNode(true)); |   btn.closest('.code-view').append(menu.cloneNode(true)); | ||||||
|  |  | ||||||
|   createTippy(btn, { |   createTippy(btn, { | ||||||
|  |     theme: 'menu', | ||||||
|     trigger: 'click', |     trigger: 'click', | ||||||
|     hideOnClick: true, |     hideOnClick: true, | ||||||
|     content: menu, |     content: menu, | ||||||
|   | |||||||
| @@ -502,6 +502,7 @@ export function initRepoPullRequestReview() { | |||||||
|   if ($reviewBtn.length && $panel.length) { |   if ($reviewBtn.length && $panel.length) { | ||||||
|     const tippy = createTippy($reviewBtn[0], { |     const tippy = createTippy($reviewBtn[0], { | ||||||
|       content: $panel[0], |       content: $panel[0], | ||||||
|  |       theme: 'default', | ||||||
|       placement: 'bottom', |       placement: 'bottom', | ||||||
|       trigger: 'click', |       trigger: 'click', | ||||||
|       maxWidth: 'none', |       maxWidth: 'none', | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| import prettyMilliseconds from 'pretty-ms'; |  | ||||||
| import {createTippy} from '../modules/tippy.js'; | import {createTippy} from '../modules/tippy.js'; | ||||||
| import {GET} from '../modules/fetch.js'; | import {GET} from '../modules/fetch.js'; | ||||||
| import {hideElem, showElem} from '../utils/dom.js'; | import {hideElem, showElem} from '../utils/dom.js'; | ||||||
| @@ -10,28 +9,31 @@ export function initStopwatch() { | |||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   const stopwatchEl = document.querySelector('.active-stopwatch-trigger'); |   const stopwatchEls = document.querySelectorAll('.active-stopwatch'); | ||||||
|   const stopwatchPopup = document.querySelector('.active-stopwatch-popup'); |   const stopwatchPopup = document.querySelector('.active-stopwatch-popup'); | ||||||
|  |  | ||||||
|   if (!stopwatchEl || !stopwatchPopup) { |   if (!stopwatchEls.length || !stopwatchPopup) { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   stopwatchEl.removeAttribute('href'); // intended for noscript mode only |  | ||||||
|  |  | ||||||
|   createTippy(stopwatchEl, { |  | ||||||
|     content: stopwatchPopup, |  | ||||||
|     placement: 'bottom-end', |  | ||||||
|     trigger: 'click', |  | ||||||
|     maxWidth: 'none', |  | ||||||
|     interactive: true, |  | ||||||
|     hideOnClick: true, |  | ||||||
|   }); |  | ||||||
|  |  | ||||||
|   // global stop watch (in the head_navbar), it should always work in any case either the EventSource or the PeriodicPoller is used. |   // global stop watch (in the head_navbar), it should always work in any case either the EventSource or the PeriodicPoller is used. | ||||||
|   const currSeconds = document.querySelector('.stopwatch-time')?.getAttribute('data-seconds'); |   const seconds = stopwatchEls[0]?.getAttribute('data-seconds'); | ||||||
|   if (currSeconds) { |   if (seconds) { | ||||||
|     updateStopwatchTime(currSeconds); |     updateStopwatchTime(parseInt(seconds)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   for (const stopwatchEl of stopwatchEls) { | ||||||
|  |     stopwatchEl.removeAttribute('href'); // intended for noscript mode only | ||||||
|  |  | ||||||
|  |     createTippy(stopwatchEl, { | ||||||
|  |       content: stopwatchPopup.cloneNode(true), | ||||||
|  |       placement: 'bottom-end', | ||||||
|  |       trigger: 'click', | ||||||
|  |       maxWidth: 'none', | ||||||
|  |       interactive: true, | ||||||
|  |       hideOnClick: true, | ||||||
|  |       theme: 'default', | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   let usingPeriodicPoller = false; |   let usingPeriodicPoller = false; | ||||||
| @@ -124,10 +126,9 @@ async function updateStopwatch() { | |||||||
|  |  | ||||||
| function updateStopwatchData(data) { | function updateStopwatchData(data) { | ||||||
|   const watch = data[0]; |   const watch = data[0]; | ||||||
|   const btnEl = document.querySelector('.active-stopwatch-trigger'); |   const btnEls = document.querySelectorAll('.active-stopwatch'); | ||||||
|   if (!watch) { |   if (!watch) { | ||||||
|     clearStopwatchTimer(); |     hideElem(btnEls); | ||||||
|     hideElem(btnEl); |  | ||||||
|   } else { |   } else { | ||||||
|     const {repo_owner_name, repo_name, issue_index, seconds} = watch; |     const {repo_owner_name, repo_name, issue_index, seconds} = watch; | ||||||
|     const issueUrl = `${appSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`; |     const issueUrl = `${appSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`; | ||||||
| @@ -137,31 +138,28 @@ function updateStopwatchData(data) { | |||||||
|     const stopwatchIssue = document.querySelector('.stopwatch-issue'); |     const stopwatchIssue = document.querySelector('.stopwatch-issue'); | ||||||
|     if (stopwatchIssue) stopwatchIssue.textContent = `${repo_owner_name}/${repo_name}#${issue_index}`; |     if (stopwatchIssue) stopwatchIssue.textContent = `${repo_owner_name}/${repo_name}#${issue_index}`; | ||||||
|     updateStopwatchTime(seconds); |     updateStopwatchTime(seconds); | ||||||
|     showElem(btnEl); |     showElem(btnEls); | ||||||
|   } |   } | ||||||
|   return Boolean(data.length); |   return Boolean(data.length); | ||||||
| } | } | ||||||
|  |  | ||||||
| let updateTimeIntervalId = null; // holds setInterval id when active | // TODO: This flickers on page load, we could avoid this by making a custom | ||||||
| function clearStopwatchTimer() { | // element to render time periods. Feeding a datetime in backend does not work | ||||||
|   if (updateTimeIntervalId !== null) { | // when time zone between server and client differs. | ||||||
|     clearInterval(updateTimeIntervalId); | function updateStopwatchTime(seconds) { | ||||||
|     updateTimeIntervalId = null; |   if (!Number.isFinite(seconds)) return; | ||||||
|  |   const datetime = (new Date(Date.now() - seconds * 1000)).toISOString(); | ||||||
|  |   for (const parent of document.querySelectorAll('.header-stopwatch-dot')) { | ||||||
|  |     const existing = parent.querySelector(':scope > relative-time'); | ||||||
|  |     if (existing) { | ||||||
|  |       existing.setAttribute('datetime', datetime); | ||||||
|  |     } else { | ||||||
|  |       const el = document.createElement('relative-time'); | ||||||
|  |       el.setAttribute('format', 'micro'); | ||||||
|  |       el.setAttribute('datetime', datetime); | ||||||
|  |       el.setAttribute('lang', 'en-US'); | ||||||
|  |       el.setAttribute('title', ''); // make <relative-time> show no title and therefor no tooltip | ||||||
|  |       parent.append(el); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| function updateStopwatchTime(seconds) { |  | ||||||
|   const secs = parseInt(seconds); |  | ||||||
|   if (!Number.isFinite(secs)) return; |  | ||||||
|  |  | ||||||
|   clearStopwatchTimer(); |  | ||||||
|   const stopwatch = document.querySelector('.stopwatch-time'); |  | ||||||
|   // TODO: replace with <relative-time> similar to how system status up time is shown |  | ||||||
|   const start = Date.now(); |  | ||||||
|   const updateUi = () => { |  | ||||||
|     const delta = Date.now() - start; |  | ||||||
|     const dur = prettyMilliseconds(secs * 1000 + delta, {compact: true}); |  | ||||||
|     if (stopwatch) stopwatch.textContent = dur; |  | ||||||
|   }; |  | ||||||
|   updateUi(); |  | ||||||
|   updateTimeIntervalId = setInterval(updateUi, 1000); |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -37,8 +37,10 @@ export function createTippy(target, opts = {}) { | |||||||
|       return onShow?.(instance); |       return onShow?.(instance); | ||||||
|     }, |     }, | ||||||
|     arrow: arrow || (theme === 'bare' ? false : arrowSvg), |     arrow: arrow || (theme === 'bare' ? false : arrowSvg), | ||||||
|     role: role || 'menu', // HTML role attribute |     // HTML role attribute, ideally the default role would be "popover" but it does not exist | ||||||
|     theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu", "box-with-header" or "bare" |     role: role || 'menu', | ||||||
|  |     // CSS theme, either "default", "tooltip", "menu", "box-with-header" or "bare" | ||||||
|  |     theme: theme || role || 'default', | ||||||
|     plugins: [followCursor], |     plugins: [followCursor], | ||||||
|     ...other, |     ...other, | ||||||
|   }); |   }); | ||||||
|   | |||||||
| @@ -131,6 +131,7 @@ window.customElements.define('overflow-menu', class extends HTMLElement { | |||||||
|       interactive: true, |       interactive: true, | ||||||
|       placement: 'bottom-end', |       placement: 'bottom-end', | ||||||
|       role: 'menu', |       role: 'menu', | ||||||
|  |       theme: 'menu', | ||||||
|       content: this.tippyContent, |       content: this.tippyContent, | ||||||
|       onShow: () => { // FIXME: onShown doesn't work (never be called) |       onShow: () => { // FIXME: onShown doesn't work (never be called) | ||||||
|         setTimeout(() => { |         setTimeout(() => { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user