mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	Use auto-updating, natively hoverable, localized time elements (#23988)
- Added [GitHub's `relative-time` element](https://github.com/github/relative-time-element) - Converted all formatted timestamps to use this element - No more flashes of unstyled content around time elements - These elements are localized using the `lang` property of the HTML file - Relative (e.g. the activities in the dashboard) and duration (e.g. server uptime in the admin page) time elements are auto-updated to keep up with the current time without refreshing the page - Code that is not needed anymore such as `formatting.js` and parts of `since.go` have been deleted Replaces #21440 Follows #22861 ## Screenshots ### Localized   ### Tooltips #### Native for dates  #### Interactive for relative  ### Auto-update  --------- Signed-off-by: Yarden Shoham <git@yardenshoham.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: delvh <dev.lh@web.de>
This commit is contained in:
		| @@ -178,7 +178,7 @@ export function initAdminCommon() { | ||||
|     // Attach view detail modals | ||||
|     $('.view-detail').on('click', function () { | ||||
|       $detailModal.find('.content pre').text($(this).parents('tr').find('.notice-description').text()); | ||||
|       $detailModal.find('.sub.header').text($(this).parents('tr').find('.notice-created-time').text()); | ||||
|       $detailModal.find('.sub.header').text($(this).parents('tr').find('relative-time').attr('title')); | ||||
|       $detailModal.modal('show'); | ||||
|       return false; | ||||
|     }); | ||||
|   | ||||
| @@ -1,31 +0,0 @@ | ||||
| const {lang} = document.documentElement; | ||||
| const dateFormatter = new Intl.DateTimeFormat(lang, {year: 'numeric', month: 'long', day: 'numeric'}); | ||||
| const shortDateFormatter = new Intl.DateTimeFormat(lang, {year: 'numeric', month: 'short', day: 'numeric'}); | ||||
| const dateTimeFormatter = new Intl.DateTimeFormat(lang, {year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric'}); | ||||
|  | ||||
| export function initFormattingReplacements() { | ||||
|   // for each <time></time> tag, if it has the data-format attribute, format | ||||
|   // the text according to the user's chosen locale and formatter. | ||||
|   formatAllTimeElements(); | ||||
| } | ||||
|  | ||||
| function formatAllTimeElements() { | ||||
|   const timeElements = document.querySelectorAll('time[data-format]'); | ||||
|   for (const timeElement of timeElements) { | ||||
|     const formatter = getFormatter(timeElement.dataset.format); | ||||
|     timeElement.textContent = formatter.format(new Date(timeElement.dateTime)); | ||||
|   } | ||||
| } | ||||
|  | ||||
| function getFormatter(format) { | ||||
|   switch (format) { | ||||
|     case 'date': | ||||
|       return dateFormatter; | ||||
|     case 'short-date': | ||||
|       return shortDateFormatter; | ||||
|     case 'date-time': | ||||
|       return dateTimeFormatter; | ||||
|     default: | ||||
|       throw new Error('Unknown format'); | ||||
|   } | ||||
| } | ||||
| @@ -74,7 +74,6 @@ import {initRepoBranchButton} from './features/repo-branch.js'; | ||||
| import {initCommonOrganization} from './features/common-organization.js'; | ||||
| import {initRepoWikiForm} from './features/repo-wiki.js'; | ||||
| import {initRepoCommentForm, initRepository} from './features/repo-legacy.js'; | ||||
| import {initFormattingReplacements} from './features/formatting.js'; | ||||
| import {initCopyContent} from './features/copycontent.js'; | ||||
| import {initCaptcha} from './features/captcha.js'; | ||||
| import {initRepositoryActionView} from './components/RepoActionView.vue'; | ||||
| @@ -83,10 +82,6 @@ import {initGiteaFomantic} from './modules/fomantic.js'; | ||||
| import {onDomReady} from './utils/dom.js'; | ||||
| import {initRepoIssueList} from './features/repo-issue-list.js'; | ||||
|  | ||||
| // Run time-critical code as soon as possible. This is safe to do because this | ||||
| // script appears at the end of <body> and rendered HTML is accessible at that point. | ||||
| // TODO: replace them with CustomElements | ||||
| initFormattingReplacements(); | ||||
| // Init Gitea's Fomantic settings | ||||
| initGiteaFomantic(); | ||||
|  | ||||
|   | ||||
| @@ -6,7 +6,7 @@ export function createTippy(target, opts = {}) { | ||||
|     animation: false, | ||||
|     allowHTML: false, | ||||
|     hideOnClick: false, | ||||
|     interactiveBorder: 30, | ||||
|     interactiveBorder: 20, | ||||
|     ignoreAttributes: true, | ||||
|     maxWidth: 500, // increase over default 350px | ||||
|     arrow: `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`, | ||||
| @@ -36,6 +36,8 @@ export function createTippy(target, opts = {}) { | ||||
|  * @returns {null|tippy} | ||||
|  */ | ||||
| function attachTooltip(target, content = null) { | ||||
|   switchTitleToTooltip(target); | ||||
|  | ||||
|   content = content ?? target.getAttribute('data-tooltip-content'); | ||||
|   if (!content) return null; | ||||
|  | ||||
| @@ -55,6 +57,18 @@ function attachTooltip(target, content = null) { | ||||
|   return target._tippy; | ||||
| } | ||||
|  | ||||
| function switchTitleToTooltip(target) { | ||||
|   const title = target.getAttribute('title'); | ||||
|   if (title) { | ||||
|     target.setAttribute('data-tooltip-content', title); | ||||
|     target.setAttribute('aria-label', title); | ||||
|     // keep the attribute, in case there are some other "[title]" selectors | ||||
|     // and to prevent infinite loop with <relative-time> which will re-add | ||||
|     // title if it is absent | ||||
|     target.setAttribute('title', ''); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Creating tooltip tippy instance is expensive, so we only create it when the user hovers over the element | ||||
|  * According to https://www.w3.org/TR/DOM-Level-3-Events/#events-mouseevent-event-order , mouseover event is fired before mouseenter event | ||||
| @@ -67,48 +81,57 @@ function lazyTooltipOnMouseHover(e) { | ||||
|   attachTooltip(this); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Activate the tooltip for all children elements | ||||
|  * And if the element has no aria-label, use the tooltip content as aria-label | ||||
|  * @param target {HTMLElement} | ||||
|  */ | ||||
| function attachChildrenLazyTooltip(target) { | ||||
|   for (const el of target.querySelectorAll('[data-tooltip-content]')) { | ||||
|     el.addEventListener('mouseover', lazyTooltipOnMouseHover, true); | ||||
| // Activate the tooltip for current element. | ||||
| // If the element has no aria-label, use the tooltip content as aria-label. | ||||
| function attachLazyTooltip(el) { | ||||
|   el.addEventListener('mouseover', lazyTooltipOnMouseHover, {capture: true}); | ||||
|  | ||||
|     // meanwhile, if the element has no aria-label, use the tooltip content as aria-label | ||||
|     if (!el.hasAttribute('aria-label')) { | ||||
|       const content = target.getAttribute('data-tooltip-content'); | ||||
|       if (content) { | ||||
|         el.setAttribute('aria-label', content); | ||||
|       } | ||||
|   // meanwhile, if the element has no aria-label, use the tooltip content as aria-label | ||||
|   if (!el.hasAttribute('aria-label')) { | ||||
|     const content = el.getAttribute('data-tooltip-content'); | ||||
|     if (content) { | ||||
|       el.setAttribute('aria-label', content); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| // Activate the tooltip for all children elements. | ||||
| function attachChildrenLazyTooltip(target) { | ||||
|   for (const el of target.querySelectorAll('[data-tooltip-content]')) { | ||||
|     attachLazyTooltip(el); | ||||
|   } | ||||
| } | ||||
|  | ||||
| const elementNodeTypes = new Set([Node.ELEMENT_NODE, Node.DOCUMENT_FRAGMENT_NODE]); | ||||
|  | ||||
| export function initGlobalTooltips() { | ||||
|   // use MutationObserver to detect new elements added to the DOM, or attributes changed | ||||
|   const observer = new MutationObserver((mutationList) => { | ||||
|     for (const mutation of mutationList) { | ||||
|   // use MutationObserver to detect new "data-tooltip-content" elements added to the DOM, or attributes changed | ||||
|   const observerConnect = (observer) => observer.observe(document, { | ||||
|     subtree: true, | ||||
|     childList: true, | ||||
|     attributeFilter: ['data-tooltip-content', 'title'] | ||||
|   }); | ||||
|   const observer = new MutationObserver((mutationList, observer) => { | ||||
|     const pending = observer.takeRecords(); | ||||
|     observer.disconnect(); | ||||
|     for (const mutation of [...mutationList, ...pending]) { | ||||
|       if (mutation.type === 'childList') { | ||||
|         // mainly for Vue components and AJAX rendered elements | ||||
|         for (const el of mutation.addedNodes) { | ||||
|           // handle all "tooltip" elements in added nodes which have 'querySelectorAll' method, skip non-related nodes (eg: "#text") | ||||
|           if ('querySelectorAll' in el) { | ||||
|           if (elementNodeTypes.has(el.nodeType)) { | ||||
|             attachChildrenLazyTooltip(el); | ||||
|             if (el.hasAttribute('data-tooltip-content')) { | ||||
|               attachLazyTooltip(el); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } else if (mutation.type === 'attributes') { | ||||
|         // sync the tooltip content if the attributes change | ||||
|         attachTooltip(mutation.target); | ||||
|       } | ||||
|     } | ||||
|     observerConnect(observer); | ||||
|   }); | ||||
|   observer.observe(document, { | ||||
|     subtree: true, | ||||
|     childList: true, | ||||
|     attributeFilter: ['data-tooltip-content'], | ||||
|   }); | ||||
|   observerConnect(observer); | ||||
|  | ||||
|   attachChildrenLazyTooltip(document.documentElement); | ||||
| } | ||||
|   | ||||
| @@ -10,9 +10,3 @@ https://developer.mozilla.org/en-US/docs/Web/Web_Components | ||||
|   so they should have their own dependencies and should be very light, | ||||
|   then they won't affect the page loading time too much. | ||||
| * If the component is not a public one, it's suggested to have its own `Gitea` or `gitea-` prefix to avoid conflicts. | ||||
|  | ||||
| # TODO | ||||
|  | ||||
| There are still some components that are not migrated to web components yet: | ||||
|  | ||||
| * `<time data-format>` | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import '@webcomponents/custom-elements'; // polyfill for some browsers like Pale Moon | ||||
| import '@github/relative-time-element'; | ||||
| import './GiteaLocaleNumber.js'; | ||||
| import './GiteaOriginUrl.js'; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user