diff options
author | Yarden Shoham <git@yardenshoham.com> | 2023-04-11 02:01:20 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-11 01:01:20 +0200 |
commit | b7b58348317cbe0145dc453d45c886b8e2764b4c (patch) | |
tree | cec91679aaba2b2d74242e6acdc7d9a8f8d3f213 /web_src/js | |
parent | 2b91841cd3e1213ff3e4ed4209d6a4be89c2fa79 (diff) | |
download | gitea-b7b58348317cbe0145dc453d45c886b8e2764b4c.tar.gz gitea-b7b58348317cbe0145dc453d45c886b8e2764b4c.zip |
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
![image](https://user-images.githubusercontent.com/20454870/230775041-f0af4fda-8f6b-46d3-b8e3-d340c791a50c.png)
![image](https://user-images.githubusercontent.com/20454870/230673393-931415a9-5729-4ac3-9a89-c0fb5fbeeeb7.png)
### Tooltips
#### Native for dates
![image](https://user-images.githubusercontent.com/20454870/230797525-1fa0a854-83e3-484c-9da5-9425ab6528a3.png)
#### Interactive for relative
![image](https://user-images.githubusercontent.com/115237/230796860-51e1d640-c820-4a34-ba2e-39087020626a.png)
### Auto-update
![rec](https://user-images.githubusercontent.com/20454870/230672159-37480d8f-435a-43e9-a2b0-44073351c805.gif)
---------
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>
Diffstat (limited to 'web_src/js')
-rw-r--r-- | web_src/js/features/admin/common.js | 2 | ||||
-rw-r--r-- | web_src/js/features/formatting.js | 31 | ||||
-rw-r--r-- | web_src/js/index.js | 5 | ||||
-rw-r--r-- | web_src/js/modules/tippy.js | 75 | ||||
-rw-r--r-- | web_src/js/webcomponents/README.md | 6 | ||||
-rw-r--r-- | web_src/js/webcomponents/webcomponents.js | 1 |
6 files changed, 51 insertions, 69 deletions
diff --git a/web_src/js/features/admin/common.js b/web_src/js/features/admin/common.js index be5aa876a5..8f895152c7 100644 --- a/web_src/js/features/admin/common.js +++ b/web_src/js/features/admin/common.js @@ -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; }); diff --git a/web_src/js/features/formatting.js b/web_src/js/features/formatting.js deleted file mode 100644 index 5590ba44d1..0000000000 --- a/web_src/js/features/formatting.js +++ /dev/null @@ -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'); - } -} diff --git a/web_src/js/index.js b/web_src/js/index.js index 878afb10d7..f7cbb24e85 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -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(); diff --git a/web_src/js/modules/tippy.js b/web_src/js/modules/tippy.js index 0d57af4f0f..6cec95d766 100644 --- a/web_src/js/modules/tippy.js +++ b/web_src/js/modules/tippy.js @@ -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); } diff --git a/web_src/js/webcomponents/README.md b/web_src/js/webcomponents/README.md index 2b586a63d2..0fde507310 100644 --- a/web_src/js/webcomponents/README.md +++ b/web_src/js/webcomponents/README.md @@ -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>` diff --git a/web_src/js/webcomponents/webcomponents.js b/web_src/js/webcomponents/webcomponents.js index 5c4afb1eec..7e8135aa00 100644 --- a/web_src/js/webcomponents/webcomponents.js +++ b/web_src/js/webcomponents/webcomponents.js @@ -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'; |