diff --git a/templates/admin/badge/edit.tmpl b/templates/admin/badge/edit.tmpl
index 0d2e1c16dde..52d394864d9 100644
--- a/templates/admin/badge/edit.tmpl
+++ b/templates/admin/badge/edit.tmpl
@@ -28,7 +28,7 @@
-
+
+
{{template "admin/layout_footer" .}}
diff --git a/templates/admin/emails/list.tmpl b/templates/admin/emails/list.tmpl
index 135b4da3a01..6cd82bd903d 100644
--- a/templates/admin/emails/list.tmpl
+++ b/templates/admin/emails/list.tmpl
@@ -76,7 +76,7 @@
{{template "base/paginate" .}}
-
+
+
{{template "admin/layout_footer" .}}
diff --git a/templates/admin/notice.tmpl b/templates/admin/notice.tmpl
index 98ab7310049..df9b79f8a77 100644
--- a/templates/admin/notice.tmpl
+++ b/templates/admin/notice.tmpl
@@ -61,9 +61,9 @@
{{template "base/paginate" .}}
-
+
{{template "admin/layout_footer" .}}
diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl
index 41fba825f86..442b82749f8 100644
--- a/templates/admin/packages/list.tmpl
+++ b/templates/admin/packages/list.tmpl
@@ -83,12 +83,14 @@
{{template "base/paginate" .}}
-
+
+
{{template "admin/layout_footer" .}}
diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl
index 981fe48a408..2e3adb2e8a2 100644
--- a/templates/admin/repo/list.tmpl
+++ b/templates/admin/repo/list.tmpl
@@ -96,14 +96,16 @@
{{template "base/paginate" .}}
-
+
+
{{template "admin/layout_footer" .}}
diff --git a/templates/admin/repo/unadopted.tmpl b/templates/admin/repo/unadopted.tmpl
index c3ea0ad98a5..b6e85c7432a 100644
--- a/templates/admin/repo/unadopted.tmpl
+++ b/templates/admin/repo/unadopted.tmpl
@@ -24,7 +24,7 @@
{{svg "octicon-file-directory-fill"}} {{$dir}}
-
+
+
-
+
+
{{end}}
diff --git a/templates/admin/user/edit.tmpl b/templates/admin/user/edit.tmpl
index 9dd9f3ff7c1..6a9883dee41 100644
--- a/templates/admin/user/edit.tmpl
+++ b/templates/admin/user/edit.tmpl
@@ -223,7 +223,7 @@
-
+
+
{{template "admin/layout_footer" .}}
diff --git a/templates/admin/user/view_orgs.tmpl b/templates/admin/user/view_orgs.tmpl
index d4554555c9d..468957db15f 100644
--- a/templates/admin/user/view_orgs.tmpl
+++ b/templates/admin/user/view_orgs.tmpl
@@ -5,18 +5,20 @@
{{ctx.Locale.Tr "settings.organization"}} ({{ctx.Locale.Tr "admin.total" $userOrgsTotal}})
{{if $userOrgs}}
-
+
+
{{end}}
diff --git a/templates/base/modal_actions_confirm.tmpl b/templates/base/modal_actions_confirm.tmpl
index dcf6fe74f2a..af3dcb86e45 100644
--- a/templates/base/modal_actions_confirm.tmpl
+++ b/templates/base/modal_actions_confirm.tmpl
@@ -6,7 +6,7 @@
Single danger button (GitHub-like):
* ModalButtonDangerText "This action will destroy your data"
-The ".ok.button" and ".cancel.button" selectors are also used by Fomantic Modal internally
+The ".ok.button" and ".cancel.button" selectors are also used by "modules/modal.ts"
*/}}
{{if .ModalButtonDangerText}}
diff --git a/templates/devtest/fomantic-modal.tmpl b/templates/devtest/modal.tmpl
similarity index 83%
rename from templates/devtest/fomantic-modal.tmpl
rename to templates/devtest/modal.tmpl
index bed6251787e..2ad70ace90e 100644
--- a/templates/devtest/fomantic-modal.tmpl
+++ b/templates/devtest/modal.tmpl
@@ -1,15 +1,15 @@
{{template "devtest/devtest-header"}}
-
+
-
+
-
+
-
{{template "devtest/devtest-footer"}}
diff --git a/templates/org/settings/options_dangerzone.tmpl b/templates/org/settings/options_dangerzone.tmpl
index e28a10c9282..f70d2fac700 100644
--- a/templates/org/settings/options_dangerzone.tmpl
+++ b/templates/org/settings/options_dangerzone.tmpl
@@ -35,7 +35,7 @@
-
+
-
+
-
+
-
+
-
+
-
+
diff --git a/templates/org/team/members.tmpl b/templates/org/team/members.tmpl
index 78af9086e57..6a783ed0d5e 100644
--- a/templates/org/team/members.tmpl
+++ b/templates/org/team/members.tmpl
@@ -72,13 +72,15 @@
-
+
+
{{template "base/footer" .}}
diff --git a/templates/org/team/new.tmpl b/templates/org/team/new.tmpl
index b34a12005b0..06b638ac28e 100644
--- a/templates/org/team/new.tmpl
+++ b/templates/org/team/new.tmpl
@@ -194,7 +194,7 @@
-
+
+
{{template "base/footer" .}}
diff --git a/templates/org/team/sidebar.tmpl b/templates/org/team/sidebar.tmpl
index 97662e23b1e..8515ec3df12 100644
--- a/templates/org/team/sidebar.tmpl
+++ b/templates/org/team/sidebar.tmpl
@@ -91,7 +91,7 @@
{{end}}
-
+
+
diff --git a/templates/org/team/teams.tmpl b/templates/org/team/teams.tmpl
index 62ee9a51e3c..524317595b7 100644
--- a/templates/org/team/teams.tmpl
+++ b/templates/org/team/teams.tmpl
@@ -63,7 +63,7 @@
{{template "base/paginate" .}}
-
+
+
{{template "base/footer" .}}
diff --git a/templates/package/settings.tmpl b/templates/package/settings.tmpl
index 271fb683118..2cd62686082 100644
--- a/templates/package/settings.tmpl
+++ b/templates/package/settings.tmpl
@@ -55,7 +55,7 @@
-
+
-
+
diff --git a/templates/package/shared/view.tmpl b/templates/package/shared/view.tmpl
index 65f3e61fdb4..21f9a3e7b99 100644
--- a/templates/package/shared/view.tmpl
+++ b/templates/package/shared/view.tmpl
@@ -110,7 +110,7 @@
+
{{end}}
diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl
index 9fedd05f949..772e53bccb5 100644
--- a/templates/projects/list.tmpl
+++ b/templates/projects/list.tmpl
@@ -94,9 +94,9 @@
{{if and $.CanWriteProjects (not $.Repository.IsArchived)}}
-
+
+
{{end}}
diff --git a/templates/projects/view.tmpl b/templates/projects/view.tmpl
index 2e719984584..bc38c8a4b40 100644
--- a/templates/projects/view.tmpl
+++ b/templates/projects/view.tmpl
@@ -133,7 +133,7 @@
{{if $canWriteProject}}
-
-
+
{{end}}
diff --git a/templates/repo/actions/list.tmpl b/templates/repo/actions/list.tmpl
index 981bafcff6c..136d3eaf7ad 100644
--- a/templates/repo/actions/list.tmpl
+++ b/templates/repo/actions/list.tmpl
@@ -113,7 +113,7 @@
{{if $showCreateWorkflowBadge}}
-
+
{{end}}
{{template "base/footer" .}}
diff --git a/templates/repo/actions/workflow_dispatch.tmpl b/templates/repo/actions/workflow_dispatch.tmpl
index 5fb1012e2ee..be37649ce97 100644
--- a/templates/repo/actions/workflow_dispatch.tmpl
+++ b/templates/repo/actions/workflow_dispatch.tmpl
@@ -5,7 +5,7 @@
-
+
diff --git a/templates/repo/branch/list.tmpl b/templates/repo/branch/list.tmpl
index b150a1e57ce..e1b053525e4 100644
--- a/templates/repo/branch/list.tmpl
+++ b/templates/repo/branch/list.tmpl
@@ -218,18 +218,20 @@
-
+
+
-
+
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
-
+
-
+
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
-
+
{{template "base/footer" .}}
diff --git a/templates/repo/cite/cite_modal.tmpl b/templates/repo/cite/cite_modal.tmpl
index 4496b33cc70..3b0409cd3ea 100644
--- a/templates/repo/cite/cite_modal.tmpl
+++ b/templates/repo/cite/cite_modal.tmpl
@@ -1,4 +1,4 @@
-
+
-
+
diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl
index 06b967537ea..f8ba74799bc 100644
--- a/templates/repo/commit_page.tmpl
+++ b/templates/repo/commit_page.tmpl
@@ -49,7 +49,7 @@
data-modal-cherry-pick-header="{{ctx.Locale.Tr "repo.commit.cherry-pick-header" (ShortSha .CommitID)}}"
data-modal-cherry-pick-content="{{ctx.Locale.Tr "repo.commit.cherry-pick-content"}}"
data-modal-cherry-pick-submit="{{ctx.Locale.Tr "repo.commit.cherry-pick"}}">{{ctx.Locale.Tr "repo.commit.cherry-pick"}}
-
+
-
-
+
+
-
-
+
+
-
+
{{end}}
diff --git a/templates/repo/header/fork.tmpl b/templates/repo/header/fork.tmpl
index 86480694d3f..3adfb5cb345 100644
--- a/templates/repo/header/fork.tmpl
+++ b/templates/repo/header/fork.tmpl
@@ -22,7 +22,7 @@
{{CountFmt $.Repository.NumForks}}
{{if $.ShowForkModal}}
-
-
+
{{end}}
diff --git a/templates/repo/issue/labels/label_edit_modal.tmpl b/templates/repo/issue/labels/label_edit_modal.tmpl
index 23851b0d45d..269478119a7 100644
--- a/templates/repo/issue/labels/label_edit_modal.tmpl
+++ b/templates/repo/issue/labels/label_edit_modal.tmpl
@@ -1,4 +1,4 @@
-
-
+
diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl
index 4af4697b44f..e707947eff8 100644
--- a/templates/repo/issue/milestones.tmpl
+++ b/templates/repo/issue/milestones.tmpl
@@ -92,11 +92,11 @@
{{if or .CanWriteIssues .CanWritePulls}}
-
+
+
{{end}}
{{template "base/footer" .}}
diff --git a/templates/repo/issue/sidebar/issue_dependencies.tmpl b/templates/repo/issue/sidebar/issue_dependencies.tmpl
index 93aa55b6aa2..323766c8652 100644
--- a/templates/repo/issue/sidebar/issue_dependencies.tmpl
+++ b/templates/repo/issue/sidebar/issue_dependencies.tmpl
@@ -127,18 +127,20 @@
{{if and .CanCreateIssueDependencies (not .Repository.IsArchived)}}
-
+
+
{{end}}
{{end}}
diff --git a/templates/repo/issue/sidebar/issue_management.tmpl b/templates/repo/issue/sidebar/issue_management.tmpl
index 2f9688800e4..a672c6089d0 100644
--- a/templates/repo/issue/sidebar/issue_management.tmpl
+++ b/templates/repo/issue/sidebar/issue_management.tmpl
@@ -24,7 +24,7 @@
{{svg "octicon-lock"}} {{ctx.Locale.Tr "repo.issues.lock"}}
{{end}}
-
+
+
-
{{if $data.CurrentPullReviewers}}
-
-
+
{{end}}
diff --git a/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl b/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl
index ef2b99e817c..ccb20a50220 100644
--- a/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl
+++ b/templates/repo/issue/sidebar/stopwatch_timetracker.tmpl
@@ -35,7 +35,7 @@
{{end}}
{{/* set time estimate modal */}}
-
-
+
{{/* manually add time modal */}}
-
+
{{end}}
{{if .WorkingUsers}}
diff --git a/templates/repo/issue/view_content/reference_issue_dialog.tmpl b/templates/repo/issue/view_content/reference_issue_dialog.tmpl
index bbe80f3f16c..752dfd364e7 100644
--- a/templates/repo/issue/view_content/reference_issue_dialog.tmpl
+++ b/templates/repo/issue/view_content/reference_issue_dialog.tmpl
@@ -1,4 +1,4 @@
-
+
-
+
diff --git a/templates/repo/migrate/migrating.tmpl b/templates/repo/migrate/migrating.tmpl
index e1a652385db..b768d2263cd 100644
--- a/templates/repo/migrate/migrating.tmpl
+++ b/templates/repo/migrate/migrating.tmpl
@@ -51,7 +51,7 @@
-
+
-
+
-
+
{{template "base/modal_actions_confirm" .}}
-
+
{{template "base/footer" .}}
diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl
index 6ccde83d50b..be2ae5df8ff 100644
--- a/templates/repo/release/list.tmpl
+++ b/templates/repo/release/list.tmpl
@@ -116,17 +116,4 @@
-{{if (and ($.Permission.CanWrite ctx.Consts.RepoUnitTypeCode) .PageIsTagList)}}
-
-
-
-
{{ctx.Locale.Tr "repo.release.deletion_tag_desc"}}
-
- {{template "base/modal_actions_confirm" .}}
-
-{{end}}
-
{{template "base/footer" .}}
diff --git a/templates/repo/release/new.tmpl b/templates/repo/release/new.tmpl
index 0495142fa7c..d1c32304b09 100644
--- a/templates/repo/release/new.tmpl
+++ b/templates/repo/release/new.tmpl
@@ -145,7 +145,7 @@
-
+
+
{{template "base/footer" .}}
diff --git a/templates/repo/settings/branches.tmpl b/templates/repo/settings/branches.tmpl
index f8538fed99c..866fa05dec0 100644
--- a/templates/repo/settings/branches.tmpl
+++ b/templates/repo/settings/branches.tmpl
@@ -65,10 +65,10 @@
{{end}}
-
+
+
{{template "repo/settings/layout_footer" .}}
diff --git a/templates/repo/settings/collaboration.tmpl b/templates/repo/settings/collaboration.tmpl
index ecd57ff9bb8..519f266b614 100644
--- a/templates/repo/settings/collaboration.tmpl
+++ b/templates/repo/settings/collaboration.tmpl
@@ -118,10 +118,10 @@
{{end}}
-
+
+
{{template "repo/settings/layout_footer" .}}
diff --git a/templates/repo/settings/deploy_keys.tmpl b/templates/repo/settings/deploy_keys.tmpl
index 7b2e6397988..8f1a6a38fc6 100644
--- a/templates/repo/settings/deploy_keys.tmpl
+++ b/templates/repo/settings/deploy_keys.tmpl
@@ -60,16 +60,16 @@
-
+
+
-
+
+
{{template "repo/settings/layout_footer" .}}
diff --git a/templates/repo/settings/lfs.tmpl b/templates/repo/settings/lfs.tmpl
index 88226c0f833..95a5a0e535e 100644
--- a/templates/repo/settings/lfs.tmpl
+++ b/templates/repo/settings/lfs.tmpl
@@ -37,12 +37,12 @@
{{template "base/paginate" .}}
-
+
{{template "repo/settings/layout_footer" .}}
diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl
index 0354725cb57..787a73daf11 100644
--- a/templates/repo/settings/options.tmpl
+++ b/templates/repo/settings/options.tmpl
@@ -919,7 +919,7 @@
{{if $.CanManagerDangerZone}}
{{if .Repository.IsMirror}}
-
+
-
+
{{end}}
{{if .CanConvertFork}}
-
+
-
+
{{end}}
-
+
-
+
-
+
-
+
{{if not .Repository.IsFork}}
-
+
-
+
{{end}}
{{if .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeWiki}}
-
+
-
+
{{end}}
{{if not .Repository.IsMirror}}
-
+
{{end}}
{{end}}
diff --git a/templates/repo/settings/push_mirror_sync_modal.tmpl b/templates/repo/settings/push_mirror_sync_modal.tmpl
index 2c34ff64a22..e6342a9e734 100644
--- a/templates/repo/settings/push_mirror_sync_modal.tmpl
+++ b/templates/repo/settings/push_mirror_sync_modal.tmpl
@@ -1,4 +1,4 @@
-
+
-
+
diff --git a/templates/repo/tag/list.tmpl b/templates/repo/tag/list.tmpl
index 9d982a612ae..e6e6807c595 100644
--- a/templates/repo/tag/list.tmpl
+++ b/templates/repo/tag/list.tmpl
@@ -67,7 +67,7 @@
{{if $.Permission.CanWrite ctx.Consts.RepoUnitTypeCode}}
-
+
{{template "base/modal_actions_confirm" .}}
-
+
{{end}}
{{template "base/footer" .}}
diff --git a/templates/repo/watch_options_modal.tmpl b/templates/repo/watch_options_modal.tmpl
index 92deb8897a9..e8c1ee08f93 100644
--- a/templates/repo/watch_options_modal.tmpl
+++ b/templates/repo/watch_options_modal.tmpl
@@ -1,4 +1,4 @@
-
+
diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl
index d9428a114f8..1698e862d32 100644
--- a/templates/repo/wiki/view.tmpl
+++ b/templates/repo/wiki/view.tmpl
@@ -96,10 +96,10 @@
-
+
+
{{template "base/footer" .}}
diff --git a/templates/shared/actions/runner_edit.tmpl b/templates/shared/actions/runner_edit.tmpl
index 4fff302d557..3aac7648eb7 100644
--- a/templates/shared/actions/runner_edit.tmpl
+++ b/templates/shared/actions/runner_edit.tmpl
@@ -95,7 +95,7 @@
{{template "base/paginate" .}}
-
+
+
diff --git a/templates/shared/secrets/add_list.tmpl b/templates/shared/secrets/add_list.tmpl
index b83b60cd3ca..a1a5508aea6 100644
--- a/templates/shared/secrets/add_list.tmpl
+++ b/templates/shared/secrets/add_list.tmpl
@@ -66,7 +66,7 @@
{{/* Add secret dialog */}}
-
+
diff --git a/templates/shared/user/access_tokens.tmpl b/templates/shared/user/access_tokens.tmpl
index 02f75df370c..a8785e79241 100644
--- a/templates/shared/user/access_tokens.tmpl
+++ b/templates/shared/user/access_tokens.tmpl
@@ -101,7 +101,7 @@
-
+
+
{{if not .IsBot}}
-
+
{{template "base/modal_actions_confirm"}}
-
+
{{end}}
diff --git a/templates/shared/user/block_user_dialog.tmpl b/templates/shared/user/block_user_dialog.tmpl
index 1e8c7b669d2..41b5dbdcdfc 100644
--- a/templates/shared/user/block_user_dialog.tmpl
+++ b/templates/shared/user/block_user_dialog.tmpl
@@ -1,4 +1,4 @@
-
-
+
diff --git a/templates/shared/user/blocked_users.tmpl b/templates/shared/user/blocked_users.tmpl
index 86baa5d5af3..a35420c1815 100644
--- a/templates/shared/user/blocked_users.tmpl
+++ b/templates/shared/user/blocked_users.tmpl
@@ -52,7 +52,7 @@
{{end}}
-
-
+
diff --git a/templates/shared/variables/variable_list.tmpl b/templates/shared/variables/variable_list.tmpl
index d6d41aaa014..fc08b2beb01 100644
--- a/templates/shared/variables/variable_list.tmpl
+++ b/templates/shared/variables/variable_list.tmpl
@@ -64,7 +64,7 @@
{{/** Edit variable dialog */}}
-
+
diff --git a/templates/user/settings/account.tmpl b/templates/user/settings/account.tmpl
index 9b1f25ec89a..c4adbdc44a9 100644
--- a/templates/user/settings/account.tmpl
+++ b/templates/user/settings/account.tmpl
@@ -127,7 +127,7 @@
-
+
+
{{end}}
-
+
+
{{template "user/settings/layout_footer" .}}
diff --git a/templates/user/settings/applications_oauth2_list.tmpl b/templates/user/settings/applications_oauth2_list.tmpl
index 8d10c3b2cd8..735eab30ac8 100644
--- a/templates/user/settings/applications_oauth2_list.tmpl
+++ b/templates/user/settings/applications_oauth2_list.tmpl
@@ -35,7 +35,7 @@
{{end}}
-
+
+
diff --git a/templates/user/settings/grants_oauth2.tmpl b/templates/user/settings/grants_oauth2.tmpl
index 3b5c19c674a..44249c7c79f 100644
--- a/templates/user/settings/grants_oauth2.tmpl
+++ b/templates/user/settings/grants_oauth2.tmpl
@@ -27,7 +27,7 @@
{{end}}
-
+
+
diff --git a/templates/user/settings/keys_gpg.tmpl b/templates/user/settings/keys_gpg.tmpl
index 6b63ffd27c3..76432601ea0 100644
--- a/templates/user/settings/keys_gpg.tmpl
+++ b/templates/user/settings/keys_gpg.tmpl
@@ -123,7 +123,7 @@
{{end}}
{{end}}
-
+
+
diff --git a/templates/user/settings/keys_principal.tmpl b/templates/user/settings/keys_principal.tmpl
index e3e5122d768..f0b09f0e0b3 100644
--- a/templates/user/settings/keys_principal.tmpl
+++ b/templates/user/settings/keys_principal.tmpl
@@ -55,7 +55,7 @@
-
+
+
{{end}}
diff --git a/templates/user/settings/keys_ssh.tmpl b/templates/user/settings/keys_ssh.tmpl
index 7eac18b4b2d..ce0b913e278 100644
--- a/templates/user/settings/keys_ssh.tmpl
+++ b/templates/user/settings/keys_ssh.tmpl
@@ -103,7 +103,7 @@
{{end}}
{{end}}
-
+
+
diff --git a/templates/user/settings/organization.tmpl b/templates/user/settings/organization.tmpl
index 3c223f12555..d7c2fa7f370 100644
--- a/templates/user/settings/organization.tmpl
+++ b/templates/user/settings/organization.tmpl
@@ -38,14 +38,16 @@
-
+
+
{{template "user/settings/layout_footer" .}}
diff --git a/templates/user/settings/repos.tmpl b/templates/user/settings/repos.tmpl
index b5242f40600..290e47bce13 100644
--- a/templates/user/settings/repos.tmpl
+++ b/templates/user/settings/repos.tmpl
@@ -35,7 +35,7 @@
{{if $.allowAdopt}}
-
+
+
{{end}}
{{if $.allowDelete}}
-
+
+
{{end}}
{{end}}
@@ -114,15 +114,4 @@
-
-
-
-
{{ctx.Locale.Tr "settings.remove_account_link_desc"}}
-
- {{template "base/modal_actions_confirm" .}}
-
-
{{template "user/settings/layout_footer" .}}
diff --git a/templates/user/settings/security/accountlinks.tmpl b/templates/user/settings/security/accountlinks.tmpl
index fa6dc53b0e9..6eb10f1b491 100644
--- a/templates/user/settings/security/accountlinks.tmpl
+++ b/templates/user/settings/security/accountlinks.tmpl
@@ -48,7 +48,7 @@
{{end}}
-
+
+
{{end}}
diff --git a/templates/user/settings/security/openid.tmpl b/templates/user/settings/security/openid.tmpl
index 1e0b21cdda7..e594f9c5b01 100644
--- a/templates/user/settings/security/openid.tmpl
+++ b/templates/user/settings/security/openid.tmpl
@@ -48,7 +48,7 @@
-
+
+
diff --git a/templates/user/settings/security/twofa.tmpl b/templates/user/settings/security/twofa.tmpl
index 2e063f00509..f9adf369bf8 100644
--- a/templates/user/settings/security/twofa.tmpl
+++ b/templates/user/settings/security/twofa.tmpl
@@ -22,7 +22,7 @@
{{end}}
-
+
+
diff --git a/templates/user/settings/security/webauthn.tmpl b/templates/user/settings/security/webauthn.tmpl
index 581383a83aa..d270fb2e3cc 100644
--- a/templates/user/settings/security/webauthn.tmpl
+++ b/templates/user/settings/security/webauthn.tmpl
@@ -35,7 +35,7 @@
-
+
-
+
+
+
diff --git a/tests/e2e/issue-comment.test.ts b/tests/e2e/issue-comment.test.ts
index ba28a79b775..be430ecbfb7 100644
--- a/tests/e2e/issue-comment.test.ts
+++ b/tests/e2e/issue-comment.test.ts
@@ -1,6 +1,6 @@
import {env} from 'node:process';
import {test, expect} from '@playwright/test';
-import {login, apiCreateRepo, apiCreateIssue, randomString} from './utils.ts';
+import {login, apiCreateRepo, apiCreateIssue, apiEditIssue, randomString} from './utils.ts';
test('comment on and close an issue', async ({page, request}) => {
const repoName = `e2e-issue-comment-${randomString(8)}`;
@@ -48,3 +48,25 @@ test('unsaved issue description prompts before leaving', async ({page, request})
expect((await dialogPromise).type()).toBe('beforeunload');
await expect(page).toHaveURL(/\/issues\/new$/);
});
+
+test('delete an edit history entry via a confirm over the history modal', async ({page, request}) => {
+ const repoName = `e2e-issue-history-${randomString(8)}`;
+ const owner = env.GITEA_TEST_E2E_USER;
+ await Promise.all([
+ (async () => {
+ await apiCreateRepo(request, {name: repoName, autoInit: false});
+ await apiCreateIssue(request, {owner, repo: repoName, title: 'History test', body: 'first'});
+ await apiEditIssue(request, owner, repoName, 1, {body: 'second'});
+ })(),
+ login(page),
+ ]);
+ await page.goto(`/${owner}/${repoName}/issues/1`);
+ const historyMenu = page.locator('.content-history-menu');
+ await historyMenu.click();
+ await historyMenu.getByRole('menuitem').last().click();
+ const history = page.getByRole('dialog');
+ await history.getByText('Options').click();
+ await history.getByRole('menuitem', {name: 'Delete from history'}).click();
+ await page.getByRole('dialog', {name: 'Delete from history?'}).getByRole('button', {name: 'Confirm'}).click();
+ await expect(page.getByRole('dialog')).toHaveCount(0);
+});
diff --git a/tests/e2e/label.test.ts b/tests/e2e/label.test.ts
new file mode 100644
index 00000000000..4f5a6890260
--- /dev/null
+++ b/tests/e2e/label.test.ts
@@ -0,0 +1,19 @@
+import {env} from 'node:process';
+import {test, expect} from '@playwright/test';
+import {login, apiCreateRepo, randomString} from './utils.ts';
+
+test('create a label in the modal with a color from the picker popup', async ({page}) => {
+ const repoName = `e2e-label-${randomString(8)}`;
+ await Promise.all([login(page), apiCreateRepo(page.request, {name: repoName, autoInit: false})]);
+ await page.goto(`/${env.GITEA_TEST_E2E_USER}/${repoName}/labels`);
+ await page.getByRole('button', {name: 'New Label'}).click();
+ const modal = page.getByRole('dialog');
+ await modal.getByRole('button', {name: 'Save'}).click();
+ await modal.getByPlaceholder('Label name').fill('e2e-label');
+ const colorInput = modal.locator('.color-picker-combo input');
+ await colorInput.fill('');
+ await page.locator('hex-color-picker').click({position: {x: 10, y: 10}});
+ await expect(colorInput).toHaveValue(/^#[0-9a-f]{6}$/);
+ await modal.getByRole('button', {name: 'Save'}).click();
+ await expect(page.locator('.issue-label-list')).toContainText('e2e-label');
+});
diff --git a/tests/e2e/repo.test.ts b/tests/e2e/repo.test.ts
index 996fdf40295..9e26bf7d0e1 100644
--- a/tests/e2e/repo.test.ts
+++ b/tests/e2e/repo.test.ts
@@ -1,6 +1,6 @@
import {env} from 'node:process';
-import {test} from '@playwright/test';
-import {login, randomString} from './utils.ts';
+import {test, expect} from '@playwright/test';
+import {login, apiCreateBranch, apiCreateRepo, randomString} from './utils.ts';
test('create a repository', async ({page}) => {
const repoName = `e2e-repo-${randomString(8)}`;
@@ -10,3 +10,21 @@ test('create a repository', async ({page}) => {
await page.getByRole('button', {name: 'Create Repository'}).click();
await page.waitForURL(new RegExp(`/${env.GITEA_TEST_E2E_USER}/${repoName}$`));
});
+
+test('delete a branch via its modal', async ({page, request}) => {
+ const repoName = `e2e-branch-${randomString(8)}`;
+ const owner = env.GITEA_TEST_E2E_USER;
+ await Promise.all([
+ (async () => {
+ await apiCreateRepo(request, {name: repoName});
+ await apiCreateBranch(request, owner, repoName, 'feature');
+ })(),
+ login(page),
+ ]);
+ await page.goto(`/${owner}/${repoName}/branches`);
+ await page.getByRole('button', {name: 'Delete Branch "feature"'}).click();
+ const modal = page.getByRole('dialog');
+ await expect(modal).toContainText('feature');
+ await modal.getByRole('button', {name: 'Yes'}).click();
+ await expect(page.locator('.flash-message')).toContainText('Branch "feature" has been deleted.');
+});
diff --git a/tests/e2e/utils.ts b/tests/e2e/utils.ts
index b742a181624..bf24603295d 100644
--- a/tests/e2e/utils.ts
+++ b/tests/e2e/utils.ts
@@ -91,6 +91,20 @@ export async function apiCloseIssue(requestContext: APIRequestContext, owner: st
}), 'apiCloseIssue');
}
+export async function apiEditIssue(requestContext: APIRequestContext, owner: string, repo: string, issueIndex: number, data: Record) {
+ await apiRetry(() => requestContext.patch(`${baseUrl()}/api/v1/repos/${owner}/${repo}/issues/${issueIndex}`, {
+ headers: apiHeaders(),
+ data,
+ }), 'apiEditIssue');
+}
+
+export async function apiCreateBranch(requestContext: APIRequestContext, owner: string, repo: string, name: string) {
+ await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/branches`, {
+ headers: apiHeaders(),
+ data: {new_branch_name: name},
+ }), 'apiCreateBranch');
+}
+
/** Create a PR via API. Returns the PR index for subsequent operations. */
export async function apiCreatePR(requestContext: APIRequestContext, owner: string, repo: string, head: string, base: string, title: string, {headers}: {headers?: Record} = {}): Promise {
const response = await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/pulls`, {
diff --git a/web_src/css/base.css b/web_src/css/base.css
index 43f49d4b49e..5a4adfeea7c 100644
--- a/web_src/css/base.css
+++ b/web_src/css/base.css
@@ -845,7 +845,7 @@ table th[data-sortt-desc] .svg {
}
/* this is useful to make a left-right (e.g.: title .... operations) layout with default gap, and it wrap for small widths */
-.ui.modal .header.flex-left-right,
+.modal-dialog .header.flex-left-right,
.flex-left-right {
display: flex;
flex-wrap: wrap;
diff --git a/web_src/css/modules/dimmer.css b/web_src/css/modules/dimmer.css
index 7d1ca6171aa..15c3309bea7 100644
--- a/web_src/css/modules/dimmer.css
+++ b/web_src/css/modules/dimmer.css
@@ -1,27 +1,24 @@
-/* These are the remnants of the fomantic dimmer module */
-
-.ui.dimmer {
+.modal-backdrop {
position: fixed;
display: none;
inset: 0;
background: var(--color-overlay-backdrop);
- opacity: 0;
- z-index: 1000;
+ z-index: var(--z-index-modal);
overflow-y: auto;
justify-content: center;
padding: 8px 0;
animation-name: fadein;
animation-duration: .2s;
+ overscroll-behavior: contain;
user-select: none;
}
-.ui.active.dimmer {
+.modal-backdrop:has(> [open]) {
display: flex;
- opacity: 1;
}
-.ui.dimmer > .ui.modal {
+.modal-backdrop > .modal-dialog {
position: static;
- margin-top: auto !important;
- margin-bottom: auto !important;
+ margin-top: auto;
+ margin-bottom: auto;
}
diff --git a/web_src/css/modules/modal.css b/web_src/css/modules/modal.css
index b89b83b7ed4..52c95801ed8 100644
--- a/web_src/css/modules/modal.css
+++ b/web_src/css/modules/modal.css
@@ -1,17 +1,17 @@
-/* to keep the changes minimal and easy to revert or fine tune, the legacy classes
-like "small" and "mini" are still kept for the HTML elements, these can be cleaned up in the future */
-.ui.modal {
- position: absolute;
- display: none;
- z-index: var(--z-index-modal);
+.modal-page-blocked {
+ overflow: hidden;
+}
+
+.modal-dialog {
text-align: left;
background: var(--color-body);
border: none;
+ margin: 0;
+ padding: 0;
+ color: inherit;
box-shadow: 1px 3px 3px 0 var(--color-shadow), 1px 3px 15px 2px var(--color-shadow);
- transform-origin: 50% 25%;
border-radius: var(--border-radius);
user-select: text;
- will-change: top, left, margin, transform, opacity;
width: fit-content;
/* on small screens, the modal should not exceed 90% of the viewport width;
@@ -22,17 +22,17 @@ like "small" and "mini" are still kept for the HTML elements, these can be clean
min-width: min(800px, max(min(300px, 90vw), 30vw));
}
-.ui.modal > :first-child {
+.modal-dialog > :first-child {
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
}
-.ui.modal > :last-child {
+.modal-dialog > :last-child {
border-bottom-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
}
-.ui.modal .close-modal {
+.modal-dialog .close-modal {
cursor: pointer;
position: absolute;
top: 50%;
@@ -47,7 +47,7 @@ like "small" and "mini" are still kept for the HTML elements, these can be clean
height: 1.5rem;
}
-.ui.modal .close-modal::before {
+.modal-dialog .close-modal::before {
content: "";
display: block;
width: 100%;
@@ -57,11 +57,11 @@ like "small" and "mini" are still kept for the HTML elements, these can be clean
mask-size: cover;
}
-.ui.modal .close-modal:hover {
+.modal-dialog .close-modal:hover {
opacity: 1;
}
-.ui.modal > .header {
+.modal-dialog > .header {
display: block;
position: relative;
font-size: 18px;
@@ -76,8 +76,8 @@ like "small" and "mini" are still kept for the HTML elements, these can be clean
border-top-right-radius: var(--border-radius);
}
-.ui.modal > .content,
-.ui.modal form > .content {
+.modal-dialog > .content,
+.modal-dialog form > .content {
display: block;
width: 100%;
font-size: 1em;
@@ -87,9 +87,9 @@ like "small" and "mini" are still kept for the HTML elements, these can be clean
border-radius: 0 0 var(--border-radius) var(--border-radius);
}
-.ui.modal > .actions,
-.ui.modal .content + .actions,
-.ui.modal .content + form > .actions {
+.modal-dialog > .actions,
+.modal-dialog .content + .actions,
+.modal-dialog .content + form > .actions {
background: var(--color-secondary-bg);
padding: 1rem;
border-top: 1px solid var(--color-secondary);
@@ -97,17 +97,13 @@ like "small" and "mini" are still kept for the HTML elements, these can be clean
border-radius: 0 0 var(--border-radius) var(--border-radius);
}
-.ui.active.modal {
- display: block;
-}
-
-.ui.modal .content > form > .actions,
-.ui.modal .content > .actions {
+.modal-dialog .content > form > .actions,
+.modal-dialog .content > .actions {
padding-top: 1em;
text-align: right;
}
-.ui.modal .actions > .ui.button {
+.modal-dialog .actions > .ui.button {
display: inline-flex;
gap: var(--gap-inline);
align-items: center;
@@ -116,7 +112,7 @@ like "small" and "mini" are still kept for the HTML elements, these can be clean
margin-right: 0;
}
-.ui.modal .actions > .ui.button.danger {
+.modal-dialog .actions > .ui.button.danger {
display: block;
width: 100%;
margin: 0 auto;
diff --git a/web_src/css/modules/transition.css b/web_src/css/modules/transition.css
index a0e7db2b4c8..1a216348f02 100644
--- a/web_src/css/modules/transition.css
+++ b/web_src/css/modules/transition.css
@@ -1,23 +1,12 @@
-/* styles for Fomantic transition (toggle): dimmer, modal, dropdown menu */
+/* styles for Fomantic transition (toggle): dropdown menu */
/* this is the only place using "hidden" and "visible" classes, it can safely work with tailwind without prefix in the future */
-.ui.modal.transition.hidden,
.ui.dropdown > .menu.transition.hidden {
display: none;
visibility: hidden;
}
-.ui.modal.transition.visible,
.ui.dropdown > .menu.transition.visible {
display: block;
visibility: visible;
}
-
-/* dimmer uses "active" but not "hidden/visible" classes, no special classes for it here */
-
-/* only modal and dimmer need animation, dropdown menu doesn't */
-.ui.modal.transition {
- animation-name: fadein;
- animation-duration: 100ms;
- animation-timing-function: ease-in-out;
-}
diff --git a/web_src/fomantic/build/components/modal.js b/web_src/fomantic/build/components/modal.js
deleted file mode 100644
index 21b7a0acb39..00000000000
--- a/web_src/fomantic/build/components/modal.js
+++ /dev/null
@@ -1,1209 +0,0 @@
-/*!
- * # Fomantic-UI - Modal
- * http://github.com/fomantic/Fomantic-UI/
- *
- *
- * Released under the MIT license
- * http://opensource.org/licenses/MIT
- *
- */
-
-;(function ($, window, document, undefined) {
-
-'use strict';
-
-$.isFunction = $.isFunction || function(obj) {
- return typeof obj === "function" && typeof obj.nodeType !== "number";
-};
-
-window = (typeof window != 'undefined' && window.Math == Math)
- ? window
- : (typeof self != 'undefined' && self.Math == Math)
- ? self
- : Function('return this')()
-;
-
-$.fn.modal = function(parameters) {
- var
- $allModules = $(this),
- $window = $(window),
- $document = $(document),
- $body = $('body'),
-
- moduleSelector = $allModules.selector || '',
-
- time = new Date().getTime(),
- performance = [],
-
- query = arguments[0],
- methodInvoked = (typeof query == 'string'),
- queryArguments = [].slice.call(arguments, 1),
-
- requestAnimationFrame = window.requestAnimationFrame
- || window.mozRequestAnimationFrame
- || window.webkitRequestAnimationFrame
- || window.msRequestAnimationFrame
- || function(callback) { setTimeout(callback, 0); },
-
- returnedValue
- ;
-
- $allModules
- .each(function() {
- var
- settings = ( $.isPlainObject(parameters) )
- ? $.extend(true, {}, $.fn.modal.settings, parameters)
- : $.extend({}, $.fn.modal.settings),
-
- selector = settings.selector,
- className = settings.className,
- namespace = settings.namespace,
- error = settings.error,
-
- eventNamespace = '.' + namespace,
- moduleNamespace = 'module-' + namespace,
-
- $module = $(this),
- $context = (typeof settings.context === 'string') ? $(document).find(settings.context) : $(settings.context), // GITEA-PATCH: use "jQuery.find(selector)" instead of "jQuery(selector)"
- $close = $module.find(selector.close),
-
- $allModals,
- $otherModals,
- $focusedElement,
- $dimmable,
- $dimmer,
-
- element = this,
- instance = $module.data(moduleNamespace),
-
- ignoreRepeatedEvents = false,
-
- initialMouseDownInModal,
- initialMouseDownInScrollbar,
- initialBodyMargin = '',
- tempBodyMargin = '',
-
- elementEventNamespace,
- id,
- observer,
- module
- ;
- module = {
-
- initialize: function() {
- module.cache = {};
- module.verbose('Initializing dimmer', $context);
-
- module.create.id();
- module.create.dimmer();
-
- if ( settings.allowMultiple ) {
- module.create.innerDimmer();
- }
- if (!settings.centered){
- $module.addClass('top aligned');
- }
- module.refreshModals();
-
- module.bind.events();
- if(settings.observeChanges) {
- module.observeChanges();
- }
- module.instantiate();
- },
-
- instantiate: function() {
- module.verbose('Storing instance of modal');
- instance = module;
- $module
- .data(moduleNamespace, instance)
- ;
- },
-
- create: {
- dimmer: function() {
- var
- defaultSettings = {
- debug : settings.debug,
- dimmerName : 'modals'
- },
- dimmerSettings = $.extend(true, defaultSettings, settings.dimmerSettings)
- ;
- if($.fn.dimmer === undefined) {
- module.error(error.dimmer);
- return;
- }
- module.debug('Creating dimmer');
- $dimmable = $context.dimmer(dimmerSettings);
- if(settings.detachable) {
- module.verbose('Modal is detachable, moving content into dimmer');
- $dimmable.dimmer('add content', $module);
- }
- else {
- module.set.undetached();
- }
- $dimmer = $dimmable.dimmer('get dimmer');
- },
- id: function() {
- id = (Math.random().toString(16) + '000000000').substr(2, 8);
- elementEventNamespace = '.' + id;
- module.verbose('Creating unique id for element', id);
- },
- innerDimmer: function() {
- if ( $module.find(selector.dimmer).length == 0 ) {
- $module.prepend('');
- }
- }
- },
-
- destroy: function() {
- if (observer) {
- observer.disconnect();
- }
- module.verbose('Destroying previous modal');
- $module
- .removeData(moduleNamespace)
- .off(eventNamespace)
- ;
- $window.off(elementEventNamespace);
- $dimmer.off(elementEventNamespace);
- $close.off(eventNamespace);
- $context.dimmer('destroy');
- },
-
- observeChanges: function() {
- if('MutationObserver' in window) {
- observer = new MutationObserver(function(mutations) {
- module.debug('DOM tree modified, refreshing');
- module.refresh();
- });
- observer.observe(element, {
- childList : true,
- subtree : true
- });
- module.debug('Setting up mutation observer', observer);
- }
- },
-
- refresh: function() {
- module.remove.scrolling();
- module.cacheSizes();
- if(!module.can.useFlex()) {
- module.set.modalOffset();
- }
- module.set.screenHeight();
- module.set.type();
- },
-
- refreshModals: function() {
- $otherModals = $module.siblings(selector.modal);
- $allModals = $otherModals.add($module);
- },
-
- attachEvents: function(selector, event) {
- var
- $toggle = $(selector)
- ;
- event = $.isFunction(module[event])
- ? module[event]
- : module.toggle
- ;
- if($toggle.length > 0) {
- module.debug('Attaching modal events to element', selector, event);
- $toggle
- .off(eventNamespace)
- .on('click' + eventNamespace, event)
- ;
- }
- else {
- module.error(error.notFound, selector);
- }
- },
-
- bind: {
- events: function() {
- module.verbose('Attaching events');
- $module
- .on('click' + eventNamespace, selector.close, module.event.close)
- .on('click' + eventNamespace, selector.approve, module.event.approve)
- .on('click' + eventNamespace, selector.deny, module.event.deny)
- ;
- $window
- .on('resize' + elementEventNamespace, module.event.resize)
- ;
- },
- scrollLock: function() {
- // touch events default to passive, due to changes in chrome to optimize mobile perf
- $dimmable.get(0).addEventListener('touchmove', module.event.preventScroll, { passive: false });
- }
- },
-
- unbind: {
- scrollLock: function() {
- $dimmable.get(0).removeEventListener('touchmove', module.event.preventScroll, { passive: false });
- }
- },
-
- get: {
- id: function() {
- return (Math.random().toString(16) + '000000000').substr(2, 8);
- }
- },
-
- event: {
- approve: function() {
- if(ignoreRepeatedEvents || settings.onApprove.call(element, $(this)) === false) {
- module.verbose('Approve callback returned false cancelling hide');
- return;
- }
- ignoreRepeatedEvents = true;
- module.hide(function() {
- ignoreRepeatedEvents = false;
- });
- },
- preventScroll: function(event) {
- if(event.target.className.indexOf('dimmer') !== -1) {
- event.preventDefault();
- }
- },
- deny: function() {
- if(ignoreRepeatedEvents || settings.onDeny.call(element, $(this)) === false) {
- module.verbose('Deny callback returned false cancelling hide');
- return;
- }
- ignoreRepeatedEvents = true;
- module.hide(function() {
- ignoreRepeatedEvents = false;
- });
- },
- close: function() {
- module.hide();
- },
- mousedown: function(event) {
- var
- $target = $(event.target),
- isRtl = module.is.rtl();
- ;
- initialMouseDownInModal = ($target.closest(selector.modal).length > 0);
- if(initialMouseDownInModal) {
- module.verbose('Mouse down event registered inside the modal');
- }
- initialMouseDownInScrollbar = module.is.scrolling() && ((!isRtl && $(window).outerWidth() - settings.scrollbarWidth <= event.clientX) || (isRtl && settings.scrollbarWidth >= event.clientX));
- if(initialMouseDownInScrollbar) {
- module.verbose('Mouse down event registered inside the scrollbar');
- }
- },
- mouseup: function(event) {
- if(!settings.closable) {
- module.verbose('Dimmer clicked but closable setting is disabled');
- return;
- }
- if(initialMouseDownInModal) {
- module.debug('Dimmer clicked but mouse down was initially registered inside the modal');
- return;
- }
- if(initialMouseDownInScrollbar){
- module.debug('Dimmer clicked but mouse down was initially registered inside the scrollbar');
- return;
- }
- var
- $target = $(event.target),
- isInModal = ($target.closest(selector.modal).length > 0),
- isInDOM = $.contains(document.documentElement, event.target)
- ;
- if(!isInModal && isInDOM && module.is.active() && $module.hasClass(className.front) ) {
- module.debug('Dimmer clicked, hiding all modals');
- if(settings.allowMultiple) {
- if(!module.hideAll()) {
- return;
- }
- }
- else if(!module.hide()){
- return;
- }
- module.remove.clickaway();
- }
- },
- debounce: function(method, delay) {
- clearTimeout(module.timer);
- module.timer = setTimeout(method, delay);
- },
- keyboard: function(event) {
- var
- keyCode = event.which,
- escapeKey = 27
- ;
- if(keyCode == escapeKey) {
- if(settings.closable) {
- module.debug('Escape key pressed hiding modal');
- if ( $module.hasClass(className.front) ) {
- module.hide();
- }
- }
- else {
- module.debug('Escape key pressed, but closable is set to false');
- }
- event.preventDefault();
- }
- },
- resize: function() {
- if( $dimmable.dimmer('is active') && ( module.is.animating() || module.is.active() ) ) {
- requestAnimationFrame(module.refresh);
- }
- }
- },
-
- toggle: function() {
- if( module.is.active() || module.is.animating() ) {
- module.hide();
- }
- else {
- module.show();
- }
- },
-
- show: function(callback) {
- callback = $.isFunction(callback)
- ? callback
- : function(){}
- ;
- module.refreshModals();
- module.set.dimmerSettings();
- module.set.dimmerStyles();
-
- module.showModal(callback);
- },
-
- hide: function(callback) {
- callback = $.isFunction(callback)
- ? callback
- : function(){}
- ;
- module.refreshModals();
- return module.hideModal(callback);
- },
-
- showModal: function(callback) {
- callback = $.isFunction(callback)
- ? callback
- : function(){}
- ;
- if( module.is.animating() || !module.is.active() ) {
- module.showDimmer();
- module.cacheSizes();
- module.set.bodyMargin();
- if(module.can.useFlex()) {
- module.remove.legacy();
- }
- else {
- module.set.legacy();
- module.set.modalOffset();
- module.debug('Using non-flex legacy modal positioning.');
- }
- module.set.screenHeight();
- module.set.type();
- module.set.clickaway();
-
- if( !settings.allowMultiple && module.others.active() ) {
- module.hideOthers(module.showModal);
- }
- else {
- ignoreRepeatedEvents = false;
- if( settings.allowMultiple ) {
- if ( module.others.active() ) {
- $otherModals.filter('.' + className.active).find(selector.dimmer).addClass('active');
- }
-
- if ( settings.detachable ) {
- $module.detach().appendTo($dimmer);
- }
- }
- settings.onShow.call(element);
- if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
- module.debug('Showing modal with css animations');
- $module
- .transition({
- debug : settings.debug,
- animation : settings.transition + ' in',
- queue : settings.queue,
- duration : settings.duration,
- useFailSafe : true,
- onComplete : function() {
- settings.onVisible.apply(element);
- if(settings.keyboardShortcuts) {
- module.add.keyboardShortcuts();
- }
- module.save.focus();
- module.set.active();
- if(settings.autofocus) {
- module.set.autofocus();
- }
- callback();
- }
- })
- ;
- }
- else {
- module.error(error.noTransition);
- }
- }
- }
- else {
- module.debug('Modal is already visible');
- }
- },
-
- hideModal: function(callback, keepDimmed, hideOthersToo) {
- var
- $previousModal = $otherModals.filter('.' + className.active).last()
- ;
- callback = $.isFunction(callback)
- ? callback
- : function(){}
- ;
- module.debug('Hiding modal');
- if(settings.onHide.call(element, $(this)) === false) {
- module.verbose('Hide callback returned false cancelling hide');
- ignoreRepeatedEvents = false;
- return false;
- }
- $module.fomanticExt.onModalBeforeHidden.call(element); // GITEA-PATCH: handle more UI updates before hidden
- if( module.is.animating() || module.is.active() ) {
- if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
- module.remove.active();
- $module
- .transition({
- debug : settings.debug,
- animation : settings.transition + ' out',
- queue : settings.queue,
- duration : settings.duration,
- useFailSafe : true,
- onStart : function() {
- if(!module.others.active() && !module.others.animating() && !keepDimmed) {
- module.hideDimmer();
- }
- if( settings.keyboardShortcuts && !module.others.active() ) {
- module.remove.keyboardShortcuts();
- }
- },
- onComplete : function() {
- module.unbind.scrollLock();
- if ( settings.allowMultiple ) {
- $previousModal.addClass(className.front);
- $module.removeClass(className.front);
-
- if ( hideOthersToo ) {
- $allModals.find(selector.dimmer).removeClass('active');
- }
- else {
- $previousModal.find(selector.dimmer).removeClass('active');
- }
- }
- settings.onHidden.call(element);
- module.remove.dimmerStyles();
- module.restore.focus();
- callback();
- }
- })
- ;
- }
- else {
- module.error(error.noTransition);
- }
- }
- },
-
- showDimmer: function() {
- if($dimmable.dimmer('is animating') || !$dimmable.dimmer('is active') ) {
- module.save.bodyMargin();
- module.debug('Showing dimmer');
- $dimmable.dimmer('show');
- }
- else {
- module.debug('Dimmer already visible');
- }
- },
-
- hideDimmer: function() {
- if( $dimmable.dimmer('is animating') || ($dimmable.dimmer('is active')) ) {
- module.unbind.scrollLock();
- $dimmable.dimmer('hide', function() {
- module.restore.bodyMargin();
- module.remove.clickaway();
- module.remove.screenHeight();
- });
- }
- else {
- module.debug('Dimmer is not visible cannot hide');
- return;
- }
- },
-
- hideAll: function(callback) {
- var
- $visibleModals = $allModals.filter('.' + className.active + ', .' + className.animating)
- ;
- callback = $.isFunction(callback)
- ? callback
- : function(){}
- ;
- if( $visibleModals.length > 0 ) {
- module.debug('Hiding all visible modals');
- var hideOk = true;
-//check in reverse order trying to hide most top displayed modal first
- $($visibleModals.get().reverse()).each(function(index,element){
- if(hideOk){
- hideOk = $(element).modal('hide modal', callback, false, true);
- }
- });
- if(hideOk) {
- module.hideDimmer();
- }
- return hideOk;
- }
- },
-
- hideOthers: function(callback) {
- var
- $visibleModals = $otherModals.filter('.' + className.active + ', .' + className.animating)
- ;
- callback = $.isFunction(callback)
- ? callback
- : function(){}
- ;
- if( $visibleModals.length > 0 ) {
- module.debug('Hiding other modals', $otherModals);
- $visibleModals
- .modal('hide modal', callback, true)
- ;
- }
- },
-
- others: {
- active: function() {
- return ($otherModals.filter('.' + className.active).length > 0);
- },
- animating: function() {
- return ($otherModals.filter('.' + className.animating).length > 0);
- }
- },
-
-
- add: {
- keyboardShortcuts: function() {
- module.verbose('Adding keyboard shortcuts');
- $document
- .on('keyup' + eventNamespace, module.event.keyboard)
- ;
- }
- },
-
- save: {
- focus: function() {
- var
- $activeElement = $(document.activeElement),
- inCurrentModal = $activeElement.closest($module).length > 0
- ;
- if(!inCurrentModal) {
- $focusedElement = $(document.activeElement).blur();
- }
- },
- bodyMargin: function() {
- initialBodyMargin = $body.css('margin-'+(module.can.leftBodyScrollbar() ? 'left':'right'));
- var bodyMarginRightPixel = parseInt(initialBodyMargin.replace(/[^\d.]/g, '')),
- bodyScrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
- tempBodyMargin = bodyMarginRightPixel + bodyScrollbarWidth;
- }
- },
-
- restore: {
- focus: function() {
- if($focusedElement && $focusedElement.length > 0 && settings.restoreFocus) {
- $focusedElement.focus();
- }
- },
- bodyMargin: function() {
- var position = module.can.leftBodyScrollbar() ? 'left':'right';
- $body.css('margin-'+position, initialBodyMargin);
- $body.find(selector.bodyFixed.replace('right',position)).css('padding-'+position, initialBodyMargin);
- }
- },
-
- remove: {
- active: function() {
- $module.removeClass(className.active);
- },
- legacy: function() {
- $module.removeClass(className.legacy);
- },
- clickaway: function() {
- if (!settings.detachable) {
- $module
- .off('mousedown' + elementEventNamespace)
- ;
- }
- $dimmer
- .off('mousedown' + elementEventNamespace)
- ;
- $dimmer
- .off('mouseup' + elementEventNamespace)
- ;
- },
- dimmerStyles: function() {
- $dimmer.removeClass(className.inverted);
- $dimmable.removeClass(className.blurring);
- },
- bodyStyle: function() {
- if($body.attr('style') === '') {
- module.verbose('Removing style attribute');
- $body.removeAttr('style');
- }
- },
- screenHeight: function() {
- module.debug('Removing page height');
- $body
- .css('height', '')
- ;
- },
- keyboardShortcuts: function() {
- module.verbose('Removing keyboard shortcuts');
- $document
- .off('keyup' + eventNamespace)
- ;
- },
- scrolling: function() {
- $dimmable.removeClass(className.scrolling);
- $module.removeClass(className.scrolling);
- }
- },
-
- cacheSizes: function() {
- $module.addClass(className.loading);
- var
- scrollHeight = $module.prop('scrollHeight'),
- modalWidth = $module.outerWidth(),
- modalHeight = $module.outerHeight()
- ;
- if(module.cache.pageHeight === undefined || modalHeight !== 0) {
- $.extend(module.cache, {
- pageHeight : $(document).outerHeight(),
- width : modalWidth,
- height : modalHeight + settings.offset,
- scrollHeight : scrollHeight + settings.offset,
- contextHeight : (settings.context == 'body')
- ? $(window).height()
- : $dimmable.height(),
- });
- module.cache.topOffset = -(module.cache.height / 2);
- }
- $module.removeClass(className.loading);
- module.debug('Caching modal and container sizes', module.cache);
- },
-
- can: {
- leftBodyScrollbar: function(){
- if(module.cache.leftBodyScrollbar === undefined) {
- module.cache.leftBodyScrollbar = module.is.rtl() && ((module.is.iframe && !module.is.firefox()) || module.is.safari() || module.is.edge() || module.is.ie());
- }
- return module.cache.leftBodyScrollbar;
- },
- useFlex: function() {
- if (settings.useFlex === 'auto') {
- return settings.detachable && !module.is.ie();
- }
- if(settings.useFlex && module.is.ie()) {
- module.debug('useFlex true is not supported in IE');
- } else if(settings.useFlex && !settings.detachable) {
- module.debug('useFlex true in combination with detachable false is not supported');
- }
- return settings.useFlex;
- },
- fit: function() {
- var
- contextHeight = module.cache.contextHeight,
- verticalCenter = module.cache.contextHeight / 2,
- topOffset = module.cache.topOffset,
- scrollHeight = module.cache.scrollHeight,
- height = module.cache.height,
- paddingHeight = settings.padding,
- startPosition = (verticalCenter + topOffset)
- ;
- return (scrollHeight > height)
- ? (startPosition + scrollHeight + paddingHeight < contextHeight)
- : (height + (paddingHeight * 2) < contextHeight)
- ;
- }
- },
-
- is: {
- active: function() {
- return $module.hasClass(className.active);
- },
- ie: function() {
- if(module.cache.isIE === undefined) {
- var
- isIE11 = (!(window.ActiveXObject) && 'ActiveXObject' in window),
- isIE = ('ActiveXObject' in window)
- ;
- module.cache.isIE = (isIE11 || isIE);
- }
- return module.cache.isIE;
- },
- animating: function() {
- return $module.transition('is supported')
- ? $module.transition('is animating')
- : $module.is(':visible')
- ;
- },
- scrolling: function() {
- return $dimmable.hasClass(className.scrolling);
- },
- modernBrowser: function() {
- // appName for IE11 reports 'Netscape' can no longer use
- return !(window.ActiveXObject || 'ActiveXObject' in window);
- },
- rtl: function() {
- if(module.cache.isRTL === undefined) {
- module.cache.isRTL = $body.attr('dir') === 'rtl' || $body.css('direction') === 'rtl';
- }
- return module.cache.isRTL;
- },
- safari: function() {
- if(module.cache.isSafari === undefined) {
- module.cache.isSafari = /constructor/i.test(window.HTMLElement) || !!window.ApplePaySession;
- }
- return module.cache.isSafari;
- },
- edge: function(){
- if(module.cache.isEdge === undefined) {
- module.cache.isEdge = !!window.setImmediate && !module.is.ie();
- }
- return module.cache.isEdge;
- },
- firefox: function(){
- if(module.cache.isFirefox === undefined) {
- module.cache.isFirefox = !!window.InstallTrigger;
- }
- return module.cache.isFirefox;
- },
- iframe: function() {
- return !(self === top);
- }
- },
-
- set: {
- autofocus: function() {
- var
- $inputs = $module.find('[tabindex], :input').filter(':visible').filter(function() {
- return $(this).closest('.disabled').length === 0;
- }),
- $autofocus = $inputs.filter('[autofocus]'),
- $input = ($autofocus.length > 0)
- ? $autofocus.first()
- : $inputs.first()
- ;
- if($input.length > 0) {
- $input.focus();
- }
- },
- bodyMargin: function() {
- var position = module.can.leftBodyScrollbar() ? 'left':'right';
- if(settings.detachable || module.can.fit()) {
- $body.css('margin-'+position, tempBodyMargin + 'px');
- }
- $body.find(selector.bodyFixed.replace('right',position)).css('padding-'+position, tempBodyMargin + 'px');
- },
- clickaway: function() {
- if (!settings.detachable) {
- $module
- .on('mousedown' + elementEventNamespace, module.event.mousedown)
- ;
- }
- $dimmer
- .on('mousedown' + elementEventNamespace, module.event.mousedown)
- ;
- $dimmer
- .on('mouseup' + elementEventNamespace, module.event.mouseup)
- ;
- },
- dimmerSettings: function() {
- if($.fn.dimmer === undefined) {
- module.error(error.dimmer);
- return;
- }
- var
- defaultSettings = {
- debug : settings.debug,
- dimmerName : 'modals',
- closable : 'auto',
- useFlex : module.can.useFlex(),
- duration : {
- show : settings.duration,
- hide : settings.duration
- }
- },
- dimmerSettings = $.extend(true, defaultSettings, settings.dimmerSettings)
- ;
- if(settings.inverted) {
- dimmerSettings.variation = (dimmerSettings.variation !== undefined)
- ? dimmerSettings.variation + ' inverted'
- : 'inverted'
- ;
- }
- $context.dimmer('setting', dimmerSettings);
- },
- dimmerStyles: function() {
- if(settings.inverted) {
- $dimmer.addClass(className.inverted);
- }
- else {
- $dimmer.removeClass(className.inverted);
- }
- if(settings.blurring) {
- $dimmable.addClass(className.blurring);
- }
- else {
- $dimmable.removeClass(className.blurring);
- }
- },
- modalOffset: function() {
- if (!settings.detachable) {
- var canFit = module.can.fit();
- $module
- .css({
- top: (!$module.hasClass('aligned') && canFit)
- ? $(document).scrollTop() + (module.cache.contextHeight - module.cache.height) / 2
- : !canFit || $module.hasClass('top')
- ? $(document).scrollTop() + settings.padding
- : $(document).scrollTop() + (module.cache.contextHeight - module.cache.height - settings.padding),
- marginLeft: -(module.cache.width / 2)
- })
- ;
- } else {
- $module
- .css({
- marginTop: (!$module.hasClass('aligned') && module.can.fit())
- ? -(module.cache.height / 2)
- : settings.padding / 2,
- marginLeft: -(module.cache.width / 2)
- })
- ;
- }
- module.verbose('Setting modal offset for legacy mode');
- },
- screenHeight: function() {
- if( module.can.fit() ) {
- $body.css('height', '');
- }
- else if(!$module.hasClass('bottom')) {
- module.debug('Modal is taller than page content, resizing page height');
- $body
- .css('height', module.cache.height + (settings.padding * 2) )
- ;
- }
- },
- active: function() {
- $module.addClass(className.active + ' ' + className.front);
- $otherModals.filter('.' + className.active).removeClass(className.front);
- },
- scrolling: function() {
- $dimmable.addClass(className.scrolling);
- $module.addClass(className.scrolling);
- module.unbind.scrollLock();
- },
- legacy: function() {
- $module.addClass(className.legacy);
- },
- type: function() {
- if(module.can.fit()) {
- module.verbose('Modal fits on screen');
- if(!module.others.active() && !module.others.animating()) {
- module.remove.scrolling();
- module.bind.scrollLock();
- }
- }
- else if (!$module.hasClass('bottom')){
- module.verbose('Modal cannot fit on screen setting to scrolling');
- module.set.scrolling();
- } else {
- module.verbose('Bottom aligned modal not fitting on screen is unsupported for scrolling');
- }
- },
- undetached: function() {
- $dimmable.addClass(className.undetached);
- }
- },
-
- setting: function(name, value) {
- module.debug('Changing setting', name, value);
- if( $.isPlainObject(name) ) {
- $.extend(true, settings, name);
- }
- else if(value !== undefined) {
- if($.isPlainObject(settings[name])) {
- $.extend(true, settings[name], value);
- }
- else {
- settings[name] = value;
- }
- }
- else {
- return settings[name];
- }
- },
- internal: function(name, value) {
- if( $.isPlainObject(name) ) {
- $.extend(true, module, name);
- }
- else if(value !== undefined) {
- module[name] = value;
- }
- else {
- return module[name];
- }
- },
- debug: function() {
- if(!settings.silent && settings.debug) {
- if(settings.performance) {
- module.performance.log(arguments);
- }
- else {
- module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
- module.debug.apply(console, arguments);
- }
- }
- },
- verbose: function() {
- if(!settings.silent && settings.verbose && settings.debug) {
- if(settings.performance) {
- module.performance.log(arguments);
- }
- else {
- module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
- module.verbose.apply(console, arguments);
- }
- }
- },
- error: function() {
- if(!settings.silent) {
- module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
- module.error.apply(console, arguments);
- }
- },
- performance: {
- log: function(message) {
- var
- currentTime,
- executionTime,
- previousTime
- ;
- if(settings.performance) {
- currentTime = new Date().getTime();
- previousTime = time || currentTime;
- executionTime = currentTime - previousTime;
- time = currentTime;
- performance.push({
- 'Name' : message[0],
- 'Arguments' : [].slice.call(message, 1) || '',
- 'Element' : element,
- 'Execution Time' : executionTime
- });
- }
- clearTimeout(module.performance.timer);
- module.performance.timer = setTimeout(module.performance.display, 500);
- },
- display: function() {
- var
- title = settings.name + ':',
- totalTime = 0
- ;
- time = false;
- clearTimeout(module.performance.timer);
- $.each(performance, function(index, data) {
- totalTime += data['Execution Time'];
- });
- title += ' ' + totalTime + 'ms';
- if(moduleSelector) {
- title += ' \'' + moduleSelector + '\'';
- }
- if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
- console.groupCollapsed(title);
- if(console.table) {
- console.table(performance);
- }
- else {
- $.each(performance, function(index, data) {
- console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
- });
- }
- console.groupEnd();
- }
- performance = [];
- }
- },
- invoke: function(query, passedArguments, context) {
- var
- object = instance,
- maxDepth,
- found,
- response
- ;
- passedArguments = passedArguments || queryArguments;
- context = element || context;
- if(typeof query == 'string' && object !== undefined) {
- query = query.split(/[\. ]/);
- maxDepth = query.length - 1;
- $.each(query, function(depth, value) {
- var camelCaseValue = (depth != maxDepth)
- ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
- : query
- ;
- if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
- object = object[camelCaseValue];
- }
- else if( object[camelCaseValue] !== undefined ) {
- found = object[camelCaseValue];
- return false;
- }
- else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
- object = object[value];
- }
- else if( object[value] !== undefined ) {
- found = object[value];
- return false;
- }
- else {
- return false;
- }
- });
- }
- if ( $.isFunction( found ) ) {
- response = found.apply(context, passedArguments);
- }
- else if(found !== undefined) {
- response = found;
- }
- if(Array.isArray(returnedValue)) {
- returnedValue.push(response);
- }
- else if(returnedValue !== undefined) {
- returnedValue = [returnedValue, response];
- }
- else if(response !== undefined) {
- returnedValue = response;
- }
- return found;
- }
- };
-
- if(methodInvoked) {
- if(instance === undefined) {
- module.initialize();
- }
- module.invoke(query);
- }
- else {
- if(instance !== undefined) {
- instance.invoke('destroy');
- }
- module.initialize();
- }
- })
- ;
-
- return (returnedValue !== undefined)
- ? returnedValue
- : this
- ;
-};
-
-$.fn.modal.settings = {
-
- name : 'Modal',
- namespace : 'modal',
-
- useFlex : 'auto',
- offset : 0,
-
- silent : false,
- debug : false,
- verbose : false,
- performance : true,
-
- observeChanges : false,
-
- allowMultiple : false,
- detachable : true,
- closable : true,
- autofocus : true,
- restoreFocus : true,
-
- inverted : false,
- blurring : false,
-
- centered : true,
-
- dimmerSettings : {
- closable : false,
- useCSS : true
- },
-
- // whether to use keyboard shortcuts
- keyboardShortcuts: true,
-
- context : 'body',
-
- queue : false,
- duration : 500,
- transition : 'scale',
-
- // padding with edge of page
- padding : 50,
- scrollbarWidth: 10,
-
- // called before show animation
- onShow : function(){},
-
- // called after show animation
- onVisible : function(){},
-
- // called before hide animation
- onHide : function(){ return true; },
-
- // called after hide animation
- onHidden : function(){},
-
- // called after approve selector match
- onApprove : function(){ return true; },
-
- // called after deny selector match
- onDeny : function(){ return true; },
-
- selector : {
- close : '.close-modal',
- approve : '.actions .positive, .actions .approve, .actions .ok',
- deny : '.actions .negative, .actions .deny, .actions .cancel',
- modal : '.ui.modal',
- dimmer : '> .ui.dimmer',
- bodyFixed: '> .ui.fixed.menu, > .ui.right.toast-container, > .ui.right.sidebar'
- },
- error : {
- dimmer : 'UI Dimmer, a required component is not included in this page',
- method : 'The method you called is not defined.',
- notFound : 'The element you specified could not be found'
- },
- className : {
- active : 'active',
- animating : 'animating',
- blurring : 'blurring',
- inverted : 'inverted',
- legacy : 'legacy',
- loading : 'loading',
- scrolling : 'scrolling',
- undetached : 'undetached',
- front : 'front'
- }
-};
-
-
-})( jQuery, window, document );
diff --git a/web_src/fomantic/build/fomantic.js b/web_src/fomantic/build/fomantic.js
index 8cc8652482b..484a8a23b4c 100644
--- a/web_src/fomantic/build/fomantic.js
+++ b/web_src/fomantic/build/fomantic.js
@@ -1,5 +1,4 @@
import './components/api.js';
import './components/dropdown.js';
-import './components/modal.js';
// Hard-forked from Fomantic UI 2.8.7, patches are commented with "GITEA-PATCH"
diff --git a/web_src/js/features/admin/common.ts b/web_src/js/features/admin/common.ts
index 1b237cda7b9..d58aab58f6e 100644
--- a/web_src/js/features/admin/common.ts
+++ b/web_src/js/features/admin/common.ts
@@ -1,7 +1,7 @@
import {checkAppUrl} from '../common-page.ts';
import {hideElem, queryElems, showElem, toggleElem} from '../../utils/dom.ts';
import {POST} from '../../modules/fetch.ts';
-import {showFomanticModal} from '../../modules/fomantic/modal.ts';
+import {showModal} from '../../modules/modal.ts';
import {pathEscape} from '../../utils/url.ts';
import {registerGlobalInitFunc} from '../../modules/observer.ts';
@@ -274,7 +274,7 @@ function initAdminNotice() {
const pageContent = document.querySelector('.page-content.admin.notice');
if (!pageContent) return;
- const detailModal = document.querySelector('#detail-modal')!;
+ const detailModal = document.querySelector('#detail-modal')!;
// Attach view detail modals
queryElems(pageContent, '.view-detail', (el) => el.addEventListener('click', (e) => {
@@ -282,7 +282,7 @@ function initAdminNotice() {
const elNoticeDesc = el.closest('tr')!.querySelector('.notice-description')!;
const elModalDesc = detailModal.querySelector('.content pre')!;
elModalDesc.textContent = elNoticeDesc.textContent;
- showFomanticModal(detailModal);
+ showModal(detailModal);
}));
// Select actions
diff --git a/web_src/js/features/common-button.ts b/web_src/js/features/common-button.ts
index 0b2b1f46715..5d8183ba7ea 100644
--- a/web_src/js/features/common-button.ts
+++ b/web_src/js/features/common-button.ts
@@ -1,5 +1,5 @@
import {addDelegatedEventListener, hideElem, isElemVisible, showElem, toggleElem} from '../utils/dom.ts';
-import {showFomanticModal} from '../modules/fomantic/modal.ts';
+import {showModal} from '../modules/modal.ts';
import {camelize} from 'vue';
import {applyAutoFocus} from './common-page.ts';
@@ -90,7 +90,7 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) {
// to make the template variables get correctly escaped in the URL.
e.preventDefault();
const modalSelector = el.getAttribute('data-modal')!;
- const elModal = document.querySelector(modalSelector);
+ const elModal = document.querySelector(modalSelector);
if (!elModal) throw new Error('no modal for this action');
const modalAttrPrefix = 'data-modal-';
@@ -123,11 +123,11 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) {
}
}
- showFomanticModal(elModal);
+ showModal(elModal);
}
export function initGlobalButtons(): void {
- // There are many "cancel button" elements in modal dialogs, Fomantic UI expects they are button-like elements but never submit a form.
+ // There are many "cancel button" elements in modal dialogs, they are expected to be button-like elements but never submit a form.
// However, Gitea misuses the modal dialog and put the cancel buttons inside forms, so we must prevent the form submission.
// There are a few cancel buttons in non-modal forms, and there are some dynamically created forms (eg: the "Edit Issue Content")
addDelegatedEventListener(document, 'click', 'form button.ui.cancel.button', (_ /* el */, e) => e.preventDefault());
diff --git a/web_src/js/features/common-page.ts b/web_src/js/features/common-page.ts
index e4344ef5cfc..268dc6b5c41 100644
--- a/web_src/js/features/common-page.ts
+++ b/web_src/js/features/common-page.ts
@@ -126,7 +126,7 @@ function autoFocusEnd(el: HTMLInputElement | HTMLTextAreaElement) {
export function applyAutoFocus(container: Element) {
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/autofocus
// "autofocus" behavior is defined by the standard: when a container (e.g.: dialog) becomes visible, focus the element with "autofocus" attribute
- // Fomantic UI already supports it for its modal dialog, we need to cover more cases (e.g.: ".show-panel" button)
+ // The native modal dialog already supports it, we need to cover more cases (e.g.: ".show-panel" button)
// Here is just a simple support, we don't expect more than one element that need "autofocus" appearing in the same container
container.querySelector('[autofocus]')?.focus();
// Also, apply our autoFocusEnd behavior
diff --git a/web_src/js/features/comp/ConfirmModal.ts b/web_src/js/features/comp/ConfirmModal.ts
index 2e4a35f2213..e6d2d6126f8 100644
--- a/web_src/js/features/comp/ConfirmModal.ts
+++ b/web_src/js/features/comp/ConfirmModal.ts
@@ -1,8 +1,7 @@
import {svg} from '../../svg.ts';
import {html, htmlRaw} from '../../utils/html.ts';
import {createElementFromHTML} from '../../utils/dom.ts';
-import {showFomanticModal} from '../../modules/fomantic/modal.ts';
-import {hideToastsAll} from '../../modules/toast.ts';
+import {showModal} from '../../modules/modal.ts';
const {i18n} = window.config;
@@ -12,27 +11,24 @@ type ConfirmModalOptions = {
confirmButtonColor?: 'primary' | 'red' | 'green' | 'blue';
};
-export function createConfirmModal({header = '', content = '', confirmButtonColor = 'primary'}:ConfirmModalOptions = {}): HTMLElement {
+export function createConfirmModal({header = '', content = '', confirmButtonColor = 'primary'}:ConfirmModalOptions = {}): HTMLDialogElement {
const headerHtml = header ? html`` : '';
return createElementFromHTML(html`
-
+
+
`.trim());
}
-export function confirmModal(modal: HTMLElement | ConfirmModalOptions): Promise {
- if (!(modal instanceof HTMLElement)) modal = createConfirmModal(modal);
- // hide existing toasts when we need to show a new modal, otherwise the toasts only interfere the UI
- // it's fine to do so because the modal is triggered by user's explicit action, so the user should already have read the toast messages
- hideToastsAll();
+export function confirmModal(modal: HTMLDialogElement | ConfirmModalOptions): Promise {
+ if (!(modal instanceof HTMLDialogElement)) modal = createConfirmModal(modal);
return new Promise((resolve) => {
- showFomanticModal(modal, {
+ showModal(modal, {
onApprove() {
resolve(true);
},
diff --git a/web_src/js/features/comp/LabelEdit.ts b/web_src/js/features/comp/LabelEdit.ts
index e10863e94ff..bc9c47be00d 100644
--- a/web_src/js/features/comp/LabelEdit.ts
+++ b/web_src/js/features/comp/LabelEdit.ts
@@ -1,5 +1,5 @@
import {queryElems, toggleElem} from '../../utils/dom.ts';
-import {showFomanticModal} from '../../modules/fomantic/modal.ts';
+import {showModal} from '../../modules/modal.ts';
import {submitFormFetchAction} from '../../modules/fetch-action.ts';
import {registerGlobalInitFunc} from '../../modules/observer.ts';
@@ -7,7 +7,7 @@ function nameHasScope(name: string): boolean {
return /.*[^/]\/[^/].*/.test(name);
}
-function initLabelEditModal(elModal: HTMLElement) {
+function initLabelEditModal(elModal: HTMLDialogElement) {
const elLabelId = elModal.querySelector('input[name="id"]')!;
const elNameInput = elModal.querySelector('.label-name-input')!;
const elExclusiveField = elModal.querySelector('.label-exclusive-input-field')!;
@@ -59,7 +59,7 @@ function initLabelEditModal(elModal: HTMLElement) {
form.action = isEdit ? `${curPageLink}/edit` : `${curPageLink}/new`;
toggleElem(elIsArchivedField, isEdit);
syncModalUi();
- showFomanticModal(elModal, {
+ showModal(elModal, {
onApprove() {
if (!form.checkValidity()) {
form.reportValidity();
diff --git a/web_src/js/features/repo-branch.ts b/web_src/js/features/repo-branch.ts
index 837ed79b3ba..16755c3561f 100644
--- a/web_src/js/features/repo-branch.ts
+++ b/web_src/js/features/repo-branch.ts
@@ -1,5 +1,5 @@
import {toggleElem} from '../utils/dom.ts';
-import {showFomanticModal} from '../modules/fomantic/modal.ts';
+import {showModal} from '../modules/modal.ts';
import {trString} from '../modules/i18n.ts';
import {registerGlobalEventFunc} from '../modules/observer.ts';
@@ -20,7 +20,7 @@ function initRepoCreateBranchButton() {
const fromSpanName = el.getAttribute('data-modal-from-span') || '#modal-create-branch-from-span';
document.querySelector(fromSpanName)!.textContent = el.getAttribute('data-branch-from');
- showFomanticModal(document.querySelector(el.getAttribute('data-modal')!));
+ showModal(document.querySelector(el.getAttribute('data-modal')!)!);
});
}
}
diff --git a/web_src/js/features/repo-issue-content.ts b/web_src/js/features/repo-issue-content.ts
index 5665da6fe10..ce73509b993 100644
--- a/web_src/js/features/repo-issue-content.ts
+++ b/web_src/js/features/repo-issue-content.ts
@@ -4,7 +4,8 @@ import {GET, POST} from '../modules/fetch.ts';
import {createElementFromHTML, showElem} from '../utils/dom.ts';
import {parseIssuePageInfo} from '../utils.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
-import {hideFomanticModal, showFomanticModal} from '../modules/fomantic/modal.ts';
+import {hideModal, showModal} from '../modules/modal.ts';
+import {confirmModal} from './comp/ConfirmModal.ts';
import {html, htmlRaw} from '../utils/html.ts';
import type {JQueryElem} from '../types.ts';
@@ -14,8 +15,8 @@ let i18nTextDeleteFromHistory: string;
let i18nTextDeleteFromHistoryConfirm: string;
function showContentHistoryDetail(issueBaseUrl: string, commentId: string, historyId: string, itemTitleHtml: string) {
- const elDetailDialog = createElementFromHTML(html`
-
+ const elDetailDialog = createElementFromHTML
(html`
+
`);
- document.body.append(elDetailDialog);
const elOptionsDropdown = elDetailDialog.querySelector('.ui.dropdown.dialog-header-options')!;
const $fomanticDropdownOptions = fomanticQuery(elOptionsDropdown);
$fomanticDropdownOptions.dropdown({
@@ -39,8 +39,7 @@ function showContentHistoryDetail(issueBaseUrl: string, commentId: string, histo
async onChange(_value: string, _text: string, $item: JQueryElem) {
const optionItem = $item.data('option-item');
if (optionItem === 'delete') {
- // TODO: to use confirmModal instead of window.confirm, need to support multiple modals on the same page first.
- if (window.confirm(i18nTextDeleteFromHistoryConfirm)) {
+ if (await confirmModal({content: i18nTextDeleteFromHistoryConfirm, confirmButtonColor: 'red'})) {
try {
const params = new URLSearchParams();
params.append('comment_id', commentId);
@@ -50,7 +49,7 @@ function showContentHistoryDetail(issueBaseUrl: string, commentId: string, histo
const resp = await response.json();
if (resp.ok) {
- hideFomanticModal(elDetailDialog);
+ hideModal(elDetailDialog);
} else {
showErrorToast(resp.message);
}
@@ -67,7 +66,7 @@ function showContentHistoryDetail(issueBaseUrl: string, commentId: string, histo
$fomanticDropdownOptions.dropdown('clear', true);
},
});
- showFomanticModal(elDetailDialog, {
+ showModal(elDetailDialog, {
async onShow() {
try {
const params = new URLSearchParams();
diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts
index c105494fd1e..a2f957dac6d 100644
--- a/web_src/js/features/repo-issue.ts
+++ b/web_src/js/features/repo-issue.ts
@@ -14,7 +14,7 @@ import {ComboMarkdownEditor, getComboMarkdownEditor, initComboMarkdownEditor} fr
import {GET, POST} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
-import {showFomanticModal} from '../modules/fomantic/modal.ts';
+import {showModal} from '../modules/modal.ts';
import {ignoreAreYouSure} from '../modules/are-you-sure.ts';
import {registerGlobalEventFunc, registerGlobalInitFunc} from '../modules/observer.ts';
import type {FomanticApiResponse} from '../types.ts';
@@ -325,10 +325,10 @@ export function initRepoIssueReferenceIssue() {
const poster = el.getAttribute('data-poster-username');
const reference = el.getAttribute('data-reference')!;
const modalSelector = el.getAttribute('data-modal')!;
- const modal = document.querySelector(modalSelector)!;
+ const modal = document.querySelector(modalSelector)!;
const textarea = modal.querySelector('textarea[name="content"]')!;
textarea.value = `${content}\n\n_Originally posted by @${poster} in ${reference}_`;
- showFomanticModal(modal);
+ showModal(modal);
});
}
diff --git a/web_src/js/features/repo-projects.ts b/web_src/js/features/repo-projects.ts
index eefbad75c04..ddcca73c11a 100644
--- a/web_src/js/features/repo-projects.ts
+++ b/web_src/js/features/repo-projects.ts
@@ -2,7 +2,7 @@ import {contrastColor} from '../utils/color.ts';
import {createSortable} from '../modules/sortable.ts';
import {POST} from '../modules/fetch.ts';
import {performFetchActionRequest} from '../modules/fetch-action.ts';
-import {hideFomanticModal} from '../modules/fomantic/modal.ts';
+import {hideModal} from '../modules/modal.ts';
import {queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts';
import type {SortableEvent} from 'sortablejs';
import {toggleFullScreen} from '../utils.ts';
@@ -82,7 +82,7 @@ function initRepoProjectSortable(): void {
}
function initRepoProjectColumnEdit(writableProjectBoard: Element): void {
- const elModal = document.querySelector('.ui.modal#project-column-modal-edit')!;
+ const elModal = document.querySelector('#project-column-modal-edit')!;
const elForm = elModal.querySelector('form')!;
const elColumnId = elForm.querySelector('input[name="id"]')!;
@@ -140,7 +140,7 @@ function initRepoProjectColumnEdit(writableProjectBoard: Element): void {
queryElemChildren(elBoardColumn, '.divider', (divider: HTMLElement) => divider.style.removeProperty('color'));
}
- hideFomanticModal(elModal);
+ hideModal(elModal);
} finally {
elForm.classList.remove('is-loading');
}
diff --git a/web_src/js/features/repo-release.ts b/web_src/js/features/repo-release.ts
index 690911d9ed6..778e933fd98 100644
--- a/web_src/js/features/repo-release.ts
+++ b/web_src/js/features/repo-release.ts
@@ -3,7 +3,7 @@ import {hideToastsAll, showErrorToast} from '../modules/toast.ts';
import {getComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
import {hideElem} from '../utils/dom.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
-import {hideFomanticModal, showFomanticModal} from '../modules/fomantic/modal.ts';
+import {hideModal, showModal} from '../modules/modal.ts';
import {registerGlobalEventFunc, registerGlobalInitFunc} from '../modules/observer.ts';
import {htmlEscape} from '../utils/html.ts';
import {compareVersions} from 'compare-versions';
@@ -84,7 +84,7 @@ function initGenerateReleaseNotes(elForm: HTMLFormElement) {
const textMissingTag = buttonShowModal.getAttribute('data-text-missing-tag')!;
const generateUrl = buttonShowModal.getAttribute('data-generate-url')!;
- const elModal = document.querySelector('#generate-release-notes-modal')!;
+ const elModal = document.querySelector('#generate-release-notes-modal')!;
const doSubmit = async (tagName: string) => {
const elPreviousTag = elModal.querySelector('[name=previous_tag]')!;
@@ -111,18 +111,18 @@ function initGenerateReleaseNotes(elForm: HTMLFormElement) {
} else {
comboEditor.value(data.content);
}
+ hideModal(elModal);
+ comboEditor.focus();
} finally {
elModal.classList.remove('loading', 'disabled');
- hideFomanticModal(elModal);
- comboEditor.focus();
}
};
let inited = false;
const doShowModal = () => {
- hideToastsAll();
const tagName = tagNameInput.value.trim();
if (!tagName) {
+ hideToastsAll();
showErrorToast(textMissingTag, {duration: 3000});
tagNameInput.focus();
return;
@@ -140,7 +140,7 @@ function initGenerateReleaseNotes(elForm: HTMLFormElement) {
}
$dropdown.dropdown('set selected', guessPreviousReleaseTag(tagName, existingTags));
- showFomanticModal(elModal, {
+ showModal(elModal, {
onApprove: () => {
doSubmit(tagName); // don't await, need to return false to keep the modal
return false;
diff --git a/web_src/js/globals.d.ts b/web_src/js/globals.d.ts
index 504c75e6feb..e9e357d7507 100644
--- a/web_src/js/globals.d.ts
+++ b/web_src/js/globals.d.ts
@@ -1,12 +1,9 @@
interface JQuery {
fomanticExt: {
onDropdownAfterFiltered?: (this: HTMLElement) => void,
- onModalBeforeHidden?: (this: HTMLElement) => void,
}; // fomantic extension
api: any, // fomantic
- dimmer: any, // fomantic
dropdown: any; // fomantic
- modal: any; // fomantic
transition: any, // fomantic
}
diff --git a/web_src/js/index.ts b/web_src/js/index.ts
index 853e410530f..ca896ea7641 100644
--- a/web_src/js/index.ts
+++ b/web_src/js/index.ts
@@ -69,11 +69,13 @@ import {initDevtest} from './modules/devtest.ts';
import {initRepoWatch} from './features/repo-watch.ts';
import {initPackagesView} from './features/packages.ts';
import {initAsyncHashChangeListeners} from './modules/hashchange.ts';
+import {initGlobalModal} from './modules/modal.ts';
const initStartTime = performance.now();
const initPerformanceTracer = callInitFunctions([
initGiteaFomantic,
+ initGlobalModal,
initGlobalComponent,
initGlobalDropdown,
initGlobalFetchAction,
diff --git a/web_src/js/modules/devtest.ts b/web_src/js/modules/devtest.ts
index f4f225c4037..6d7fe3fcfee 100644
--- a/web_src/js/modules/devtest.ts
+++ b/web_src/js/modules/devtest.ts
@@ -1,6 +1,6 @@
import {showSuccessToast, showInfoToast, showWarningToast, showErrorToast} from './toast.ts';
import {registerGlobalInitFunc} from './observer.ts';
-import {showFomanticModal} from './fomantic/modal.ts';
+import {showModal} from './modal.ts';
import {createElementFromHTML} from '../utils/dom.ts';
import {html} from '../utils/html.ts';
import {showGlobalErrorMessage} from './errors.ts';
@@ -25,9 +25,9 @@ function initDevtestPage() {
const modalButtons = document.querySelector('.modal-buttons');
if (modalButtons) {
- for (const el of document.querySelectorAll('.ui.modal:not([data-skip-button])')) {
+ for (const el of document.querySelectorAll('.modal-dialog:not([data-skip-button])')) {
const btn = createElementFromHTML(html` showFomanticModal(el));
+ btn.addEventListener('click', () => showModal(el));
modalButtons.append(btn);
}
}
diff --git a/web_src/js/modules/fetch-action.ts b/web_src/js/modules/fetch-action.ts
index 0e47000fd32..58b2e14a21f 100644
--- a/web_src/js/modules/fetch-action.ts
+++ b/web_src/js/modules/fetch-action.ts
@@ -231,10 +231,10 @@ export async function submitFormFetchAction(elForm: HTMLFormElement, opts: Submi
}
async function confirmFetchAction(el: HTMLElement) {
- let elModal: HTMLElement | null = null;
+ let elModal: HTMLDialogElement | null = null;
const dataModalConfirm = el.getAttribute('data-modal-confirm') || '';
if (dataModalConfirm.startsWith('#') && dataModalConfirm.length > 1) {
- elModal = document.querySelector(`#${CSS.escape(dataModalConfirm.substring(1))}`);
+ elModal = document.querySelector(`#${CSS.escape(dataModalConfirm.substring(1))}`);
if (elModal) {
elModal = createElementFromHTML(elModal.outerHTML);
elModal.removeAttribute('id');
diff --git a/web_src/js/modules/fomantic.ts b/web_src/js/modules/fomantic.ts
index 84283bd4d66..4cc2fd5c566 100644
--- a/web_src/js/modules/fomantic.ts
+++ b/web_src/js/modules/fomantic.ts
@@ -1,7 +1,5 @@
import {initAriaDropdownPatch} from './fomantic/dropdown.ts';
-import {initAriaModalPatch} from './fomantic/modal.ts';
import {initFomanticTransition} from './fomantic/transition.ts';
-import {initFomanticDimmer} from './fomantic/dimmer.ts';
import {svg} from '../svg.ts';
export const fomanticMobileScreen = window.matchMedia('only screen and (max-width: 767.98px)');
@@ -20,9 +18,7 @@ export function initGiteaFomantic() {
};
initFomanticTransition();
- initFomanticDimmer();
// Use the patches to improve accessibility, these patches are designed to be as independent as possible, make it easy to modify or remove in the future.
initAriaDropdownPatch();
- initAriaModalPatch();
}
diff --git a/web_src/js/modules/fomantic/dimmer.ts b/web_src/js/modules/fomantic/dimmer.ts
deleted file mode 100644
index 6782f0137d9..00000000000
--- a/web_src/js/modules/fomantic/dimmer.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import {queryElemChildren} from '../../utils/dom.ts';
-
-export function initFomanticDimmer() {
- // stand-in for removed dimmer module
- $.fn.dimmer = function (this: any, arg0: string, arg1: any) {
- if (arg0 === 'add content') {
- const $el = arg1;
- const existingDimmer = document.querySelector('body > .ui.dimmer');
- if (existingDimmer) {
- queryElemChildren(existingDimmer, '*', (el) => el.classList.add('hidden'));
- this._dimmer = existingDimmer;
- } else {
- this._dimmer = document.createElement('div');
- this._dimmer.classList.add('ui', 'dimmer');
- document.body.append(this._dimmer);
- }
- this._dimmer.append($el[0]);
- } else if (arg0 === 'get dimmer') {
- return $(this._dimmer);
- } else if (arg0 === 'show') {
- this._dimmer.classList.add('active');
- document.body.classList.add('tw-overflow-hidden');
- } else if (arg0 === 'hide') {
- const cb = arg1;
- this._dimmer.classList.remove('active');
- document.body.classList.remove('tw-overflow-hidden');
- cb();
- }
- return this;
- };
-}
diff --git a/web_src/js/modules/fomantic/modal.ts b/web_src/js/modules/fomantic/modal.ts
deleted file mode 100644
index 75f68bddd95..00000000000
--- a/web_src/js/modules/fomantic/modal.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import type {FomanticInitFunction, JQueryElem} from '../../types.ts';
-import {queryElems} from '../../utils/dom.ts';
-import {hideToastsFrom} from '../toast.ts';
-
-type ModalOpts = {
- closable?: boolean;
- onApprove?: (this: HTMLElement) => boolean | void;
- onShow?: (this: HTMLElement) => void | Promise;
- onHide?: (this: HTMLElement) => void;
- onHidden?: (this: HTMLElement) => void;
-};
-
-// thin wrapper around Fomantic's jQuery modal plugin so callers don't have to touch jQuery or fomanticQuery
-export function showFomanticModal(el: Element | null, opts: ModalOpts = {}) {
- if (!el) return;
- const $el = $(el);
- if (Object.keys(opts).length) $el.modal(opts);
- $el.modal('show');
-}
-
-export function hideFomanticModal(el: Element | null) {
- if (!el) return;
- $(el).modal('hide');
-}
-
-const fomanticModalFn = $.fn.modal;
-
-// use our own `$.fn.modal` to patch Fomantic's modal module
-export function initAriaModalPatch() {
- if ($.fn.modal === ariaModalFn) throw new Error('initAriaModalPatch could only be called once');
- $.fn.modal = ariaModalFn;
- (ariaModalFn as FomanticInitFunction).settings = fomanticModalFn.settings;
- $.fn.fomanticExt.onModalBeforeHidden = onModalBeforeHidden;
- $.fn.modal.settings.onApprove = onModalApproveDefault;
-}
-
-// the patched `$.fn.modal` modal function
-// * it does the one-time attaching on the first call
-function ariaModalFn(this: JQueryElem, ...args: Parameters) {
- const ret = fomanticModalFn.apply(this, args);
- if (args[0] === 'show' || args[0]?.autoShow) {
- for (const el of this) {
- el.querySelector('.close-modal')?.setAttribute('aria-label', 'Close');
- // If there is a form in the modal, there might be a "cancel" button before "ok" button (all buttons are "type=submit" by default).
- // In such case, the "Enter" key will trigger the "cancel" button instead of "ok" button, then the dialog will be closed.
- // It breaks the user experience - the "Enter" key should confirm the dialog and submit the form.
- // So, all "cancel" buttons without "[type]" must be marked as "type=button".
- // Although there are lint rules for "button type", some HTML code is generated in JS/Vue/Go which isn't linted, so we still need this patch.
- for (const button of el.querySelectorAll('form button.cancel:not([type])')) {
- button.setAttribute('type', 'button');
- }
- }
- }
- return ret;
-}
-
-function onModalBeforeHidden(this: HTMLElement) {
- const $modal = $(this);
- const elModal = $modal[0];
- hideToastsFrom(elModal.closest('.ui.dimmer') ?? document.body);
-
- // reset the form after the modal is hidden, after other modal events and handlers (e.g. "onApprove", form submit)
- setTimeout(() => {
- queryElems(elModal, 'form', (form: HTMLFormElement) => form.reset());
- }, 0);
-}
-
-function onModalApproveDefault(this: HTMLElement) {
- const $modal = $(this);
- const selectors = $modal.modal('setting', 'selector');
- const elModal = $modal[0];
- const elApprove = elModal.querySelector(selectors.approve);
- const elForm = elApprove?.closest('form');
- if (!elForm) return true; // no form, just allow closing the modal
-
- // "form-fetch-action" can handle network errors gracefully,
- // so keep the modal dialog to make users can re-submit the form if anything wrong happens.
- if (elForm.matches('.form-fetch-action')) return false;
-
- // There is an abuse for the "modal" + "form" combination, the "Approve" button is a traditional form submit button in the form.
- // Then "approve" and "submit" occur at the same time, the modal will be closed immediately before the form is submitted.
- // So here we prevent the modal from closing automatically by returning false, add the "is-loading" class to the form element.
- if (!elForm.reportValidity()) return false;
- elForm.classList.add('is-loading');
- return false;
-}
diff --git a/web_src/js/modules/fomantic/transition.ts b/web_src/js/modules/fomantic/transition.ts
index 2cb02126683..f5c62e58d9a 100644
--- a/web_src/js/modules/fomantic/transition.ts
+++ b/web_src/js/modules/fomantic/transition.ts
@@ -1,12 +1,6 @@
export function initFomanticTransition() {
- const transitionNopBehaviors = new Set([
- 'clear queue', 'stop', 'stop all', 'destroy',
- 'force repaint', 'repaint', 'reset',
- 'looping', 'remove looping', 'disable', 'enable',
- 'set duration', 'save conditions', 'restore conditions',
- ]);
// stand-in for removed transition module
- $.fn.transition = function (arg0: any, arg1?: number, arg2?: (this: HTMLElement) => void) {
+ $.fn.transition = function (arg0: any) {
if (arg0 === 'is supported') return true;
if (arg0 === 'is animating') return false;
if (arg0 === 'is inward') return false;
@@ -14,10 +8,8 @@ export function initFomanticTransition() {
let argObj: Record;
if (typeof arg0 === 'string') {
- // many behaviors are no-op now. https://fomantic-ui.com/modules/transition.html#/usage
- if (transitionNopBehaviors.has(arg0)) return this;
- // now, the arg0 is an animation name, the syntax: (animation, duration, complete)
- argObj = {animation: arg0, ...(arg1 && {duration: arg1}), ...(arg2 && {onComplete: arg2})};
+ if (arg0 === 'stop all') return this;
+ argObj = {animation: arg0};
} else if (typeof arg0 === 'object') {
argObj = arg0;
} else {
@@ -38,12 +30,10 @@ export function initFomanticTransition() {
el.classList.remove('hidden');
el.classList.add('visible', 'transition');
if (argObj.displayType) el.style.setProperty('display', argObj.displayType, 'important');
- argObj.onShow?.call(el);
} else {
el.classList.add('hidden');
el.classList.remove('visible'); // don't remove the transition class because the Fomantic animation style is `.hidden.transition`.
el.style.removeProperty('display');
- argObj.onHidden?.call(el);
}
argObj.onComplete?.call(el);
});
diff --git a/web_src/js/modules/modal.test.ts b/web_src/js/modules/modal.test.ts
new file mode 100644
index 00000000000..e550b6431b0
--- /dev/null
+++ b/web_src/js/modules/modal.test.ts
@@ -0,0 +1,87 @@
+import {userEvent} from 'vitest/browser';
+import {createElementFromHTML} from '../utils/dom.ts';
+import {hideModal, initGlobalModal, showModal} from './modal.ts';
+import {showErrorToast} from './toast.ts';
+
+test('showModal blocks the page and restores focus until closed by actions, backdrop or unconsumed Escape on the topmost modal', async () => {
+ initGlobalModal();
+ document.body.innerHTML = `
+
+
+ `;
+ const opener = document.querySelector('#opener')!;
+ const modal = document.querySelector('.modal-dialog')!;
+ const onHidden = vi.fn();
+ let approve = false;
+ opener.focus();
+ showModal(modal, {onApprove: () => approve, onHidden});
+ const backdrop = modal.parentElement!;
+ expect(opener.inert).toBe(true);
+ expect(backdrop.inert).toBe(false);
+ expect(modal.getAttribute('aria-labelledby')).toBe(modal.querySelector('.header')!.id);
+ expect(document.body.classList.contains('modal-page-blocked')).toBe(true);
+ expect(modal.querySelector('.cancel')!.getAttribute('type')).toBe('button');
+
+ modal.querySelector('input')!.value = 'changed';
+ modal.querySelector('.ok')!.click();
+ expect(modal.open).toBe(true);
+ approve = true;
+ modal.querySelector('.ok')!.click();
+ expect(onHidden).toHaveBeenCalledOnce();
+ expect(modal.querySelector('input')!.value).toBe('');
+ expect(document.body.classList.contains('modal-page-blocked')).toBe(false);
+ expect(opener.inert).toBe(false);
+ expect(document.activeElement).toBe(opener);
+
+ for (const selector of ['.cancel', '.close-modal']) {
+ showModal(modal);
+ modal.querySelector(selector)!.click();
+ expect(modal.open).toBe(false);
+ }
+
+ showModal(modal);
+ await userEvent.click(modal.querySelector('input')!);
+ expect(modal.open).toBe(true);
+ backdrop.dispatchEvent(new PointerEvent('pointerdown'));
+ backdrop.click();
+ expect(modal.open).toBe(false);
+
+ showModal(modal);
+ document.addEventListener('keydown', (e) => e.preventDefault(), {once: true});
+ await userEvent.keyboard('{Escape}');
+ expect(modal.open).toBe(true);
+
+ const nested = createElementFromHTML('');
+ showModal(nested, {onHidden: () => nested.remove()});
+ const nestedBackdrop = nested.parentElement!;
+ expect(backdrop.inert).toBe(true);
+ await userEvent.keyboard('{Escape}');
+ expect(nestedBackdrop.isConnected).toBe(false);
+ expect(modal.open).toBe(true);
+ expect(backdrop.inert).toBe(false);
+ expect(opener.inert).toBe(true);
+ expect(modal.contains(document.activeElement)).toBe(true);
+
+ showErrorToast('error in modal');
+ expect(document.querySelector('.toastify.on')).not.toBeNull();
+ await userEvent.keyboard('{Escape}');
+ expect(modal.open).toBe(false);
+ expect(document.querySelector('.toastify.on')).toBeNull();
+
+ (document.activeElement as HTMLElement).blur();
+ showModal(modal);
+ hideModal(modal);
+ expect(document.activeElement).toBe(document.body);
+
+ showModal(modal, {onHidden: () => modal.remove()});
+ hideModal(modal);
+ opener.focus();
+ expect(backdrop.isConnected).toBe(false);
+ expect(document.activeElement).toBe(opener);
+});
diff --git a/web_src/js/modules/modal.ts b/web_src/js/modules/modal.ts
new file mode 100644
index 00000000000..cfaf2639c8a
--- /dev/null
+++ b/web_src/js/modules/modal.ts
@@ -0,0 +1,112 @@
+import {hideAll} from 'tippy.js';
+import {generateElemId, queryElems} from '../utils/dom.ts';
+import {hideToastsAll} from './toast.ts';
+
+type ModalOpts = {
+ onApprove?: () => boolean | void, // return false to keep the modal open
+ onShow?: () => void | Promise,
+ onHidden?: () => void,
+};
+
+const modalStates = new WeakMap();
+
+const topModal = () => Array.from(document.querySelectorAll('.modal-dialog[open]')).at(-1);
+
+function blockPage(elBackdrop?: Element | null) {
+ const wasBlocked = document.body.classList.contains('modal-page-blocked');
+ const shouldBlock = Boolean(elBackdrop);
+ if (wasBlocked !== shouldBlock) {
+ // keep the page width when its scrollbar disappears
+ document.body.style.marginRight = shouldBlock ? `${window.innerWidth - document.documentElement.clientWidth}px` : '';
+ document.body.classList.toggle('modal-page-blocked', shouldBlock);
+ }
+ for (const el of document.body.children) {
+ (el as HTMLElement).inert = shouldBlock && el !== elBackdrop;
+ }
+}
+
+export function initGlobalModal() {
+ // on window, so popups handling Escape on document can consume it first
+ window.addEventListener('keydown', (e) => {
+ if (e.key !== 'Escape' || e.defaultPrevented || e.isComposing) return;
+ const elModal = topModal();
+ if (elModal) hideModal(elModal);
+ });
+}
+
+function defaultApprove(elApprove: Element): boolean {
+ const elForm = elApprove.closest('form');
+ if (!elForm) return true;
+
+ // if there is traditional form, keep the modal open while the form submits
+ // "form-fetch-action" form's submit (is-loading) is handled by the fetch-action module
+ if (!elForm.matches('.form-fetch-action') && elForm.reportValidity()) {
+ elForm.classList.add('is-loading');
+ }
+ return false;
+}
+
+function createModalBackdrop(elModal: HTMLDialogElement): HTMLElement {
+ const elBackdrop = document.createElement('div');
+ elBackdrop.className = 'modal-backdrop';
+ elBackdrop.append(elModal);
+
+ const elAriaLabelTarget = elModal.querySelector(':scope > .header, :scope > .content');
+ if (elAriaLabelTarget) {
+ elAriaLabelTarget.id ||= generateElemId('_modal_label_');
+ elModal.setAttribute('aria-labelledby', elAriaLabelTarget.id);
+ }
+
+ let backdropPressed = false;
+ elBackdrop.addEventListener('pointerdown', (event) => { backdropPressed = event.target === elBackdrop });
+ elBackdrop.addEventListener('click', (event) => {
+ const target = event.target as Element;
+ if (target === elBackdrop) {
+ if (backdropPressed) hideModal(elModal);
+ } else if (target.closest('.close-modal, .actions .cancel')) {
+ hideModal(elModal);
+ } else {
+ const elApprove = target.closest('.actions :is(.approve, .ok)');
+ if (!elApprove) return;
+ const {onApprove} = modalStates.get(elModal)!.opts;
+ if ((onApprove ? onApprove() : defaultApprove(elApprove)) !== false) hideModal(elModal);
+ }
+ });
+ document.body.append(elBackdrop);
+ return elBackdrop;
+}
+
+export function showModal(elModal: HTMLDialogElement, opts: ModalOpts = {}) {
+ if (elModal.open) return;
+
+ const elBackdrop = elModal.parentElement?.matches('.modal-backdrop') ? elModal.parentElement : createModalBackdrop(elModal);
+ modalStates.set(elModal, {opts, opener: document.activeElement});
+ elModal.querySelector('.close-modal')?.setAttribute('aria-label', 'Close');
+
+ // Avoid "cancel" buttons submitting the form, otherwise "Enter" in the form clicks this "cancel" button before the "ok" button
+ queryElems(elModal, 'form button.cancel:not([type])', (button) => button.setAttribute('type', 'button'));
+
+ hideToastsAll();
+ hideAll();
+ blockPage(elBackdrop);
+
+ elModal.show(); // not showModal: popups and toasts mounted on body must stay usable above it, see https://github.com/whatwg/html/issues/9936
+ opts.onShow?.();
+}
+
+export function hideModal(elModal: HTMLDialogElement) {
+ if (!elModal.open) return;
+ const {opts, opener} = modalStates.get(elModal)!;
+ const elBackdrop = elModal.parentElement!;
+ elModal.close();
+ blockPage(topModal()?.parentElement);
+
+ // don't leave the focus in the closed dialog when the opener is gone
+ if (document.activeElement instanceof HTMLElement) document.activeElement.blur();
+ if (opener instanceof HTMLElement) opener.focus();
+
+ hideToastsAll();
+ queryElems(elModal, 'form', (form: HTMLFormElement) => form.reset());
+ opts.onHidden?.();
+ if (!elBackdrop.contains(elModal)) elBackdrop.remove();
+}
diff --git a/web_src/js/modules/shortcut.test.ts b/web_src/js/modules/shortcut.test.ts
index d2258334725..00c3aa409f2 100644
--- a/web_src/js/modules/shortcut.test.ts
+++ b/web_src/js/modules/shortcut.test.ts
@@ -1,8 +1,6 @@
-import '../../fomantic/build/fomantic.js';
-import {userEvent} from 'vitest/browser';
import {initGlobalShortcut, makeFullKeyFromEvent} from './shortcut.ts';
import {initGlobalSelectorObserver} from './observer.ts';
-import {initGiteaFomantic} from './fomantic.ts';
+import {initGlobalModal} from './modal.ts';
function press(key: string, options: KeyboardEventInit = {}, target: HTMLElement = document.body) {
const event = new KeyboardEvent('keydown', {key, bubbles: true, cancelable: true, ...options});
@@ -21,7 +19,7 @@ beforeAll(() => {
Editable
`;
- initGiteaFomantic();
+ initGlobalModal();
initGlobalShortcut();
initGlobalSelectorObserver(null);
});
@@ -106,7 +104,7 @@ test('normalizes keys and sorts modifiers alphabetically', () => {
}
});
-test('shows current shortcuts with ? and closes with Escape or the close button', async () => {
+test('shows current shortcuts with ? and closes with Escape or the close button', () => {
const input = document.querySelector('input')!;
const click = vi.spyOn(document.querySelector('button')!, 'click');
press('?', {shiftKey: true}, input);
@@ -115,20 +113,20 @@ test('shows current shortcuts with ? and closes with Escape or the close button'
expect(document.querySelector('#global-shortcut-help')).toBeNull();
press('g');
expect(press('?', {shiftKey: true}).defaultPrevented).toBe(true);
- const modal = document.querySelector('#global-shortcut-help')!;
- expect(modal.classList.contains('active')).toBe(true);
+ const modal = document.querySelector('#global-shortcut-help')!;
+ expect(modal.open).toBe(true);
expect(modal.querySelector('.shortcut-list tr:nth-child(2) td:last-child')!.textContent).toBe('Issues');
expect(modal.querySelector('.shortcut-list')!.textContent).toContain('Search code');
expect(modal.querySelectorAll('.shortcut-list tr')).toHaveLength(3);
press('g');
press('i');
expect(click).not.toHaveBeenCalled();
- await userEvent.keyboard('{Escape}');
- expect(modal.classList.contains('active')).toBe(false);
+ press('Escape');
+ expect(modal.open).toBe(false);
input.setAttribute('placeholder', 'Updated search');
press('?');
- expect(document.querySelectorAll('.ui.modal')).toHaveLength(1);
+ expect(document.querySelectorAll('.modal-dialog')).toHaveLength(1);
expect(modal.querySelector('.shortcut-list')!.textContent).toContain('Updated search');
expect(modal.querySelector('.shortcut-list b')).toBeNull();
expect(modal.querySelectorAll('.shortcut-list tr')).toHaveLength(3);
diff --git a/web_src/js/modules/shortcut.ts b/web_src/js/modules/shortcut.ts
index 44622228b1e..1f9c629bc75 100644
--- a/web_src/js/modules/shortcut.ts
+++ b/web_src/js/modules/shortcut.ts
@@ -1,6 +1,6 @@
import {registerGlobalInitFunc} from './observer.ts';
import {createElementFromHTML, hideElem, toggleElem} from '../utils/dom.ts';
-import {showFomanticModal} from './fomantic/modal.ts';
+import {showModal} from './modal.ts';
import {html, htmlRaw} from '../utils/html.ts';
type ShortcutHandler = (el: HTMLElement) => boolean;
@@ -69,10 +69,10 @@ function getShortcutTextContent(elem: Element): string {
}
function showShortcutHelp() {
- let modal = document.querySelector('#global-shortcut-help');
+ let modal = document.querySelector('#global-shortcut-help');
if (!modal) {
- modal = createElementFromHTML(html`
-
+ modal = createElementFromHTML(html`
+
+
`);
- document.body.append(modal);
}
const seenShortcuts = new Set();
@@ -112,7 +111,7 @@ function showShortcutHelp() {
}
list.append(createElementFromHTML(html`| ${htmlRaw(keysHtml)} | ${label} |
`));
}
- showFomanticModal(modal);
+ showModal(modal);
}
export function registerShortcutHandler(name: string, handler: ShortcutHandler) {
@@ -151,7 +150,7 @@ export function initGlobalShortcut() {
// Don't trigger shortcuts when typing in input fields or contenteditable areas
let shouldSkip = target.matches('input, textarea, select') || target.isContentEditable;
// Skip shortcuts when a modal or dropdown is open (active), to avoid interfering with their own keyboard handling
- shouldSkip = shouldSkip || Boolean(document.querySelector('.ui.modal.active') || document.querySelector('.ui.dropdown.active'));
+ shouldSkip = shouldSkip || Boolean(document.querySelector('.modal-dialog[open], .ui.dropdown.active'));
if (shouldSkip) {
shortcutPresses.length = 0;
return;
diff --git a/web_src/js/modules/toast.ts b/web_src/js/modules/toast.ts
index 0f6a054593b..f8aa14be353 100644
--- a/web_src/js/modules/toast.ts
+++ b/web_src/js/modules/toast.ts
@@ -44,12 +44,11 @@ type ToastifyElement = HTMLElement & {_giteaToastifyInstance?: Toast};
/** See https://github.com/apvarun/toastify-js#api for options */
function showToast(message: string, level: Intent = 'info', {gravity, position, duration, useHtmlBody, preventDuplicates = true, ...other}: ToastOpts = {}): Toast | null {
- const parent = document.querySelector('.ui.dimmer.active') ?? document.body;
const duplicateKey = preventDuplicates ? (typeof preventDuplicates === 'string' ? preventDuplicates : `${level}-${message}`) : '';
// prevent showing duplicate toasts with the same level and message, and give visual feedback for end users
if (preventDuplicates) {
- const toastEl = parent.querySelector(`:scope > .toastify.on[data-toast-unique-key="${CSS.escape(duplicateKey)}"]`);
+ const toastEl = document.body.querySelector(`:scope > .toastify.on[data-toast-unique-key="${CSS.escape(duplicateKey)}"]`);
if (toastEl) {
const toastDupNumEl = toastEl.querySelector('.toast-duplicate-number')!;
showElem(toastDupNumEl);
@@ -62,7 +61,6 @@ function showToast(message: string, level: Intent = 'info', {gravity, position,
const {icon, duration: levelDuration} = levels[level];
const bodyHtml = useHtmlBody ? message : htmlEscape(message);
const toast = Toastify({
- selector: parent,
text: html`
${svgRaw(icon)}
1${htmlRaw(bodyHtml)}
@@ -104,10 +102,6 @@ function hideToastByElement(el: Element): void {
(el as ToastifyElement)?._giteaToastifyInstance?.hideToast();
}
-export function hideToastsFrom(parent: Element): void {
- queryElems(parent, ':scope > .toastify.on', hideToastByElement);
-}
-
export function hideToastsAll(): void {
queryElems(document, '.toastify.on', hideToastByElement);
}