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>
pull/23896/head
Yarden Shoham 2 years ago committed by GitHub
parent 2b91841cd3
commit b7b5834831
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      modules/templates/helper.go
  2. 135
      modules/timeutil/since.go
  3. 95
      modules/timeutil/since_test.go
  4. 5
      modules/timeutil/timestamp.go
  5. 3
      options/locale/locale_en-US.ini
  6. 6
      package-lock.json
  7. 1
      package.json
  8. 6
      routers/web/admin/admin.go
  9. 4
      templates/admin/auth/list.tmpl
  10. 4
      templates/admin/cron.tmpl
  11. 2
      templates/admin/dashboard.tmpl
  12. 2
      templates/admin/notice.tmpl
  13. 2
      templates/admin/org/list.tmpl
  14. 2
      templates/admin/packages/list.tmpl
  15. 2
      templates/admin/process-row.tmpl
  16. 4
      templates/admin/queue.tmpl
  17. 2
      templates/admin/repo/list.tmpl
  18. 2
      templates/admin/stacktrace-row.tmpl
  19. 4
      templates/admin/user/list.tmpl
  20. 2
      templates/explore/organizations.tmpl
  21. 2
      templates/explore/users.tmpl
  22. 2
      templates/package/shared/cleanup_rules/preview.tmpl
  23. 2
      templates/package/view.tmpl
  24. 2
      templates/repo/activity.tmpl
  25. 2
      templates/repo/issue/view_content/sidebar.tmpl
  26. 2
      templates/repo/settings/deploy_keys.tmpl
  27. 4
      templates/repo/settings/options.tmpl
  28. 2
      templates/repo/user_cards.tmpl
  29. 1
      templates/shared/datetime/full.tmpl
  30. 1
      templates/shared/datetime/long.tmpl
  31. 1
      templates/shared/datetime/short.tmpl
  32. 2
      templates/shared/issuelist.tmpl
  33. 2
      templates/user/profile.tmpl
  34. 2
      templates/user/settings/applications.tmpl
  35. 2
      templates/user/settings/grants_oauth2.tmpl
  36. 4
      templates/user/settings/keys_gpg.tmpl
  37. 2
      templates/user/settings/keys_principal.tmpl
  38. 2
      templates/user/settings/keys_ssh.tmpl
  39. 5
      tests/integration/repo_test.go
  40. 2
      web_src/js/features/admin/common.js
  41. 31
      web_src/js/features/formatting.js
  42. 5
      web_src/js/index.js
  43. 65
      web_src/js/modules/tippy.js
  44. 6
      web_src/js/webcomponents/README.md
  45. 1
      web_src/js/webcomponents/webcomponents.js

