mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-04 20:44:09 +00:00
enhance(ui): cleanup navbar template and styling (#39554)
Clean up the navbar template and styling, and fix the navbar stopwatch, which navigated to the issue instead of opening its popup since https://github.com/go-gitea/gitea/pull/36965. 1. Add hover background to the create and user menus 2. Simplify navbar HTML and CSS and remove Fomantic styles 3. Render the notification and stopwatch icons once instead of separate mobile and desktop copies 4. Make the stopwatch a keyboard accessible button whose popup updates on push and closes when the stopwatch stops Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: bircni <bircni@icloud.com>
This commit is contained in:
@@ -1,51 +1,79 @@
|
||||
import {createTippy} from '../modules/tippy.ts';
|
||||
import {GET} from '../modules/fetch.ts';
|
||||
import {hideElem, queryElems, showElem} from '../utils/dom.ts';
|
||||
import {hideElem, showElem} from '../utils/dom.ts';
|
||||
import {onUserEvent} from '../modules/worker.ts';
|
||||
import type {StopwatchData} from '../types.ts';
|
||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
|
||||
const {appSubUrl, notificationSettings, enableTimeTracking} = window.config;
|
||||
|
||||
export function initStopwatch() {
|
||||
if (!enableTimeTracking) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stopwatchEls = document.querySelectorAll('.active-stopwatch');
|
||||
const stopwatchPopup = document.querySelector('.active-stopwatch-popup');
|
||||
|
||||
if (!stopwatchEls.length || !stopwatchPopup) {
|
||||
return;
|
||||
}
|
||||
const {appSubUrl, notificationSettings} = window.config;
|
||||
|
||||
export const initStopwatch = () => registerGlobalInitFunc('initActiveStopwatchNotification', (btn: HTMLElement) => {
|
||||
// Init the icon + popup even when no stopwatch is active so a real-time push has a target to toggle.
|
||||
const seconds = stopwatchEls[0]?.getAttribute('data-seconds');
|
||||
if (seconds) {
|
||||
updateStopwatchTime(parseInt(seconds));
|
||||
}
|
||||
const popup = btn.nextElementSibling!;
|
||||
const tippy = createTippy(btn, {
|
||||
content: popup,
|
||||
placement: 'bottom-end',
|
||||
trigger: 'click',
|
||||
maxWidth: 'none',
|
||||
interactive: true,
|
||||
hideOnClick: true,
|
||||
theme: 'default',
|
||||
});
|
||||
|
||||
for (const stopwatchEl of stopwatchEls) {
|
||||
createTippy(stopwatchEl, {
|
||||
content: stopwatchPopup.cloneNode(true) as Element,
|
||||
placement: 'bottom-end',
|
||||
trigger: 'click',
|
||||
maxWidth: 'none',
|
||||
interactive: true,
|
||||
hideOnClick: true,
|
||||
theme: 'default',
|
||||
onShow(instance) {
|
||||
// Re-clone on every open so the popup reflects the latest stopwatch state,
|
||||
// including the case where the icon became visible via a real-time push.
|
||||
instance.setContent(stopwatchPopup.cloneNode(true) as Element);
|
||||
},
|
||||
});
|
||||
}
|
||||
// TODO: This flickers on page load, we could avoid this by making a custom element to render time periods.
|
||||
const updateStopwatchTime = (seconds: number) => {
|
||||
const hours = seconds / 3600 || 0;
|
||||
const minutes = seconds / 60 || 0;
|
||||
btn.querySelector('.header-stopwatch-dot')!.textContent = hours >= 1 ? `${Math.round(hours)}h` : `${Math.round(minutes)}m`;
|
||||
};
|
||||
|
||||
const updateStopwatchData = (data: Array<StopwatchData>) => {
|
||||
const watch = data[0];
|
||||
if (!watch) {
|
||||
tippy.hide();
|
||||
hideElem(btn);
|
||||
return false;
|
||||
}
|
||||
const {repo_owner_name, repo_name, issue_index, seconds} = watch;
|
||||
const issueUrl = `${appSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`;
|
||||
popup.querySelector('.stopwatch-link')!.setAttribute('href', issueUrl);
|
||||
popup.querySelector('.stopwatch-commit')!.setAttribute('action', `${issueUrl}/times/stopwatch/stop`);
|
||||
popup.querySelector('.stopwatch-cancel')!.setAttribute('action', `${issueUrl}/times/stopwatch/cancel`);
|
||||
popup.querySelector('.stopwatch-issue')!.textContent = `${repo_owner_name}/${repo_name}#${issue_index}`;
|
||||
updateStopwatchTime(seconds);
|
||||
showElem(btn);
|
||||
return true;
|
||||
};
|
||||
|
||||
const updateStopwatch = async () => {
|
||||
try {
|
||||
const response = await GET(`${appSubUrl}/user/stopwatches`);
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch stopwatch data');
|
||||
return false;
|
||||
}
|
||||
return updateStopwatchData(await response.json());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const startPeriodicPoller = (timeout: number) => {
|
||||
if (timeout <= 0 || !Number.isFinite(timeout)) return;
|
||||
setTimeout(() => updateStopwatchWithCallback(startPeriodicPoller, timeout), timeout);
|
||||
setTimeout(async () => {
|
||||
if (!await updateStopwatch()) {
|
||||
timeout = notificationSettings.MinTimeout;
|
||||
} else if (timeout < notificationSettings.MaxTimeout) {
|
||||
timeout += notificationSettings.TimeoutStep;
|
||||
}
|
||||
startPeriodicPoller(timeout);
|
||||
}, timeout);
|
||||
};
|
||||
|
||||
const seconds = btn.getAttribute('data-seconds');
|
||||
if (seconds) updateStopwatchTime(parseInt(seconds));
|
||||
|
||||
let pollerStarted = false;
|
||||
onUserEvent('stopwatches', (msg) => updateStopwatchData(msg.eventData));
|
||||
// On each (re)connect, reconcile stopwatch state from the server to recover any push dropped during the connect gap.
|
||||
@@ -55,60 +83,4 @@ export function initStopwatch() {
|
||||
pollerStarted = true;
|
||||
startPeriodicPoller(notificationSettings.MinTimeout);
|
||||
});
|
||||
}
|
||||
|
||||
async function updateStopwatchWithCallback(callback: (timeout: number) => void, timeout: number) {
|
||||
const isSet = await updateStopwatch();
|
||||
|
||||
if (!isSet) {
|
||||
timeout = notificationSettings.MinTimeout;
|
||||
} else if (timeout < notificationSettings.MaxTimeout) {
|
||||
timeout += notificationSettings.TimeoutStep;
|
||||
}
|
||||
|
||||
callback(timeout);
|
||||
}
|
||||
|
||||
async function updateStopwatch() {
|
||||
try {
|
||||
const response = await GET(`${appSubUrl}/user/stopwatches`);
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch stopwatch data');
|
||||
return false;
|
||||
}
|
||||
const data = await response.json();
|
||||
return updateStopwatchData(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function updateStopwatchData(data: Array<StopwatchData>) {
|
||||
if (!data) return;
|
||||
const watch = data[0];
|
||||
const btnEls = document.querySelectorAll('.active-stopwatch');
|
||||
if (!watch) {
|
||||
hideElem(btnEls);
|
||||
} else {
|
||||
const {repo_owner_name, repo_name, issue_index, seconds} = watch;
|
||||
const issueUrl = `${appSubUrl}/${repo_owner_name}/${repo_name}/issues/${issue_index}`;
|
||||
for (const btnEl of btnEls) btnEl.setAttribute('href', issueUrl);
|
||||
document.querySelector('.stopwatch-link')?.setAttribute('href', issueUrl);
|
||||
document.querySelector('.stopwatch-commit')?.setAttribute('action', `${issueUrl}/times/stopwatch/stop`);
|
||||
document.querySelector('.stopwatch-cancel')?.setAttribute('action', `${issueUrl}/times/stopwatch/cancel`);
|
||||
const stopwatchIssue = document.querySelector('.stopwatch-issue');
|
||||
if (stopwatchIssue) stopwatchIssue.textContent = `${repo_owner_name}/${repo_name}#${issue_index}`;
|
||||
updateStopwatchTime(seconds);
|
||||
showElem(btnEls);
|
||||
}
|
||||
return Boolean(data.length);
|
||||
}
|
||||
|
||||
// TODO: This flickers on page load, we could avoid this by making a custom element to render time periods.
|
||||
function updateStopwatchTime(seconds: number) {
|
||||
const hours = seconds / 3600 || 0;
|
||||
const minutes = seconds / 60 || 0;
|
||||
const timeText = hours >= 1 ? `${Math.round(hours)}h` : `${Math.round(minutes)}m`;
|
||||
queryElems(document, '.header-stopwatch-dot', (el) => el.textContent = timeText);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user