@ -138,7 +138,7 @@ func NewFuncMap() []template.FuncMap {
"TimeSinceUnix": timeutil.TimeSinceUnix, "TimeSinceUnix": timeutil.TimeSinceUnix,
"Sec2Time": util.SecToTime, "Sec2Time": util.SecToTime,
"DateFmtLong": func(t time.Time) string { "DateFmtLong": func(t time.Time) string {
return t.Format(time.RFC1123Z) return t.Format(time.RFC3339)
}, },
"LoadTimes": func(startTime time.Time) string { "LoadTimes": func(startTime time.Time) string {
return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms" return fmt.Sprint(time.Since(startTime).Nanoseconds()/1e6) + "ms"

@ -6,11 +6,9 @@ package timeutil
import ( import (
"fmt" "fmt"
"html/template" "html/template"
"math"
"strings" "strings"
"time" "time"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
) )
@ -24,10 +22,6 @@ const (
Year = 12 * Month Year = 12 * Month
) )
func round(s float64) int64 {
return int64(math.Round(s))
}
func computeTimeDiffFloor(diff int64, lang translation.Locale) (int64, string) { func computeTimeDiffFloor(diff int64, lang translation.Locale) (int64, string) {
diffStr := "" diffStr := ""
switch { switch {
@ -86,94 +80,6 @@ func computeTimeDiffFloor(diff int64, lang translation.Locale) (int64, string) {
return diff, diffStr return diff, diffStr
} }
func computeTimeDiff(diff int64, lang translation.Locale) (int64, string) {
diffStr := ""
switch {
case diff <= 0:
diff = 0
diffStr = lang.Tr("tool.now")
case diff < 2:
diff = 0
diffStr = lang.Tr("tool.1s")
case diff < 1*Minute:
diffStr = lang.Tr("tool.seconds", diff)
diff = 0
case diff < Minute+Minute/2:
diff -= 1 * Minute
diffStr = lang.Tr("tool.1m")
case diff < 1*Hour:
minutes := round(float64(diff) / Minute)
if minutes > 1 {
diffStr = lang.Tr("tool.minutes", minutes)
} else {
diffStr = lang.Tr("tool.1m")
}
diff -= diff / Minute * Minute
case diff < Hour+Hour/2:
diff -= 1 * Hour
diffStr = lang.Tr("tool.1h")
case diff < 1*Day:
hours := round(float64(diff) / Hour)
if hours > 1 {
diffStr = lang.Tr("tool.hours", hours)
} else {
diffStr = lang.Tr("tool.1h")
}
diff -= diff / Hour * Hour
case diff < Day+Day/2:
diff -= 1 * Day
diffStr = lang.Tr("tool.1d")
case diff < 1*Week:
days := round(float64(diff) / Day)
if days > 1 {
diffStr = lang.Tr("tool.days", days)
} else {
diffStr = lang.Tr("tool.1d")
}
diff -= diff / Day * Day
case diff < Week+Week/2:
diff -= 1 * Week
diffStr = lang.Tr("tool.1w")
case diff < 1*Month:
weeks := round(float64(diff) / Week)
if weeks > 1 {
diffStr = lang.Tr("tool.weeks", weeks)
} else {
diffStr = lang.Tr("tool.1w")
}
diff -= diff / Week * Week
case diff < 1*Month+Month/2:
diff -= 1 * Month
diffStr = lang.Tr("tool.1mon")
case diff < 1*Year:
months := round(float64(diff) / Month)
if months > 1 {
diffStr = lang.Tr("tool.months", months)
} else {
diffStr = lang.Tr("tool.1mon")
}
diff -= diff / Month * Month
case diff < Year+Year/2:
diff -= 1 * Year
diffStr = lang.Tr("tool.1y")
default:
years := round(float64(diff) / Year)
if years > 1 {
diffStr = lang.Tr("tool.years", years)
} else {
diffStr = lang.Tr("tool.1y")
}
diff -= (diff / Year) * Year
}
return diff, diffStr
}
// MinutesToFriendly returns a user friendly string with number of minutes // MinutesToFriendly returns a user friendly string with number of minutes
// converted to hours and minutes. // converted to hours and minutes.
func MinutesToFriendly(minutes int, lang translation.Locale) string { func MinutesToFriendly(minutes int, lang translation.Locale) string {
@ -208,43 +114,14 @@ func timeSincePro(then, now time.Time, lang translation.Locale) string {
return strings.TrimPrefix(timeStr, ", ") return strings.TrimPrefix(timeStr, ", ")
} }
func timeSince(then, now time.Time, lang translation.Locale) string { // TimeSince renders relative time HTML given a time.Time
return timeSinceUnix(then.Unix(), now.Unix(), lang)
}
func timeSinceUnix(then, now int64, lang translation.Locale) string {
lbl := "tool.ago"
diff := now - then
if then > now {
lbl = "tool.from_now"
diff = then - now
}
if diff <= 0 {
return lang.Tr("tool.now")
}
_, diffStr := computeTimeDiff(diff, lang)
return lang.Tr(lbl, diffStr)
}
// TimeSince calculates the time interval and generate user-friendly string.
func TimeSince(then time.Time, lang translation.Locale) template.HTML { func TimeSince(then time.Time, lang translation.Locale) template.HTML {
return htmlTimeSince(then, time.Now(), lang) timestamp := then.UTC().Format(time.RFC3339)
// declare data-tooltip-content attribute to switch from "title" tooltip to "tippy" tooltip
return template.HTML(fmt.Sprintf(`<relative-time class="time-since" prefix="%s" datetime="%s" data-tooltip-content data-tooltip-interactive="true">%s</relative-time>`, lang.Tr("on_date"), timestamp, timestamp))
} }
func htmlTimeSince(then, now time.Time, lang translation.Locale) template.HTML { // TimeSinceUnix renders relative time HTML given a TimeStamp
return template.HTML(fmt.Sprintf(`<span class="time-since" data-tooltip-content="%s" data-tooltip-interactive="true">%s</span>`,
then.In(setting.DefaultUILocation).Format(GetTimeFormat(lang.Language())),
timeSince(then, now, lang)))
}
// TimeSinceUnix calculates the time interval and generate user-friendly string.
func TimeSinceUnix(then TimeStamp, lang translation.Locale) template.HTML { func TimeSinceUnix(then TimeStamp, lang translation.Locale) template.HTML {
return htmlTimeSinceUnix(then, TimeStamp(time.Now().Unix()), lang) return TimeSince(then.AsLocalTime(), lang)
}
func htmlTimeSinceUnix(then, now TimeStamp, lang translation.Locale) template.HTML {
return template.HTML(fmt.Sprintf(`<span class="time-since" data-tooltip-content="%s" data-tooltip-interactive="true">%s</span>`,
then.FormatInLocation(GetTimeFormat(lang.Language()), setting.DefaultUILocation),
timeSinceUnix(int64(then), int64(now), lang)))
} }

@ -5,7 +5,6 @@ package timeutil
import ( import (
"context" "context"
"fmt"
"os" "os"
"testing" "testing"
"time" "time"
@ -40,46 +39,6 @@ func TestMain(m *testing.M) {
os.Exit(retVal) os.Exit(retVal)
} }
func TestTimeSince(t *testing.T) {
assert.Equal(t, "now", timeSince(BaseDate, BaseDate, translation.NewLocale("en-US")))
// test that each diff in `diffs` yields the expected string
test := func(expected string, diffs ...time.Duration) {
t.Run(expected, func(t *testing.T) {
for _, diff := range diffs {
actual := timeSince(BaseDate, BaseDate.Add(diff), translation.NewLocale("en-US"))
assert.Equal(t, translation.NewLocale("en-US").Tr("tool.ago", expected), actual)
actual = timeSince(BaseDate.Add(diff), BaseDate, translation.NewLocale("en-US"))
assert.Equal(t, translation.NewLocale("en-US").Tr("tool.from_now", expected), actual)
}
})
}
test("1 second", time.Second, time.Second+50*time.Millisecond)
test("2 seconds", 2*time.Second, 2*time.Second+50*time.Millisecond)
test("1 minute", time.Minute, time.Minute+29*time.Second)
test("2 minutes", 2*time.Minute, time.Minute+30*time.Second)
test("2 minutes", 2*time.Minute, 2*time.Minute+29*time.Second)
test("1 hour", time.Hour, time.Hour+29*time.Minute)
test("2 hours", 2*time.Hour, time.Hour+30*time.Minute)
test("2 hours", 2*time.Hour, 2*time.Hour+29*time.Minute)
test("3 hours", 3*time.Hour, 2*time.Hour+30*time.Minute)
test("1 day", DayDur, DayDur+11*time.Hour)
test("2 days", 2*DayDur, DayDur+12*time.Hour)
test("2 days", 2*DayDur, 2*DayDur+11*time.Hour)
test("3 days", 3*DayDur, 2*DayDur+12*time.Hour)
test("1 week", WeekDur, WeekDur+3*DayDur)
test("2 weeks", 2*WeekDur, WeekDur+4*DayDur)
test("2 weeks", 2*WeekDur, 2*WeekDur+3*DayDur)
test("3 weeks", 3*WeekDur, 2*WeekDur+4*DayDur)
test("1 month", MonthDur, MonthDur+14*DayDur)
test("2 months", 2*MonthDur, MonthDur+15*DayDur)
test("2 months", 2*MonthDur, 2*MonthDur+14*DayDur)
test("1 year", YearDur, YearDur+5*MonthDur)
test("2 years", 2*YearDur, YearDur+6*MonthDur)
test("2 years", 2*YearDur, 2*YearDur+5*MonthDur)
test("3 years", 3*YearDur, 2*YearDur+6*MonthDur)
}
func TestTimeSincePro(t *testing.T) { func TestTimeSincePro(t *testing.T) {
assert.Equal(t, "now", timeSincePro(BaseDate, BaseDate, translation.NewLocale("en-US"))) assert.Equal(t, "now", timeSincePro(BaseDate, BaseDate, translation.NewLocale("en-US")))
@ -113,60 +72,6 @@ func TestTimeSincePro(t *testing.T) {
12*time.Minute+17*time.Second) 12*time.Minute+17*time.Second)
} }
func TestHtmlTimeSince(t *testing.T) {
setting.TimeFormat = time.UnixDate
setting.DefaultUILocation = time.UTC
// test that `diff` yields a result containing `expected`
test := func(expected string, diff time.Duration) {
actual := htmlTimeSince(BaseDate, BaseDate.Add(diff), translation.NewLocale("en-US"))
assert.Contains(t, actual, `data-tooltip-content="Sat Jan 1 00:00:00 UTC 2000"`)
assert.Contains(t, actual, expected)
}
test("1 second", time.Second)
test("3 minutes", 3*time.Minute+5*time.Second)
test("1 day", DayDur+11*time.Hour)
test("1 week", WeekDur+3*DayDur)
test("3 months", 3*MonthDur+2*WeekDur)
test("2 years", 2*YearDur)
test("3 years", 2*YearDur+11*MonthDur+4*WeekDur)
}
func TestComputeTimeDiff(t *testing.T) {
// test that for each offset in offsets,
// computeTimeDiff(base + offset) == (offset, str)
test := func(base int64, str string, offsets ...int64) {
for _, offset := range offsets {
t.Run(fmt.Sprintf("%s:%d", str, offset), func(t *testing.T) {
diff, diffStr := computeTimeDiff(base+offset, translation.NewLocale("en-US"))
assert.Equal(t, offset, diff)
assert.Equal(t, str, diffStr)
})
}
}
test(0, "now", 0)
test(1, "1 second", 0)
test(2, "2 seconds", 0)
test(Minute, "1 minute", 0, 1, 29)
test(Minute, "2 minutes", 30, Minute-1)
test(2*Minute, "2 minutes", 0, 29)
test(2*Minute, "3 minutes", 30, Minute-1)
test(Hour, "1 hour", 0, 1, 29*Minute)
test(Hour, "2 hours", 30*Minute, Hour-1)
test(5*Hour, "5 hours", 0, 29*Minute)
test(Day, "1 day", 0, 1, 11*Hour)
test(Day, "2 days", 12*Hour, Day-1)
test(5*Day, "5 days", 0, 11*Hour)
test(Week, "1 week", 0, 1, 3*Day)
test(Week, "2 weeks", 4*Day, Week-1)
test(3*Week, "3 weeks", 0, 3*Day)
test(Month, "1 month", 0, 1)
test(Month, "2 months", 16*Day, Month-1)
test(10*Month, "10 months", 0, 13*Day)
test(Year, "1 year", 0, 179*Day)
test(Year, "2 years", 180*Day, Year-1)
test(3*Year, "3 years", 0, 179*Day)
}
func TestMinutesToFriendly(t *testing.T) { func TestMinutesToFriendly(t *testing.T) {
// test that a number of minutes yields the expected string // test that a number of minutes yields the expected string
test := func(expected string, minutes int) { test := func(expected string, minutes int) {

@ -64,9 +64,8 @@ func (ts TimeStamp) AsLocalTime() time.Time {
} }
// AsTimeInLocation convert timestamp as time.Time in Local locale // AsTimeInLocation convert timestamp as time.Time in Local locale
func (ts TimeStamp) AsTimeInLocation(loc *time.Location) (tm time.Time) { func (ts TimeStamp) AsTimeInLocation(loc *time.Location) time.Time {
tm = time.Unix(int64(ts), 0).In(loc) return time.Unix(int64(ts), 0).In(loc)
return tm
} }
// AsTimePtr convert timestamp as *time.Time in Local locale // AsTimePtr convert timestamp as *time.Time in Local locale

@ -112,6 +112,8 @@ never = Never
rss_feed = RSS Feed rss_feed = RSS Feed
on_date = on
[aria] [aria]
navbar = Navigation Bar navbar = Navigation Bar
footer = Footer footer = Footer
@ -3191,7 +3193,6 @@ details.documentation_site = Documentation Site
details.license = License details.license = License
assets = Assets assets = Assets
versions = Versions versions = Versions
versions.on = on
versions.view_all = View all versions.view_all = View all
dependency.id = ID dependency.id = ID
dependency.version = Version dependency.version = Version

6
package-lock.json generated

@ -13,6 +13,7 @@
"@citation-js/plugin-software-formats": "0.6.1", "@citation-js/plugin-software-formats": "0.6.1",
"@claviska/jquery-minicolors": "2.3.6", "@claviska/jquery-minicolors": "2.3.6",
"@github/markdown-toolbar-element": "2.1.1", "@github/markdown-toolbar-element": "2.1.1",
"@github/relative-time-element": "4.2.4",
"@github/text-expander-element": "2.3.0", "@github/text-expander-element": "2.3.0",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "18.3.0", "@primer/octicons": "18.3.0",
@ -851,6 +852,11 @@
"resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-2.1.1.tgz", "resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-2.1.1.tgz",
"integrity": "sha512-J++rpd5H9baztabJQB82h26jtueOeBRSTqetk9Cri+Lj/s28ndu6Tovn0uHQaOKtBWDobFunk9b5pP5vcqt7cA==" "integrity": "sha512-J++rpd5H9baztabJQB82h26jtueOeBRSTqetk9Cri+Lj/s28ndu6Tovn0uHQaOKtBWDobFunk9b5pP5vcqt7cA=="
}, },
"node_modules/@github/relative-time-element": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.2.4.tgz",
"integrity": "sha512-18qgH9FYUHYN9K3z4s35auDHww1dKTU6TacI8JkA5OuvHVa1lTMuSTZ4hIoJngD5+mizcoRMOs6p/yZYMIjsyg=="
},
"node_modules/@github/text-expander-element": { "node_modules/@github/text-expander-element": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.3.0.tgz", "resolved": "https://registry.npmjs.org/@github/text-expander-element/-/text-expander-element-2.3.0.tgz",

@ -13,6 +13,7 @@
"@citation-js/plugin-software-formats": "0.6.1", "@citation-js/plugin-software-formats": "0.6.1",
"@claviska/jquery-minicolors": "2.3.6", "@claviska/jquery-minicolors": "2.3.6",
"@github/markdown-toolbar-element": "2.1.1", "@github/markdown-toolbar-element": "2.1.1",
"@github/relative-time-element": "4.2.4",
"@github/text-expander-element": "2.3.0", "@github/text-expander-element": "2.3.0",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "18.3.0", "@primer/octicons": "18.3.0",

@ -18,8 +18,6 @@ import (
"code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/queue" "code.gitea.io/gitea/modules/queue"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/updatechecker" "code.gitea.io/gitea/modules/updatechecker"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/cron" "code.gitea.io/gitea/services/cron"
@ -34,7 +32,7 @@ const (
) )
var sysStatus struct { var sysStatus struct {
Uptime string StartTime string
NumGoroutine int NumGoroutine int
// General statistics. // General statistics.
@ -75,7 +73,7 @@ var sysStatus struct {
} }
func updateSystemStatus() { func updateSystemStatus() {
sysStatus.Uptime = timeutil.TimeSincePro(setting.AppStartTime, translation.NewLocale("en-US")) sysStatus.StartTime = setting.AppStartTime.Format(time.RFC3339)
m := new(runtime.MemStats) m := new(runtime.MemStats)
runtime.ReadMemStats(m) runtime.ReadMemStats(m)

@ -29,8 +29,8 @@
<td><a href="{{AppSubUrl}}/admin/auths/{{.ID}}">{{.Name}}</a></td> <td><a href="{{AppSubUrl}}/admin/auths/{{.ID}}">{{.Name}}</a></td>
<td>{{.TypeName}}</td> <td>{{.TypeName}}</td>
<td>{{if .IsActive}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td> <td>{{if .IsActive}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
<td><span data-tooltip-content="{{.UpdatedUnix.FormatShort}}"><time data-format="short-date" datetime="{{.UpdatedUnix.FormatLong}}">{{.UpdatedUnix.FormatShort}}</time></span></td> <td>{{template "shared/datetime/short" (dict "Datetime" .UpdatedUnix.FormatLong "Fallback" .UpdatedUnix.FormatShort)}}</td>
<td><span data-tooltip-content="{{.CreatedUnix.FormatLong}}"><time data-format="short-date" datetime="{{.CreatedUnix.FormatLong}}">{{.CreatedUnix.FormatShort}}</time></span></td> <td>{{template "shared/datetime/short" (dict "Datetime" .CreatedUnix.FormatLong "Fallback" .CreatedUnix.FormatShort)}}</td>
<td><a href="{{AppSubUrl}}/admin/auths/{{.ID}}">{{svg "octicon-pencil"}}</a></td> <td><a href="{{AppSubUrl}}/admin/auths/{{.ID}}">{{svg "octicon-pencil"}}</a></td>
</tr> </tr>
{{end}} {{end}}

@ -21,8 +21,8 @@
<td><button type="submit" class="ui green button" name="op" value="{{.Name}}" title="{{$.locale.Tr "admin.dashboard.operation_run"}}">{{svg "octicon-triangle-right"}}</button></td> <td><button type="submit" class="ui green button" name="op" value="{{.Name}}" title="{{$.locale.Tr "admin.dashboard.operation_run"}}">{{svg "octicon-triangle-right"}}</button></td>
<td>{{$.locale.Tr (printf "admin.dashboard.%s" .Name)}}</td> <td>{{$.locale.Tr (printf "admin.dashboard.%s" .Name)}}</td>
<td>{{.Spec}}</td> <td>{{.Spec}}</td>
<td>{{DateFmtLong .Next}}</td> <td>{{template "shared/datetime/full" (dict "Datetime" (DateFmtLong .Next) "Fallback" (DateFmtLong .Next) )}}</td>
<td>{{if gt .Prev.Year 1}}{{DateFmtLong .Prev}}{{else}}N/A{{end}}</td> <td>{{if gt .Prev.Year 1}}{{template "shared/datetime/full" (dict "Datetime" (DateFmtLong .Prev) "Fallback" (DateFmtLong .Prev) )}}{{else}}N/A{{end}}</td>
<td>{{.ExecTimes}}</td> <td>{{.ExecTimes}}</td>
<td {{if ne .Status ""}}data-tooltip-content="{{.FormatLastMessage $.locale}}"{{end}} >{{if eq .Status ""}}{{else if eq .Status "finished"}}{{svg "octicon-check" 16}}{{else}}{{svg "octicon-x" 16}}{{end}}</td> <td {{if ne .Status ""}}data-tooltip-content="{{.FormatLastMessage $.locale}}"{{end}} >{{if eq .Status ""}}{{else if eq .Status "finished"}}{{svg "octicon-check" 16}}{{else}}{{svg "octicon-x" 16}}{{end}}</td>
</tr> </tr>

@ -83,7 +83,7 @@
<div class="ui attached table segment"> <div class="ui attached table segment">
<dl class="dl-horizontal admin-dl-horizontal"> <dl class="dl-horizontal admin-dl-horizontal">
<dt>{{.locale.Tr "admin.dashboard.server_uptime"}}</dt> <dt>{{.locale.Tr "admin.dashboard.server_uptime"}}</dt>
<dd>{{.SysStatus.Uptime}}</dd> <dd><relative-time format="duration" datetime="{{.SysStatus.StartTime}}">{{.SysStatus.StartTime}}</relative-time></dd>
<dt>{{.locale.Tr "admin.dashboard.current_goroutine"}}</dt> <dt>{{.locale.Tr "admin.dashboard.current_goroutine"}}</dt>
<dd>{{.SysStatus.NumGoroutine}}</dd> <dd>{{.SysStatus.NumGoroutine}}</dd>
<div class="ui divider"></div> <div class="ui divider"></div>

@ -29,7 +29,7 @@
<td>{{.ID}}</td> <td>{{.ID}}</td>
<td>{{$.locale.Tr .TrStr}}</td> <td>{{$.locale.Tr .TrStr}}</td>
<td class="view-detail"><span class="notice-description text truncate">{{.Description}}</span></td> <td class="view-detail"><span class="notice-description text truncate">{{.Description}}</span></td>
<td><span class="notice-created-time" data-tooltip-content="{{.CreatedUnix.AsTime}}"><time data-format="short-date" datetime="{{.CreatedUnix.FormatLong}}">{{.CreatedUnix.FormatShort}}</time></span></td> <td>{{template "shared/datetime/short" (dict "Datetime" .CreatedUnix.FormatLong "Fallback" .CreatedUnix.FormatShort)}}</td>
<td><a href="#">{{svg "octicon-note" 16 "view-detail"}}</a></td> <td><a href="#">{{svg "octicon-note" 16 "view-detail"}}</a></td>
</tr> </tr>
{{end}} {{end}}

@ -44,7 +44,7 @@
<td>{{.NumTeams}}</td> <td>{{.NumTeams}}</td>
<td>{{.NumMembers}}</td> <td>{{.NumMembers}}</td>
<td>{{.NumRepos}}</td> <td>{{.NumRepos}}</td>
<td><span title="{{.CreatedUnix.FormatLong}}"><time data-format="short-date" datetime="{{.CreatedUnix.FormatLong}}">{{.CreatedUnix.FormatShort}}</time></span></td> <td>{{template "shared/datetime/short" (dict "Datetime" .CreatedUnix.FormatLong "Fallback" .CreatedUnix.FormatShort)}}</td>
<td><a href="{{.OrganisationLink}}/settings">{{svg "octicon-pencil"}}</a></td> <td><a href="{{.OrganisationLink}}/settings">{{svg "octicon-pencil"}}</a></td>
</tr> </tr>
{{end}} {{end}}

@ -68,7 +68,7 @@
{{end}} {{end}}
</td> </td>
<td>{{FileSize .CalculateBlobSize}}</td> <td>{{FileSize .CalculateBlobSize}}</td>
<td><span title="{{.Version.CreatedUnix.FormatLong}}"><time data-format="short-date" datetime="{{.Version.CreatedUnix.FormatLong}}">{{.Version.CreatedUnix.FormatShort}}</time></span></td> <td>{{template "shared/datetime/short" (dict "Datetime" .Version.CreatedUnix.FormatLong "Fallback" .Version.CreatedUnix.FormatShort)}}</td>
<td><a class="delete-button" href="" data-url="{{$.Link}}/delete?page={{$.Page.Paginater.Current}}&sort={{$.SortType}}" data-id="{{.Version.ID}}" data-name="{{.Package.Name}}" data-data-version="{{.Version.Version}}">{{svg "octicon-trash"}}</a></td> <td><a class="delete-button" href="" data-url="{{$.Link}}/delete?page={{$.Page.Paginater.Current}}&sort={{$.SortType}}" data-id="{{.Version.ID}}" data-name="{{.Package.Name}}" data-data-version="{{.Version.Version}}">{{svg "octicon-trash"}}</a></td>
</tr> </tr>
{{end}} {{end}}

@ -3,7 +3,7 @@
<div class="icon gt-ml-3 gt-mr-3">{{if eq .Process.Type "request"}}{{svg "octicon-globe" 16}}{{else if eq .Process.Type "system"}}{{svg "octicon-cpu" 16}}{{else}}{{svg "octicon-terminal" 16}}{{end}}</div> <div class="icon gt-ml-3 gt-mr-3">{{if eq .Process.Type "request"}}{{svg "octicon-globe" 16}}{{else if eq .Process.Type "system"}}{{svg "octicon-cpu" 16}}{{else}}{{svg "octicon-terminal" 16}}{{end}}</div>
<div class="content gt-f1"> <div class="content gt-f1">
<div class="header">{{.Process.Description}}</div> <div class="header">{{.Process.Description}}</div>
<div class="description"><span title="{{DateFmtLong .Process.Start}}">{{TimeSince .Process.Start .root.locale}}</span></div> <div class="description">{{TimeSince .Process.Start .root.locale}}</div>
</div> </div>
<div> <div>
{{if ne .Process.Type "system"}} {{if ne .Process.Type "system"}}

@ -158,8 +158,8 @@
{{range .Queue.Workers}} {{range .Queue.Workers}}
<tr> <tr>
<td>{{.Workers}}{{if .IsFlusher}}<span title="{{$.locale.Tr "admin.monitor.queue.flush"}}">{{svg "octicon-sync"}}</span>{{end}}</td> <td>{{.Workers}}{{if .IsFlusher}}<span title="{{$.locale.Tr "admin.monitor.queue.flush"}}">{{svg "octicon-sync"}}</span>{{end}}</td>
<td>{{DateFmtLong .Start}}</td> <td>{{template "shared/datetime/full" (dict "Datetime" (DateFmtLong .Start) "Fallback" (DateFmtLong .Start) )}}</td>
<td>{{if .HasTimeout}}{{DateFmtLong .Timeout}}{{else}}-{{end}}</td> <td>{{if .HasTimeout}}{{template "shared/datetime/full" (dict "Datetime" (DateFmtLong .Timeout) "Fallback" (DateFmtLong .Timeout) )}}{{else}}-{{end}}</td>
<td> <td>
<a class="delete-button" href="" data-url="{{$.Link}}/cancel/{{.PID}}" data-id="{{.PID}}" data-name="{{.Workers}}" title="{{$.locale.Tr "remove"}}">{{svg "octicon-trash"}}</a> <a class="delete-button" href="" data-url="{{$.Link}}/cancel/{{.PID}}" data-id="{{.PID}}" data-name="{{.Workers}}" title="{{$.locale.Tr "remove"}}">{{svg "octicon-trash"}}</a>
</td> </td>

@ -83,7 +83,7 @@
<td>{{.NumForks}}</td> <td>{{.NumForks}}</td>
<td>{{.NumIssues}}</td> <td>{{.NumIssues}}</td>
<td>{{FileSize .Size}}</td> <td>{{FileSize .Size}}</td>
<td><span title="{{.CreatedUnix.FormatLong}}"><time data-format="short-date" datetime="{{.CreatedUnix.FormatLong}}">{{.CreatedUnix.FormatShort}}</time></span></td> <td>{{template "shared/datetime/short" (dict "Datetime" .CreatedUnix.FormatLong "Fallback" .CreatedUnix.FormatShort)}}</td>
<td><a class="delete-button" href="" data-url="{{$.Link}}/delete?page={{$.Page.Paginater.Current}}&sort={{$.SortType}}" data-id="{{.ID}}" data-name="{{.Name}}">{{svg "octicon-trash"}}</a></td> <td><a class="delete-button" href="" data-url="{{$.Link}}/delete?page={{$.Page.Paginater.Current}}&sort={{$.SortType}}" data-id="{{.ID}}" data-name="{{.Name}}">{{svg "octicon-trash"}}</a></td>
</tr> </tr>
{{end}} {{end}}

@ -13,7 +13,7 @@
</div> </div>
<div class="content gt-f1"> <div class="content gt-f1">
<div class="header">{{.Process.Description}}</div> <div class="header">{{.Process.Description}}</div>
<div class="description">{{if ne .Process.Type "none"}}<span title="{{DateFmtLong .Process.Start}}">{{TimeSince .Process.Start .root.locale}}</span>{{end}}</div> <div class="description">{{if ne .Process.Type "none"}}{{TimeSince .Process.Start .root.locale}}{{end}}</div>
</div> </div>
<div> <div>
{{if or (eq .Process.Type "request") (eq .Process.Type "normal")}} {{if or (eq .Process.Type "request") (eq .Process.Type "normal")}}

@ -94,9 +94,9 @@
<td>{{if .IsRestricted}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td> <td>{{if .IsRestricted}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
<td>{{if index $.UsersTwoFaStatus .ID}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td> <td>{{if index $.UsersTwoFaStatus .ID}}{{svg "octicon-check"}}{{else}}{{svg "octicon-x"}}{{end}}</td>
<td>{{.NumRepos}}</td> <td>{{.NumRepos}}</td>
<td><span title="{{.CreatedUnix.FormatLong}}"><time data-format="short-date" datetime="{{.CreatedUnix.FormatLong}}">{{.CreatedUnix.FormatShort}}</time></span></td> <td>{{template "shared/datetime/short" (dict "Datetime" .CreatedUnix.FormatLong "Fallback" .CreatedUnix.FormatShort)}}</td>
{{if .LastLoginUnix}} {{if .LastLoginUnix}}
<td><span title="{{.LastLoginUnix.FormatLong}}"><time data-format="short-date" datetime="{{.LastLoginUnix.FormatLong}}">{{.LastLoginUnix.FormatShort}}</time></span></td> <td>{{template "shared/datetime/short" (dict "Datetime" .LastLoginUnix.FormatLong "Fallback" .LastLoginUnix.FormatShort)}}</td>
{{else}} {{else}}
<td><span>{{$.locale.Tr "admin.users.never_login"}}</span></td> <td><span>{{$.locale.Tr "admin.users.never_login"}}</span></td>
{{end}} {{end}}

@ -23,7 +23,7 @@
{{svg "octicon-link"}} {{svg "octicon-link"}}
<a href="{{.Website}}" rel="nofollow">{{.Website}}</a> <a href="{{.Website}}" rel="nofollow">{{.Website}}</a>
{{end}} {{end}}
{{svg "octicon-clock"}} {{$.locale.Tr "user.join_on"}} <time data-format="short-date" datetime="{{.CreatedUnix.FormatLong}}">{{.CreatedUnix.FormatShort}}</time> {{svg "octicon-clock"}} {{$.locale.Tr "user.join_on"}} {{template "shared/datetime/short" (dict "Datetime" .CreatedUnix.FormatLong "Fallback" .CreatedUnix.FormatShort)}}
</div> </div>
</div> </div>
</div> </div>

@ -18,7 +18,7 @@
{{svg "octicon-mail"}} {{svg "octicon-mail"}}
<a href="mailto:{{.Email}}" rel="nofollow">{{.Email}}</a> <a href="mailto:{{.Email}}" rel="nofollow">{{.Email}}</a>
{{end}} {{end}}
{{svg "octicon-clock"}} {{$.locale.Tr "user.join_on"}} <time data-format="short-date" datetime="{{.CreatedUnix.FormatLong}}">{{.CreatedUnix.FormatShort}}</time> {{svg "octicon-clock"}} {{$.locale.Tr "user.join_on"}} {{template "shared/datetime/short" (dict "Datetime" .CreatedUnix.FormatLong "Fallback" .CreatedUnix.FormatShort)}}
</div> </div>
</div> </div>
</div> </div>

@ -22,7 +22,7 @@
<td><a href="{{.FullWebLink}}">{{.Version.Version}}</a></td> <td><a href="{{.FullWebLink}}">{{.Version.Version}}</a></td>
<td><a href="{{.Creator.HomeLink}}">{{.Creator.Name}}</a></td> <td><a href="{{.Creator.HomeLink}}">{{.Creator.Name}}</a></td>
<td>{{FileSize .CalculateBlobSize}}</td> <td>{{FileSize .CalculateBlobSize}}</td>
<td><span title="{{.Version.CreatedUnix.FormatLong}}"><time data-format="short-date" datetime="{{.Version.CreatedUnix.FormatLong}}">{{.Version.CreatedUnix.FormatShort}}</time></span></td> <td>{{template "shared/datetime/short" (dict "Datetime" .Version.CreatedUnix.FormatLong "Fallback" .Version.CreatedUnix.FormatShort)}}</td>
</tr> </tr>
{{else}} {{else}}
<tr> <tr>

@ -86,7 +86,7 @@
{{range .LatestVersions}} {{range .LatestVersions}}
<div class="item"> <div class="item">
<a href="{{$.PackageDescriptor.PackageWebLink}}/{{PathEscape .LowerVersion}}">{{.Version}}</a> <a href="{{$.PackageDescriptor.PackageWebLink}}/{{PathEscape .LowerVersion}}">{{.Version}}</a>
<span class="text small">{{$.locale.Tr "packages.versions.on"}} {{.CreatedUnix.FormatDate}}</span> <span class="text small">{{$.locale.Tr "on_date"}} {{.CreatedUnix.FormatDate}}</span>
</div> </div>
{{end}} {{end}}
</div> </div>

@ -2,7 +2,7 @@
<div role="main" aria-label="{{.Title}}" class="page-content repository commits"> <div role="main" aria-label="{{.Title}}" class="page-content repository commits">
{{template "repo/header" .}} {{template "repo/header" .}}
<div class="ui container"> <div class="ui container">
<h2 class="ui header"><time data-format="date" datetime="{{.DateFrom}}">{{.DateFrom}}</time> - <time data-format="date" datetime="{{.DateUntil}}">{{.DateUntil}}</time> <h2 class="ui header">{{template "shared/datetime/long" (dict "Datetime" .DateFrom "Fallback" .DateFrom)}} - {{template "shared/datetime/long" (dict "Datetime" .DateUntil "Fallback" .DateUntil)}}
<div class="ui right"> <div class="ui right">
<!-- Period --> <!-- Period -->
<div class="ui floating dropdown jump filter"> <div class="ui floating dropdown jump filter">

@ -385,7 +385,7 @@
<div class="gt-df gt-sb gt-ac"> <div class="gt-df gt-sb gt-ac">
<div class="due-date {{if .Issue.IsOverdue}}text red{{end}}" {{if .Issue.IsOverdue}}data-tooltip-content="{{.locale.Tr "repo.issues.due_date_overdue"}}"{{end}}> <div class="due-date {{if .Issue.IsOverdue}}text red{{end}}" {{if .Issue.IsOverdue}}data-tooltip-content="{{.locale.Tr "repo.issues.due_date_overdue"}}"{{end}}>
{{svg "octicon-calendar" 16 "gt-mr-3"}} {{svg "octicon-calendar" 16 "gt-mr-3"}}
<time data-format="date" datetime="{{.Issue.DeadlineUnix.FormatDate}}">{{.Issue.DeadlineUnix.FormatDate}}</time> {{template "shared/datetime/long" (dict "Datetime" .Issue.DeadlineUnix.FormatDate "Fallback" .Issue.DeadlineUnix.FormatDate)}}
</div> </div>
<div> <div>
{{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}} {{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}

@ -64,7 +64,7 @@
{{.Fingerprint}} {{.Fingerprint}}
</div> </div>
<div class="activity meta"> <div class="activity meta">
<i>{{$.locale.Tr "settings.add_on"}} <span><time data-format="short-date" datetime="{{.CreatedUnix.FormatLong}}">{{.CreatedUnix.FormatShort}}</time></span> — {{svg "octicon-info"}} {{if .HasUsed}}{{$.locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}><time data-format="short-date" datetime="{{.UpdatedUnix.FormatLong}}">{{.UpdatedUnix.FormatShort}}</time></span>{{else}}{{$.locale.Tr "settings.no_activity"}}{{end}} - <span>{{$.locale.Tr "settings.can_read_info"}}{{if not .IsReadOnly}} / {{$.locale.Tr "settings.can_write_info"}} {{end}}</span></i> <i>{{$.locale.Tr "settings.add_on"}} <span>{{template "shared/datetime/short" (dict "Datetime" .CreatedUnix.FormatLong "Fallback" .CreatedUnix.FormatShort)}}</span> — {{svg "octicon-info"}} {{if .HasUsed}}{{$.locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}>{{template "shared/datetime/short" (dict "Datetime" .UpdatedUnix.FormatLong "Fallback" .UpdatedUnix.FormatShort)}}</span>{{else}}{{$.locale.Tr "settings.no_activity"}}{{end}} - <span>{{$.locale.Tr "settings.can_read_info"}}{{if not .IsReadOnly}} / {{$.locale.Tr "settings.can_write_info"}} {{end}}</span></i>
</div> </div>
</div> </div>
</div> </div>

@ -93,7 +93,7 @@
<tr> <tr>
<td>{{(MirrorRemoteAddress $.Context .Repository .Mirror.GetRemoteName false).Address}}</td> <td>{{(MirrorRemoteAddress $.Context .Repository .Mirror.GetRemoteName false).Address}}</td>
<td>{{$.locale.Tr "repo.settings.mirror_settings.direction.pull"}}</td> <td>{{$.locale.Tr "repo.settings.mirror_settings.direction.pull"}}</td>
<td><time data-format="date-time" datetime="{{.Mirror.UpdatedUnix.FormatLong}}">{{.Mirror.UpdatedUnix.AsTime}}</time></td> <td>{{template "shared/datetime/full" (dict "Datetime" .Mirror.UpdatedUnix.FormatLong "Fallback" .Mirror.UpdatedUnix.AsTime)}}</td>
<td class="right aligned"> <td class="right aligned">
<form method="post" style="display: inline-block"> <form method="post" style="display: inline-block">
{{.CsrfTokenHtml}} {{.CsrfTokenHtml}}
@ -171,7 +171,7 @@
{{$address := MirrorRemoteAddress $.Context $.Repository .GetRemoteName true}} {{$address := MirrorRemoteAddress $.Context $.Repository .GetRemoteName true}}
<td>{{$address.Address}}</td> <td>{{$address.Address}}</td>
<td>{{$.locale.Tr "repo.settings.mirror_settings.direction.push"}}</td> <td>{{$.locale.Tr "repo.settings.mirror_settings.direction.push"}}</td>
<td>{{if .LastUpdateUnix}}<time data-format="date-time" datetime="{{.LastUpdateUnix.FormatLong}}">{{.LastUpdateUnix.AsTime}}</time>{{else}}{{$.locale.Tr "never"}}{{end}} {{if .LastError}}<div class="ui red label" data-tooltip-content="{{.LastError}}">{{$.locale.Tr "error"}}</div>{{end}}</td> <td>{{if .LastUpdateUnix}}{{template "shared/datetime/full" (dict "Datetime" .LastUpdateUnix.FormatLong "Fallback" .LastUpdateUnix.AsTime)}}{{else}}{{$.locale.Tr "never"}}{{end}} {{if .LastError}}<div class="ui red label" data-tooltip-content="{{.LastError}}">{{$.locale.Tr "error"}}</div>{{end}}</td>
<td class="right aligned"> <td class="right aligned">
<form method="post" style="display: inline-block"> <form method="post" style="display: inline-block">
{{$.CsrfTokenHtml}} {{$.CsrfTokenHtml}}

@ -18,7 +18,7 @@
{{else if .Location}} {{else if .Location}}
{{svg "octicon-location"}} {{.Location}} {{svg "octicon-location"}} {{.Location}}
{{else}} {{else}}
{{svg "octicon-clock"}} {{$.locale.Tr "user.join_on"}} <time data-format="short-date" datetime="{{.CreatedUnix.FormatLong}}">{{.CreatedUnix.FormatShort}}</time> {{svg "octicon-clock"}} {{$.locale.Tr "user.join_on"}} {{template "shared/datetime/short" (dict "Datetime" .CreatedUnix.FormatLong "Fallback" .CreatedUnix.FormatShort)}}
{{end}} {{end}}
</div> </div>
</li> </li>

@ -0,0 +1 @@
<relative-time format="datetime" weekday="" year="numeric" month="short" day="numeric" hour="numeric" minute="numeric" second="numeric" datetime="{{.Datetime}}">{{.Fallback}}</relative-time>

@ -0,0 +1 @@
<relative-time format="datetime" year="numeric" month="long" day="numeric" weekday="" datetime="{{.Datetime}}">{{.Fallback}}</relative-time>

@ -0,0 +1 @@
<relative-time format="datetime" year="numeric" month="short" day="numeric" weekday="" datetime="{{.Datetime}}">{{.Fallback}}</relative-time>

@ -106,7 +106,7 @@
<span class="due-date" data-tooltip-content="{{$.locale.Tr "repo.issues.due_date"}}"> <span class="due-date" data-tooltip-content="{{$.locale.Tr "repo.issues.due_date"}}">
<span{{if .IsOverdue}} class="overdue"{{end}}> <span{{if .IsOverdue}} class="overdue"{{end}}>
{{svg "octicon-calendar" 14 "gt-mr-2"}} {{svg "octicon-calendar" 14 "gt-mr-2"}}
<time data-format="short-date" datetime="{{.DeadlineUnix.FormatDate}}">{{.DeadlineUnix.FormatShort}}</time> {{template "shared/datetime/short" (dict "Datetime" .DeadlineUnix.FormatDate "Fallback" .DeadlineUnix.FormatShort)}}
</span> </span>
</span> </span>
{{end}} {{end}}

@ -73,7 +73,7 @@
</li> </li>
{{end}} {{end}}
{{end}} {{end}}
<li>{{svg "octicon-clock"}} {{.locale.Tr "user.join_on"}} <time data-format="short-date" datetime="{{.Owner.CreatedUnix.FormatLong}}">{{.Owner.CreatedUnix.FormatShort}}</time></li> <li>{{svg "octicon-clock"}} {{.locale.Tr "user.join_on"}} {{template "shared/datetime/short" (dict "Datetime" .Owner.CreatedUnix.FormatLong "Fallback" .Owner.CreatedUnix.FormatShort)}}</li>
{{if and .Orgs .HasOrgsVisible}} {{if and .Orgs .HasOrgsVisible}}
<li> <li>
<ul class="user-orgs"> <ul class="user-orgs">

@ -30,7 +30,7 @@
</ul> </ul>
</details> </details>
<div class="activity meta"> <div class="activity meta">
<i>{{$.locale.Tr "settings.add_on"}} <span><time data-format="short-date" datetime="{{.CreatedUnix.FormatLong}}">{{.CreatedUnix.FormatShort}}</time></span> — {{svg "octicon-info"}} {{if .HasUsed}}{{$.locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}><time data-format="short-date" datetime="{{.UpdatedUnix.FormatLong}}">{{.UpdatedUnix.FormatShort}}</time></span>{{else}}{{$.locale.Tr "settings.no_activity"}}{{end}}</i> <i>{{$.locale.Tr "settings.add_on"}} <span>{{template "shared/datetime/short" (dict "Datetime" .CreatedUnix.FormatLong "Fallback" .CreatedUnix.FormatShort)}}</span> — {{svg "octicon-info"}} {{if .HasUsed}}{{$.locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}>{{template "shared/datetime/short" (dict "Datetime" .UpdatedUnix.FormatLong "Fallback" .UpdatedUnix.FormatShort)}}</span>{{else}}{{$.locale.Tr "settings.no_activity"}}{{end}}</i>
</div> </div>
</div> </div>
</div> </div>

@ -20,7 +20,7 @@
<div class="content"> <div class="content">
<strong>{{$grant.Application.Name}}</strong> <strong>{{$grant.Application.Name}}</strong>
<div class="activity meta"> <div class="activity meta">
<i>{{$.locale.Tr "settings.add_on"}} <span><time data-format="short-date" datetime="{{$grant.CreatedUnix.FormatLong}}">{{$grant.CreatedUnix.FormatShort}}</time></span></i> <i>{{$.locale.Tr "settings.add_on"}} <span>{{template "shared/datetime/short" (dict "Datetime" $grant.CreatedUnix.FormatLong "Fallback" $grant.CreatedUnix.FormatShort)}}</span></i>
</div> </div>
</div> </div>
</div> </div>

@ -68,9 +68,9 @@
<b>{{$.locale.Tr "settings.subkeys"}}:</b> {{range .SubsKey}} {{.PaddedKeyID}} {{end}} <b>{{$.locale.Tr "settings.subkeys"}}:</b> {{range .SubsKey}} {{.PaddedKeyID}} {{end}}
</div> </div>
<div class="activity meta"> <div class="activity meta">
<i>{{$.locale.Tr "settings.add_on"}} <span><time data-format="short-date" datetime="{{.AddedUnix.FormatLong}}">{{.AddedUnix.FormatShort}}</time></span></i> <i>{{$.locale.Tr "settings.add_on"}} <span>{{template "shared/datetime/short" (dict "Datetime" .AddedUnix.FormatLong "Fallback" .AddedUnix.FormatShort)}}</span></i>
- -
<i>{{if not .ExpiredUnix.IsZero}}{{$.locale.Tr "settings.valid_until"}} <span><time data-format="short-date" datetime="{{.ExpiredUnix.FormatLong}}">{{.ExpiredUnix.FormatShort}}</time></span>{{else}}{{$.locale.Tr "settings.valid_forever"}}{{end}}</i> <i>{{if not .ExpiredUnix.IsZero}}{{$.locale.Tr "settings.valid_until"}} <span>{{template "shared/datetime/short" (dict "Datetime" .ExpiredUnix.FormatLong "Fallback" .ExpiredUnix.FormatShort)}}</span>{{else}}{{$.locale.Tr "settings.valid_forever"}}{{end}}</i>
</div> </div>
</div> </div>
</div> </div>

@ -25,7 +25,7 @@
<div class="content"> <div class="content">
<strong>{{.Name}}</strong> <strong>{{.Name}}</strong>
<div class="activity meta"> <div class="activity meta">
<i>{{$.locale.Tr "settings.add_on"}} <span><time data-format="short-date" datetime="{{.CreatedUnix.FormatLong}}">{{.CreatedUnix.FormatShort}}</time></span> — {{svg "octicon-info" 16}} {{if .HasUsed}}{{$.locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}><time data-format="short-date" datetime="{{.UpdatedUnix.FormatLong}}">{{.UpdatedUnix.FormatShort}}</time></span>{{else}}{{$.locale.Tr "settings.no_activity"}}{{end}}</i> <i>{{$.locale.Tr "settings.add_on"}} <span>{{template "shared/datetime/short" (dict "Datetime" .CreatedUnix.FormatLong "Fallback" .CreatedUnix.FormatShort)}}</span> — {{svg "octicon-info" 16}} {{if .HasUsed}}{{$.locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}>{{template "shared/datetime/short" (dict "Datetime" .UpdatedUnix.FormatLong "Fallback" .UpdatedUnix.FormatShort)}}</span>{{else}}{{$.locale.Tr "settings.no_activity"}}{{end}}</i>
</div> </div>
</div> </div>
</div> </div>

@ -59,7 +59,7 @@
{{.Fingerprint}} {{.Fingerprint}}
</div> </div>
<div class="activity meta"> <div class="activity meta">
<i>{{$.locale.Tr "settings.add_on"}} <span><time data-format="short-date" datetime="{{.CreatedUnix.FormatLong}}">{{.CreatedUnix.FormatShort}}</time></span> — {{svg "octicon-info"}} {{if .HasUsed}}{{$.locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}><time data-format="short-date" datetime="{{.UpdatedUnix.FormatLong}}">{{.UpdatedUnix.FormatShort}}</time></span>{{else}}{{$.locale.Tr "settings.no_activity"}}{{end}}</i> <i>{{$.locale.Tr "settings.add_on"}} <span>{{template "shared/datetime/short" (dict "Datetime" .CreatedUnix.FormatLong "Fallback" .CreatedUnix.FormatShort)}}</span> — {{svg "octicon-info"}} {{if .HasUsed}}{{$.locale.Tr "settings.last_used"}} <span {{if .HasRecentActivity}}class="green"{{end}}>{{template "shared/datetime/short" (dict "Datetime" .UpdatedUnix.FormatLong "Fallback" .UpdatedUnix.FormatShort)}}</span>{{else}}{{$.locale.Tr "settings.no_activity"}}{{end}}</i>
</div> </div>
</div> </div>
</div> </div>

@ -75,7 +75,10 @@ func testViewRepo(t *testing.T) {
} }
}) })
f.commitTime, _ = s.Find("span.time-since").Attr("data-tooltip-content") // convert "2017-06-14 21:54:21 +0800" to "Wed, 14 Jun 2017 13:54:21 UTC"
htmlTimeString, _ := s.Find("relative-time.time-since").Attr("datetime")
htmlTime, _ := time.Parse(time.RFC3339, htmlTimeString)
f.commitTime = htmlTime.UTC().Format("Mon, 02 Jan 2006 15:04:05 UTC")
items = append(items, f) items = append(items, f)
}) })

@ -178,7 +178,7 @@ export function initAdminCommon() {
// Attach view detail modals // Attach view detail modals
$('.view-detail').on('click', function () { $('.view-detail').on('click', function () {
$detailModal.find('.content pre').text($(this).parents('tr').find('.notice-description').text()); $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'); $detailModal.modal('show');
return false; 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 {initCommonOrganization} from './features/common-organization.js';
import {initRepoWikiForm} from './features/repo-wiki.js'; import {initRepoWikiForm} from './features/repo-wiki.js';
import {initRepoCommentForm, initRepository} from './features/repo-legacy.js'; import {initRepoCommentForm, initRepository} from './features/repo-legacy.js';
import {initFormattingReplacements} from './features/formatting.js';
import {initCopyContent} from './features/copycontent.js'; import {initCopyContent} from './features/copycontent.js';
import {initCaptcha} from './features/captcha.js'; import {initCaptcha} from './features/captcha.js';
import {initRepositoryActionView} from './components/RepoActionView.vue'; import {initRepositoryActionView} from './components/RepoActionView.vue';
@ -83,10 +82,6 @@ import {initGiteaFomantic} from './modules/fomantic.js';
import {onDomReady} from './utils/dom.js'; import {onDomReady} from './utils/dom.js';
import {initRepoIssueList} from './features/repo-issue-list.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 // Init Gitea's Fomantic settings
initGiteaFomantic(); initGiteaFomantic();

@ -6,7 +6,7 @@ export function createTippy(target, opts = {}) {
animation: false, animation: false,
allowHTML: false, allowHTML: false,
hideOnClick: false, hideOnClick: false,
interactiveBorder: 30, interactiveBorder: 20,
ignoreAttributes: true, ignoreAttributes: true,
maxWidth: 500, // increase over default 350px 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>`, 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} * @returns {null|tippy}
*/ */
function attachTooltip(target, content = null) { function attachTooltip(target, content = null) {
switchTitleToTooltip(target);
content = content ?? target.getAttribute('data-tooltip-content'); content = content ?? target.getAttribute('data-tooltip-content');
if (!content) return null; if (!content) return null;
@ -55,6 +57,18 @@ function attachTooltip(target, content = null) {
return target._tippy; 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 * 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 * 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); attachTooltip(this);
} }
/** // Activate the tooltip for current element.
* Activate the tooltip for all children elements // If the element has no aria-label, use the tooltip content as aria-label.
* And if the element has no aria-label, use the tooltip content as aria-label function attachLazyTooltip(el) {
* @param target {HTMLElement} el.addEventListener('mouseover', lazyTooltipOnMouseHover, {capture: true});
*/
function attachChildrenLazyTooltip(target) {
for (const el of target.querySelectorAll('[data-tooltip-content]')) {
el.addEventListener('mouseover', lazyTooltipOnMouseHover, true);
// meanwhile, if the element has no aria-label, use the tooltip content as aria-label // meanwhile, if the element has no aria-label, use the tooltip content as aria-label
if (!el.hasAttribute('aria-label')) { if (!el.hasAttribute('aria-label')) {
const content = target.getAttribute('data-tooltip-content'); const content = el.getAttribute('data-tooltip-content');
if (content) { if (content) {
el.setAttribute('aria-label', 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() { export function initGlobalTooltips() {
// use MutationObserver to detect new elements added to the DOM, or attributes changed // use MutationObserver to detect new "data-tooltip-content" elements added to the DOM, or attributes changed
const observer = new MutationObserver((mutationList) => { const observerConnect = (observer) => observer.observe(document, {
for (const mutation of mutationList) { 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') { if (mutation.type === 'childList') {
// mainly for Vue components and AJAX rendered elements // mainly for Vue components and AJAX rendered elements
for (const el of mutation.addedNodes) { for (const el of mutation.addedNodes) {
// handle all "tooltip" elements in added nodes which have 'querySelectorAll' method, skip non-related nodes (eg: "#text") if (elementNodeTypes.has(el.nodeType)) {
if ('querySelectorAll' in el) {
attachChildrenLazyTooltip(el); attachChildrenLazyTooltip(el);
if (el.hasAttribute('data-tooltip-content')) {
attachLazyTooltip(el);
}
} }
} }
} else if (mutation.type === 'attributes') { } else if (mutation.type === 'attributes') {
// sync the tooltip content if the attributes change
attachTooltip(mutation.target); attachTooltip(mutation.target);
} }
} }
observerConnect(observer);
}); });
observer.observe(document, { observerConnect(observer);
subtree: true,
childList: true,
attributeFilter: ['data-tooltip-content'],
});
attachChildrenLazyTooltip(document.documentElement); 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, so they should have their own dependencies and should be very light,
then they won't affect the page loading time too much. 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. * 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 '@webcomponents/custom-elements'; // polyfill for some browsers like Pale Moon
import '@github/relative-time-element';
import './GiteaLocaleNumber.js'; import './GiteaLocaleNumber.js';
import './GiteaOriginUrl.js'; import './GiteaOriginUrl.js';

Loading…
Cancel
Save