refactor: replace Fomantic modal with native dialog (#39846)

Replace Fomantic's hard-forked jQuery modal with native `<dialog>`
elements, advancing https://github.com/go-gitea/gitea/issues/29849. The
UI is visually unchanged. Dialogs open with `show()` over an `inert`
page, so tooltips, popups and toasts mounted on `<body>` stay usable
above them, unlike with `showModal()`
(https://github.com/whatwg/html/issues/9936).

- Remove the Fomantic modal and dimmer, add `showModal` and `hideModal`
- Declare modals as `<dialog class="modal-dialog">`, shown inside a
`.modal-backdrop`, instead of `ui modal` with its unused size classes
- Remove two modals that could never be opened
- Block the page while a modal is open, close the topmost one on Escape
and restore focus to its opener
- Allow stacked modals, use a confirm modal instead of `window.confirm`
for deleting content history
- Hide popups and toasts when a modal opens or closes
- Keep the release notes modal open when generating notes fails

Fixes: https://github.com/go-gitea/gitea/issues/39643
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
silverwindandwxiaoguang authored and GitHub committed 2026-10-20 12:32:27 +02:00
1 parent 7610336d74
commit e645afbb3f
95 files changed
+607 -1719

No files matched your search

+2 -2
View File
@@ -28,7 +28,7 @@
</div>
</div>
<div class="ui g-modal-confirm modal" id="delete-badge-modal">
<dialog class="modal-dialog" id="delete-badge-modal">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "admin.badges.delete_badge"}}
@@ -39,6 +39,6 @@
</div>
{{template "base/modal_actions_confirm" .}}
</form>
</div>
</dialog>
{{template "admin/layout_footer" .}}
+2 -2
View File
@@ -76,7 +76,7 @@
{{template "base/paginate" .}}
<div class="ui g-modal-confirm modal" id="change-email-modal">
<dialog class="modal-dialog" id="change-email-modal">
<div class="header">
{{ctx.Locale.Tr "admin.emails.change_email_header"}}
</div>
@@ -95,7 +95,7 @@
{{template "base/modal_actions_confirm" .}}
</form>
</div>
</dialog>
</div>
{{template "admin/layout_footer" .}}
+2 -2
View File
@@ -61,9 +61,9 @@
{{template "base/paginate" .}}
</div>
<div class="ui modal admin" id="detail-modal">
<dialog class="modal-dialog admin" id="detail-modal">
<div class="header">{{ctx.Locale.Tr "admin.notices.view_detail_header"}}</div>
<div class="content"><pre></pre></div>
</div>
</dialog>
{{template "admin/layout_footer" .}}
+8 -6
View File
@@ -83,12 +83,14 @@
{{template "base/paginate" .}}
</div>
<form class="ui small modal form-fetch-action" method="post" id="admin-package-delete-modal">
<dialog class="modal-dialog" id="admin-package-delete-modal">
<div class="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "packages.settings.delete.version"}}</div>
<div class="content">
{{ctx.Locale.Tr "packages.settings.delete.notice" (HTMLFormat `<span class="%s"></span>` "package-name") (HTMLFormat `<span class="%s"></span>` "package-version")}}
</div>
{{template "base/modal_actions_confirm" .}}
</form>
<form class="form-fetch-action" method="post">
<div class="content">
{{ctx.Locale.Tr "packages.settings.delete.notice" (HTMLFormat `<span class="%s"></span>` "package-name") (HTMLFormat `<span class="%s"></span>` "package-version")}}
</div>
{{template "base/modal_actions_confirm" .}}
</form>
</dialog>
{{template "admin/layout_footer" .}}
+10 -8
View File
@@ -96,14 +96,16 @@
{{template "base/paginate" .}}
</div>
<form class="ui small modal form-fetch-action" id="admin-repo-delete-modal" method="post">
<dialog class="modal-dialog" id="admin-repo-delete-modal">
<div class="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.settings.delete"}}</div>
<div class="content">
<p>{{ctx.Locale.Tr "repo.settings.delete_desc"}}</p>
{{ctx.Locale.Tr "repo.settings.delete_notices_2" (HTMLFormat `<span class="%s"></span>` "repo-name")}}<br>
{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}
</div>
{{template "base/modal_actions_confirm" .}}
</form>
<form class="form-fetch-action" method="post">
<div class="content">
<p>{{ctx.Locale.Tr "repo.settings.delete_desc"}}</p>
{{ctx.Locale.Tr "repo.settings.delete_notices_2" (HTMLFormat `<span class="%s"></span>` "repo-name")}}<br>
{{ctx.Locale.Tr "repo.settings.delete_notices_fork_1"}}
</div>
{{template "base/modal_actions_confirm" .}}
</form>
</dialog>
{{template "admin/layout_footer" .}}
+4 -4
View File
@@ -24,7 +24,7 @@
<span class="tw-flex-1"> {{svg "octicon-file-directory-fill"}} {{$dir}}</span>
<div>
<button type="button" class="ui button primary show-modal tw-p-2" data-modal="#adopt-unadopted-modal-{{$dirI}}">{{svg "octicon-plus"}} {{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</button>
<div class="ui g-modal-confirm modal" id="adopt-unadopted-modal-{{$dirI}}">
<dialog class="modal-dialog" id="adopt-unadopted-modal-{{$dirI}}">
<div class="header">
<span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting"}}</span>
</div>
@@ -38,9 +38,9 @@
<input type="hidden" name="page" value="{{$.CurrentPage}}">
{{template "base/modal_actions_confirm"}}
</form>
</div>
</dialog>
<button type="button" class="ui button red show-modal tw-p-2" data-modal="#delete-unadopted-modal-{{$dirI}}">{{svg "octicon-x"}} {{ctx.Locale.Tr "repo.delete_preexisting_label"}}</button>
<div class="ui g-modal-confirm modal" id="delete-unadopted-modal-{{$dirI}}">
<dialog class="modal-dialog" id="delete-unadopted-modal-{{$dirI}}">
<div class="header">
<span class="label">{{ctx.Locale.Tr "repo.delete_preexisting"}}</span>
</div>
@@ -54,7 +54,7 @@
<input type="hidden" name="page" value="{{$.CurrentPage}}">
{{template "base/modal_actions_confirm"}}
</form>
</div>
</dialog>
</div>
</div>
{{end}}
+2 -2
View File
@@ -223,7 +223,7 @@
</div>
</div>
<div class="ui g-modal-confirm delete modal" id="delete-user-modal">
<dialog class="modal-dialog" id="delete-user-modal">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "settings.delete_account_title"}}
@@ -241,6 +241,6 @@
</div>
{{template "base/modal_actions_confirm" .}}
</form>
</div>
</dialog>
{{template "admin/layout_footer" .}}
+11 -9
View File
@@ -5,18 +5,20 @@
<span>{{ctx.Locale.Tr "settings.organization"}} ({{ctx.Locale.Tr "admin.total" $userOrgsTotal}})</span>
{{if $userOrgs}}
<button type="button" class="ui red tiny button show-modal" data-modal="#remove-all-orgs-modal">{{ctx.Locale.Tr "remove_all"}}</button>
<form class="ui small modal form-fetch-action" id="remove-all-orgs-modal" method="post" action="{{ctx.RootData.Link}}/orgs/remove-all">
<dialog class="modal-dialog" id="remove-all-orgs-modal">
<div class="header">
{{ctx.Locale.Tr "admin.users.remove_all_orgs_title"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "admin.users.remove_all_orgs_desc" $user.Name $userOrgsTotal}}</p>
</div>
<div class="actions">
<div class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</div>
<button class="ui red button" type="submit">{{ctx.Locale.Tr "remove_all"}}</button>
</div>
</form>
<form class="form-fetch-action" method="post" action="{{ctx.RootData.Link}}/orgs/remove-all">
<div class="content">
<p>{{ctx.Locale.Tr "admin.users.remove_all_orgs_desc" $user.Name $userOrgsTotal}}</p>
</div>
<div class="actions">
<div class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</div>
<button class="ui red button" type="submit">{{ctx.Locale.Tr "remove_all"}}</button>
</div>
</form>
</dialog>
{{end}}
</h4>
+1 -1
View File
@@ -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"
*/}}
<div class="actions">
{{if .ModalButtonDangerText}}
@@ -1,15 +1,15 @@
{{template "devtest/devtest-header"}}
<div class="page-content devtest ui container">
<div class="modal-buttons flex-text-block tw-flex-wrap"></div>
<div id="test-modal-form-1" class="ui mini modal">
<dialog id="test-modal-form-1" class="modal-dialog">
<div class="header">Form dialog (layout 1)</div>
<form class="content" method="post" action="?mock_response_delay=1">
<div class="ui input tw-w-full"><input name="user_input"></div>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form>
</div>
</dialog>
<div id="test-modal-form-2" class="ui mini modal">
<dialog id="test-modal-form-2" class="modal-dialog">
<div class="header">Form dialog (layout 2)</div>
<form method="post" action="?mock_response_delay=1">
<div class="content">
@@ -17,9 +17,9 @@
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</div>
</form>
</div>
</dialog>
<div id="test-modal-form-3" class="ui mini modal">
<dialog id="test-modal-form-3" class="modal-dialog">
<div class="header">Form dialog (layout 3)</div>
<form method="post" action="?mock_response_delay=1">
<div class="content">
@@ -27,9 +27,9 @@
</div>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form>
</div>
</dialog>
<div id="test-modal-form-4" class="ui mini modal">
<dialog id="test-modal-form-4" class="modal-dialog">
<div class="header">Form dialog (layout 4)</div>
<div class="content">
<div class="ui input tw-w-full"><input name="user_input"></div>
@@ -37,9 +37,9 @@
<form method="post" action="?mock_response_delay=1">
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form>
</div>
</dialog>
<div id="test-modal-form-5" class="ui mini modal">
<dialog id="test-modal-form-5" class="modal-dialog">
<div class="header">Form dialog (layout 5)</div>
<div class="content">
<form method="post" action="?mock_response_delay=1">
@@ -47,27 +47,27 @@
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form>
</div>
</div>
</dialog>
<div class="ui g-modal-confirm modal" id="test-modal-default">
<dialog class="modal-dialog" id="test-modal-default">
<div class="header">{{svg "octicon-file"}} Default dialog <span>title</span></div>
<div class="content">
very long aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
</div>
{{template "base/modal_actions_confirm"}}
</div>
</dialog>
<div class="ui g-modal-confirm modal" id="test-modal-confirm">
<dialog class="modal-dialog" id="test-modal-confirm">
<div class="header">Confirm dialog</div>
<div class="content">hello, this is the modal dialog content</div>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</div>
</dialog>
<div class="ui g-modal-confirm modal" id="test-modal-danger">
<dialog class="modal-dialog" id="test-modal-danger">
<div class="header">dangerous action dialog <button type="button" class="close-modal"></button></div>
<div class="content">hello, this is the modal dialog content, this is a dangerous operation</div>
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" "I know and must do this is dangerous operation")}}
</div>
</dialog>
<div class="divider"></div>
@@ -79,7 +79,7 @@
data-modal-a.text-content="fill with attr"
>test-modal-fill-values</button>
<div id="test-modal-fill-values" class="ui mini modal" data-skip-button>
<dialog id="test-modal-fill-values" class="modal-dialog" data-skip-button>
<div class="header">Modal dialog (fill values)</div>
<form class="content">
<div id="fill-by-id"></div>
@@ -89,6 +89,6 @@
<a href="#">link</a>
{{template "base/modal_actions_confirm" dict}}
</form>
</div>
</dialog>
</div>
{{template "devtest/devtest-footer"}}
@@ -35,7 +35,7 @@
</div>
</div>
<div class="ui small modal" id="change-visibility-org-modal">
<dialog class="modal-dialog" id="change-visibility-org-modal">
<div class="header">
{{ctx.Locale.Tr "org.settings.change_visibility"}}
</div>
@@ -70,9 +70,9 @@
</div>
</form>
</div>
</div>
</dialog>
<div class="ui small modal" id="rename-org-modal">
<dialog class="modal-dialog" id="rename-org-modal">
<div class="header">
{{ctx.Locale.Tr "org.settings.rename"}}
</div>
@@ -106,9 +106,9 @@
</div>
</form>
</div>
</div>
</dialog>
<div class="ui small modal" id="delete-org-modal">
<dialog class="modal-dialog" id="delete-org-modal">
<div class="header">
{{ctx.Locale.Tr "org.settings.delete_account"}}
</div>
@@ -139,4 +139,4 @@
</div>
</form>
</div>
</div>
</dialog>
+8 -6
View File
@@ -72,13 +72,15 @@
</div>
</div>
</div>
<form class="ui small modal form-fetch-action" method="post" id="remove-team-member">
<dialog class="modal-dialog" id="remove-team-member">
<div class="header">
{{ctx.Locale.Tr "org.members.remove"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "org.members.remove.detail" (HTMLFormat `<span class="%s"></span>` "name") (HTMLFormat `<span class="%s"></span>` "team-name")}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</form>
<form class="form-fetch-action" method="post">
<div class="content">
<p>{{ctx.Locale.Tr "org.members.remove.detail" (HTMLFormat `<span class="%s"></span>` "name") (HTMLFormat `<span class="%s"></span>` "team-name")}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</form>
</dialog>
{{template "base/footer" .}}
+2 -2
View File
@@ -194,7 +194,7 @@
</div>
</div>
<div class="ui small modal" id="delete-team">
<dialog class="modal-dialog" id="delete-team">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "org.teams.delete_team_title"}}
@@ -203,5 +203,5 @@
<p>{{ctx.Locale.Tr "org.teams.delete_team_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
{{template "base/footer" .}}
+2 -2
View File
@@ -91,7 +91,7 @@
</div>
{{end}}
</div>
<div class="ui mini modal" id="org-member-leave-team">
<dialog class="modal-dialog" id="org-member-leave-team">
<div class="header">
{{ctx.Locale.Tr "org.teams.leave"}}
</div>
@@ -99,4 +99,4 @@
<p>{{ctx.Locale.Tr "org.teams.leave.detail" (HTMLFormat `<span class="%s"></span>` "to-leave-team-name")}}</p>
{{template "base/modal_actions_confirm" .}}
</form>
</div>
</dialog>
+2 -2
View File
@@ -63,7 +63,7 @@
{{template "base/paginate" .}}
</div>
</div>
<div class="ui mini modal" id="org-member-leave-team">
<dialog class="modal-dialog" id="org-member-leave-team">
<div class="header">
{{ctx.Locale.Tr "org.teams.leave"}}
</div>
@@ -71,5 +71,5 @@
<p>{{ctx.Locale.Tr "org.teams.leave.detail" (HTMLFormat `<span class="%s"></span>` "to-leave-team-name")}}</p>
{{template "base/modal_actions_confirm" .}}
</form>
</div>
</dialog>
{{template "base/footer" .}}
+2 -2
View File
@@ -55,7 +55,7 @@
<div class="item-trailing">
<button type="button" class="ui basic red show-modal button" data-modal="#delete-package-modal">{{ctx.Locale.Tr "packages.settings.delete"}}</button>
</div>
<div class="ui tiny modal" id="delete-package-modal">
<dialog class="modal-dialog" id="delete-package-modal">
<div class="header">
{{ctx.Locale.Tr "packages.settings.delete"}}
</div>
@@ -78,7 +78,7 @@
{{template "base/modal_actions_confirm" .}}
</form>
</div>
</div>
</dialog>
</div>
</div>
</div>
+2 -2
View File
@@ -110,7 +110,7 @@
<div class="item">
{{svg "octicon-trash"}}
<a class="show-modal" href data-modal="#delete-package-version-modal">{{ctx.Locale.Tr "packages.settings.delete.version"}}</a>
<div class="ui tiny modal" id="delete-package-version-modal">
<dialog class="modal-dialog" id="delete-package-version-modal">
<div class="header">
{{ctx.Locale.Tr "packages.settings.delete.version"}}
</div>
@@ -122,7 +122,7 @@
{{template "base/modal_actions_confirm" .}}
</form>
</div>
</div>
</dialog>
</div>
{{end}}
</div>
+2 -2
View File
@@ -94,9 +94,9 @@
</div>
{{if and $.CanWriteProjects (not $.Repository.IsArchived)}}
<div class="ui small modal" id="repo-project-delete-modal">
<dialog class="modal-dialog" id="repo-project-delete-modal">
<div class="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.projects.deletion"}}</div>
<div class="content"><p>{{ctx.Locale.Tr "repo.projects.deletion_desc"}}</p></div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
{{end}}
+2 -2
View File
@@ -133,7 +133,7 @@
</div>
{{if $canWriteProject}}
<div class="ui small modal" id="project-column-modal-edit">
<dialog class="modal-dialog" id="project-column-modal-edit">
<div class="header">{{ctx.Locale.Tr "repo.projects.column.edit"}}</div>
<div class="content">
<form class="ui form ignore-dirty" method="post" data-action-base-link="{{$.Link}}">
@@ -155,5 +155,5 @@
</div>
</form>
</div>
</div>
</dialog>
{{end}}
+2 -2
View File
@@ -113,7 +113,7 @@
</div>
{{if $showCreateWorkflowBadge}}
<div id="workflow-status-badge-modal" class="ui tiny modal">
<dialog id="workflow-status-badge-modal" class="modal-dialog">
<div class="header">{{ctx.Locale.Tr "actions.workflow.status_badge"}}</div>
<div class="content">
<div class="ui form" data-global-init="initWorkflowBadgeForm"
@@ -157,7 +157,7 @@
</div>
</div>
</div>
</div>
</dialog>
{{end}}
{{template "base/footer" .}}
@@ -5,7 +5,7 @@
<button type="button" class="ui mini button show-modal" data-modal="#runWorkflowDispatchModal">{{ctx.Locale.Tr "actions.workflow.run"}}{{svg "octicon-triangle-down" 14 "dropdown icon"}}</button>
</div>
</div>
<div id="runWorkflowDispatchModal" class="ui tiny modal">
<dialog id="runWorkflowDispatchModal" class="modal-dialog">
<div class="content">
<form id="runWorkflowDispatchForm" class="ui form ignore-dirty" action="{{$.Link}}/run?workflow={{$.CurWorkflow}}&scoped_workflow_source_repo_id={{$.CurWorkflowRepoID}}&actor={{$.CurActor}}&status={{$.CurStatus}}&branch={{$.CurBranch}}" method="post">
<div class="ui inline field required tw-flex tw-items-center">
@@ -55,4 +55,4 @@
{{template "repo/actions/workflow_dispatch_inputs" .}}
</form>
</div>
</div>
</dialog>
+12 -10
View File
@@ -218,18 +218,20 @@
</div>
</div>
<form class="ui small modal form-fetch-action" method="post" id="delete-branch-modal">
<dialog class="modal-dialog" id="delete-branch-modal">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "repo.branch.delete_html"}} <span class="name"></span>
</div>
<div class="content">
<p>{{ctx.Locale.Tr "repo.branch.delete_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</form>
<form class="form-fetch-action" method="post">
<div class="content">
<p>{{ctx.Locale.Tr "repo.branch.delete_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</form>
</dialog>
<div class="ui mini modal" id="create-branch-modal">
<dialog class="modal-dialog" id="create-branch-modal">
<div class="header">
{{ctx.Locale.Tr "repo.branch.new_branch"}}
</div>
@@ -247,9 +249,9 @@
</div>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form>
</div>
</dialog>
<div class="ui mini modal" id="rename-branch-modal">
<dialog class="modal-dialog" id="rename-branch-modal">
<div class="header">
{{ctx.Locale.Tr "repo.settings.rename_branch"}}
</div>
@@ -266,5 +268,5 @@
</div>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form>
</div>
</dialog>
{{template "base/footer" .}}
+2 -2
View File
@@ -1,4 +1,4 @@
<div class="ui small modal" id="cite-repo-modal">
<dialog class="modal-dialog" id="cite-repo-modal">
<div class="header">
{{ctx.Locale.Tr "repo.cite_this_repo"}}
</div>
@@ -28,4 +28,4 @@
{{ctx.Locale.Tr "cancel"}}
</button>
</div>
</div>
</dialog>
+6 -6
View File
@@ -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"}}</div>
<div class="ui g-modal-confirm modal" id="cherry-pick-modal">
<dialog class="modal-dialog" id="cherry-pick-modal">
<div class="header">
<span id="cherry-pick-header"></span>
</div>
@@ -68,8 +68,8 @@
<button type="submit" id="cherry-pick-submit" class="ui primary button"></button>
</form>
</div>
</div>
<div class="ui small modal" id="create-branch-modal">
</dialog>
<dialog class="modal-dialog" id="create-branch-modal">
<div class="header">
{{ctx.Locale.Tr "repo.branch.new_branch"}}
</div>
@@ -91,8 +91,8 @@
</div>
</form>
</div>
</div>
<div class="ui small modal" id="create-tag-modal">
</dialog>
<dialog class="modal-dialog" id="create-tag-modal">
<div class="header">
{{ctx.Locale.Tr "repo.tag.create_tag_operation"}}
</div>
@@ -115,7 +115,7 @@
</div>
</form>
</div>
</div>
</dialog>
</div>
</div>
{{end}}
+2 -2
View File
@@ -22,7 +22,7 @@
<span class="ui small label">{{CountFmt $.Repository.NumForks}}</span>
</a>
{{if $.ShowForkModal}}
<div class="ui small modal" id="fork-repo-modal">
<dialog class="modal-dialog" id="fork-repo-modal">
<div class="header">
{{ctx.Locale.Tr "repo.already_forked" $.Repository.Name}}
</div>
@@ -39,5 +39,5 @@
<a href="{{$.RepoLink}}/fork">{{ctx.Locale.Tr "repo.fork_to_different_account"}}</a>
{{end}}
</div>
</div>
</dialog>
{{end}}
@@ -1,4 +1,4 @@
<div class="ui small modal" id="issue-label-edit-modal" data-global-init="initLabelEditModal"
<dialog class="modal-dialog" id="issue-label-edit-modal" data-global-init="initLabelEditModal"
data-current-page-link="{{$.Link}}"{{/*will be used to construct "new label" and "edit label" URLs*/}}
data-text-new-label="{{ctx.Locale.Tr "repo.issues.new_label"}}"
data-text-edit-label="{{ctx.Locale.Tr "repo.issues.label_modify"}}"
@@ -66,4 +66,4 @@
{{ctx.Locale.Tr "save"}}
</button>
</div>
</div>
</dialog>
+2 -2
View File
@@ -92,11 +92,11 @@
</div>
{{if or .CanWriteIssues .CanWritePulls}}
<div class="ui small modal" id="repo-milestone-delete-modal">
<dialog class="modal-dialog" id="repo-milestone-delete-modal">
<div class="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.milestones.deletion"}}</div>
<div class="content"><p>{{ctx.Locale.Tr "repo.milestones.deletion_desc"}}</p></div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
{{end}}
{{template "base/footer" .}}
@@ -127,18 +127,20 @@
</div>
{{if and .CanCreateIssueDependencies (not .Repository.IsArchived)}}
<form id="issue-remove-dependency-confirm" class="ui g-modal-confirm modal form-fetch-action" method="post" action="{{.Issue.Link}}/dependency/delete">
<dialog class="modal-dialog" id="issue-remove-dependency-confirm">
<div class="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.issues.dependency.remove_header"}}</div>
<div class="content">
<input type="hidden" value="" name="removeDependencyID" class="remove-dependency-id">
<input type="hidden" value="" name="dependencyType" class="dependency-type">
<p>
{{ctx.Locale.Tr (Iif .Issue.IsPull "repo.issues.dependency.pr_remove_text" "repo.issues.dependency.issue_remove_text")}}
</p>
{{$ModalButtonCancelText := ctx.Locale.Tr "repo.issues.dependency.cancel"}}
{{$ModalButtonOkText := ctx.Locale.Tr "repo.issues.dependency.remove"}}
{{template "base/modal_actions_confirm" (dict "ModalButtonCancelText" $ModalButtonCancelText "ModalButtonOkText" $ModalButtonOkText)}}
</div>
</form>
<form class="form-fetch-action" method="post" action="{{.Issue.Link}}/dependency/delete">
<div class="content">
<input type="hidden" value="" name="removeDependencyID" class="remove-dependency-id">
<input type="hidden" value="" name="dependencyType" class="dependency-type">
<p>
{{ctx.Locale.Tr (Iif .Issue.IsPull "repo.issues.dependency.pr_remove_text" "repo.issues.dependency.issue_remove_text")}}
</p>
{{$ModalButtonCancelText := ctx.Locale.Tr "repo.issues.dependency.cancel"}}
{{$ModalButtonOkText := ctx.Locale.Tr "repo.issues.dependency.remove"}}
{{template "base/modal_actions_confirm" (dict "ModalButtonCancelText" $ModalButtonCancelText "ModalButtonOkText" $ModalButtonOkText)}}
</div>
</form>
</dialog>
{{end}}
{{end}}
@@ -24,7 +24,7 @@
{{svg "octicon-lock"}} {{ctx.Locale.Tr "repo.issues.lock"}}
{{end}}
</button>
<div class="ui tiny modal" id="lock-conversation">
<dialog class="modal-dialog" id="lock-conversation">
<div class="header">
{{if .Issue.IsLocked}}
{{ctx.Locale.Tr "repo.issues.unlock.title"}}
@@ -67,7 +67,7 @@
<div class="actions">
<button type="button" class="ui cancel button">{{ctx.Locale.Tr "settings.cancel"}}</button>
{{/* explicitly focus the submit button, to avoid Fomantic modal focuses and popups the dropdown */}}
{{/* explicitly focus the submit button, otherwise the modal focuses and pops up the dropdown */}}
<button type="submit" class="ui red button" autofocus>
{{if .Issue.IsLocked}}
{{ctx.Locale.Tr "repo.issues.unlock_confirm"}}
@@ -78,12 +78,12 @@
</div>
</form>
</div>
</div>
</dialog>
<button type="button" class="tw-mt-1 fluid ui show-modal button" data-modal="#sidebar-delete-issue">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "repo.issues.delete"}}
</button>
<div class="ui g-modal-confirm modal" id="sidebar-delete-issue">
<dialog class="modal-dialog" id="sidebar-delete-issue">
<div class="header">
{{if .Issue.IsPull}}
{{ctx.Locale.Tr "repo.pulls.delete.title"}}
@@ -103,5 +103,5 @@
<form action="{{.Issue.Link}}/delete" method="post">
{{template "base/modal_actions_confirm" .}}
</form>
</div>
</dialog>
{{end}}
@@ -106,7 +106,7 @@
</div>
{{if $data.CurrentPullReviewers}}
<div class="ui small modal" id="issue-sidebar-dismiss-review-modal">
<dialog class="modal-dialog" id="issue-sidebar-dismiss-review-modal">
<div class="header">
{{ctx.Locale.Tr "repo.issues.dismiss_review"}}
</div>
@@ -126,6 +126,6 @@
</div>
</form>
</div>
</div>
</dialog>
{{end}}
</div>
@@ -35,7 +35,7 @@
{{end}}
{{/* set time estimate modal */}}
<div class="ui mini modal" id="issue-time-set-estimate-modal">
<dialog class="modal-dialog" id="issue-time-set-estimate-modal">
<div class="header">{{ctx.Locale.Tr "repo.issues.time_estimate_set"}}</div>
<form method="post" class="ui form form-fetch-action" action="{{.Issue.Link}}/time_estimate">
<div class="content">
@@ -46,10 +46,10 @@
</div>
</div>
</form>
</div>
</dialog>
{{/* manually add time modal */}}
<div class="ui mini modal" id="issue-time-manually-add-modal">
<dialog class="modal-dialog" id="issue-time-manually-add-modal">
<div class="header">{{ctx.Locale.Tr "repo.issues.add_time_manually"}}</div>
<form method="post" class="ui form form-fetch-action" action="{{.Issue.Link}}/times/add">
<div class="content flex-text-block">
@@ -61,7 +61,7 @@
<button type="submit" class="ui primary button">{{ctx.Locale.Tr "repo.issues.timetracker_timer_manually_add"}}</button>
</div>
</form>
</div>
</dialog>
</div>
{{end}}
{{if .WorkingUsers}}
@@ -1,4 +1,4 @@
<div class="ui small modal" id="reference-issue-modal">
<dialog class="modal-dialog" id="reference-issue-modal">
<div class="header">
{{ctx.Locale.Tr "repo.issues.context.reference_issue"}}
</div>
@@ -24,4 +24,4 @@
</div>
</form>
</div>
</div>
</dialog>
+4 -4
View File
@@ -51,7 +51,7 @@
</div>
</div>
<div class="ui small modal" id="delete-repo-modal">
<dialog class="modal-dialog" id="delete-repo-modal">
<div class="header">
{{ctx.Locale.Tr "repo.settings.delete"}}
</div>
@@ -67,9 +67,9 @@
"RepoOperationText" (ctx.Locale.Tr "repo.settings.confirm_delete")
)}}
</div>
</div>
</dialog>
<div class="ui g-modal-confirm modal" id="cancel-repo-modal">
<dialog class="modal-dialog" id="cancel-repo-modal">
<div class="header">
{{ctx.Locale.Tr "repo.migrate.cancel_migrating_title"}}
</div>
@@ -79,6 +79,6 @@
</div>
{{template "base/modal_actions_confirm" .}}
</form>
</div>
</dialog>
{{template "base/footer" .}}
-13
View File
@@ -116,17 +116,4 @@
</div>
</div>
{{if (and ($.Permission.CanWrite ctx.Consts.RepoUnitTypeCode) .PageIsTagList)}}
<div class="ui g-modal-confirm delete modal">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "repo.release.delete_tag"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "repo.release.deletion_tag_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
{{end}}
{{template "base/footer" .}}
+2 -2
View File
@@ -145,7 +145,7 @@
</div>
</div>
<div id="generate-release-notes-modal" class="ui mini modal">
<dialog id="generate-release-notes-modal" class="modal-dialog">
<div class="content ui form">
<p>{{ctx.Locale.Tr "repo.release.generate_notes_desc"}}</p>
<div class="field">
@@ -156,6 +156,6 @@
<button type="button" class="ui primary ok button">{{ctx.Locale.Tr "repo.release.generate_notes"}}</button>
</div>
</div>
</div>
</dialog>
{{template "base/footer" .}}
+2 -2
View File
@@ -65,10 +65,10 @@
{{end}}
</div>
<div class="ui small modal" id="repo-branch-protection-delete-modal">
<dialog class="modal-dialog" id="repo-branch-protection-delete-modal">
<div class="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.settings.protected_branch_deletion"}}</div>
<div class="content"><p>{{ctx.Locale.Tr "repo.settings.protected_branch_deletion_desc"}}</p></div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
{{template "repo/settings/layout_footer" .}}
+2 -2
View File
@@ -118,10 +118,10 @@
{{end}}
</div>
<div class="ui small modal" id="repo-collaborator-delete-modal">
<dialog class="modal-dialog" id="repo-collaborator-delete-modal">
<div class="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.settings.collaborator_deletion"}}</div>
<div class="content"><p>{{ctx.Locale.Tr "repo.settings.collaborator_deletion_desc"}}</p></div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
{{template "repo/settings/layout_footer" .}}
+4 -4
View File
@@ -60,16 +60,16 @@
</div>
</div>
<div class="ui small modal" id="repo-deploy-key-delete-modal">
<dialog class="modal-dialog" id="repo-deploy-key-delete-modal">
<div class="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "remove"}}</div>
<div class="content"><p>{{ctx.Locale.Tr "repo.settings.deploy_key_deletion_desc"}}</p></div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
<div class="ui small modal" id="repo-deploy-token-regenerate-modal">
<dialog class="modal-dialog" id="repo-deploy-token-regenerate-modal">
<div class="header">{{svg "octicon-sync"}} {{ctx.Locale.Tr "settings.regenerate_token"}}</div>
<div class="content"><p>{{ctx.Locale.Tr "repo.settings.regenerate_deploy_token_desc"}}</p></div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
{{template "repo/settings/layout_footer" .}}
+2 -2
View File
@@ -37,12 +37,12 @@
</tbody>
</table>
{{template "base/paginate" .}}
<div class="ui g-modal-confirm modal" id="delete-lfs-object">
<dialog class="modal-dialog" id="delete-lfs-object">
<div class="header"></div>
<form class="content" method="post">
<p>{{ctx.Locale.Tr "repo.settings.lfs_delete_warning"}}</p>
{{template "base/modal_actions_confirm"}}
</form>
</div>
</dialog>
</div>
{{template "repo/settings/layout_footer" .}}
+14 -14
View File
@@ -919,7 +919,7 @@
{{if $.CanManagerDangerZone}}
{{if .Repository.IsMirror}}
<div class="ui small modal" id="convert-mirror-repo-modal">
<dialog class="modal-dialog" id="convert-mirror-repo-modal">
<div class="header">
{{ctx.Locale.Tr "repo.settings.convert"}}
</div>
@@ -934,10 +934,10 @@
"RepoOperationText" (ctx.Locale.Tr "repo.settings.convert_confirm")
)}}
</div>
</div>
</dialog>
{{end}}
{{if .CanConvertFork}}
<div class="ui small modal" id="convert-fork-repo-modal">
<dialog class="modal-dialog" id="convert-fork-repo-modal">
<div class="header">
{{ctx.Locale.Tr "repo.settings.convert_fork"}}
</div>
@@ -952,9 +952,9 @@
"RepoOperationText" (ctx.Locale.Tr "repo.settings.convert_fork_confirm")
)}}
</div>
</div>
</dialog>
{{end}}
<div class="ui small modal" id="transfer-repo-modal">
<dialog class="modal-dialog" id="transfer-repo-modal">
<div class="header">
{{ctx.Locale.Tr "repo.settings.transfer"}}
</div>
@@ -972,9 +972,9 @@
"RepoOperationText" (ctx.Locale.Tr "repo.settings.transfer_perform")
)}}
</div>
</div>
</dialog>
<div class="ui small modal" id="delete-repo-modal">
<dialog class="modal-dialog" id="delete-repo-modal">
<div class="header">
{{ctx.Locale.Tr "repo.settings.delete"}}
</div>
@@ -993,10 +993,10 @@
"RepoOperationText" (ctx.Locale.Tr "repo.settings.confirm_delete")
)}}
</div>
</div>
</dialog>
{{if not .Repository.IsFork}}
<div class="ui small modal" id="visibility-repo-modal">
<dialog class="modal-dialog" id="visibility-repo-modal">
<div class="header">
{{ctx.Locale.Tr "repo.visibility"}}
</div>
@@ -1040,11 +1040,11 @@
{{template "base/modal_actions_confirm" (dict "ModalButtonDangerText" (Iif .Repository.IsPrivate (ctx.Locale.Tr "repo.settings.visibility.public.button") (ctx.Locale.Tr "repo.settings.visibility.private.button")))}}
</form>
</div>
</div>
</dialog>
{{end}}
{{if .Repository.UnitEnabled ctx ctx.Consts.RepoUnitTypeWiki}}
<div class="ui small modal" id="delete-wiki-modal">
<dialog class="modal-dialog" id="delete-wiki-modal">
<div class="header">
{{ctx.Locale.Tr "repo.settings.wiki_delete"}}
</div>
@@ -1060,11 +1060,11 @@
"RepoOperationText" (ctx.Locale.Tr "repo.settings.confirm_wiki_delete")
)}}
</div>
</div>
</dialog>
{{end}}
{{if not .Repository.IsMirror}}
<div class="ui g-modal-confirm modal" id="archive-repo-modal">
<dialog class="modal-dialog" id="archive-repo-modal">
<div class="header">
{{if .Repository.IsArchived}}
{{ctx.Locale.Tr "repo.settings.unarchive.header"}}
@@ -1086,7 +1086,7 @@
<input type="hidden" name="repo_id" value="{{.Repository.ID}}">
{{template "base/modal_actions_confirm" .}}
</form>
</div>
</dialog>
{{end}}
{{end}}
@@ -1,4 +1,4 @@
<div class="ui small modal" id="push-mirror-edit-modal">
<dialog class="modal-dialog" id="push-mirror-edit-modal">
<div class="header">
{{ctx.Locale.Tr "repo.settings.mirror_settings.push_mirror.edit_sync_time"}}
</div>
@@ -24,4 +24,4 @@
</button>
</div>
</form>
</div>
</dialog>
+2 -2
View File
@@ -67,7 +67,7 @@
</div>
{{if $.Permission.CanWrite ctx.Consts.RepoUnitTypeCode}}
<div id="confirm-delete-tag-modal" class="ui small modal">
<dialog id="confirm-delete-tag-modal" class="modal-dialog">
<div class="header">
{{ctx.Locale.Tr "repo.release.delete_tag"}}
</div>
@@ -75,7 +75,7 @@
<p>{{ctx.Locale.Tr "repo.release.deletion_tag_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
{{end}}
{{template "base/footer" .}}
+2 -2
View File
@@ -1,4 +1,4 @@
<div class="ui small modal" id="repo-watch-options-modal">
<dialog class="modal-dialog" id="repo-watch-options-modal">
<div class="header">{{ctx.Locale.Tr "notifications"}}</div>
<form class="ui form form-fetch-action" method="post">
<div class="content">
@@ -19,4 +19,4 @@
</div>
{{template "base/modal_actions_confirm" dict "ModalButtonTypes" "confirm"}}
</form>
</div>
</dialog>
+2 -2
View File
@@ -96,10 +96,10 @@
</div>
</div>
<div class="ui small modal" id="repo-wiki-delete-page-modal">
<dialog class="modal-dialog" id="repo-wiki-delete-page-modal">
<div class="header">{{svg "octicon-trash"}} {{ctx.Locale.Tr "repo.wiki.delete_page_button"}}</div>
<div class="content"><p>{{ctx.Locale.Tr "repo.wiki.delete_page_notice_1" $title}}</p></div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
{{template "base/footer" .}}
+2 -2
View File
@@ -95,7 +95,7 @@
</table>
{{template "base/paginate" .}}
</div>
<div class="ui small modal" id="runner-delete-modal">
<dialog class="modal-dialog" id="runner-delete-modal">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "actions.runners.delete_runner_header"}}
@@ -104,5 +104,5 @@
<p>{{ctx.Locale.Tr "actions.runners.delete_runner_notice"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
</div>
+2 -2
View File
@@ -66,7 +66,7 @@
</div>
{{/* Add secret dialog */}}
<div class="ui small modal" id="add-secret-modal">
<dialog class="modal-dialog" id="add-secret-modal">
<div class="header"></div>
<form class="ui form form-fetch-action" method="post">
<div class="content">
@@ -105,4 +105,4 @@
</div>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form>
</div>
</dialog>
+4 -4
View File
@@ -101,7 +101,7 @@
</div>
</div>
<div class="ui small modal" id="delete-token">
<dialog class="modal-dialog" id="delete-token">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "settings.access_token_deletion"}}
@@ -110,10 +110,10 @@
<p>{{ctx.Locale.Tr "settings.access_token_deletion_desc"}}</p>
</div>
{{template "base/modal_actions_confirm"}}
</div>
</dialog>
{{if not .IsBot}}
<div class="ui small modal" id="regenerate-token">
<dialog class="modal-dialog" id="regenerate-token">
<div class="header">
{{ctx.Locale.Tr "settings.access_token_regeneration"}}
</div>
@@ -121,5 +121,5 @@
<p>{{ctx.Locale.Tr "settings.access_token_regeneration_desc"}}</p>
</div>
{{template "base/modal_actions_confirm"}}
</div>
</dialog>
{{end}}
+3 -3
View File
@@ -1,4 +1,4 @@
<div class="ui small modal" id="block-user-modal">
<dialog class="modal-dialog" id="block-user-modal">
<div class="header">{{ctx.Locale.Tr "user.block.title"}}</div>
<div class="content">
<div class="ui warning message">{{ctx.Locale.Tr "user.block.info"}} <a target="_blank" href="https://docs.gitea.com/usage/access-control/blocking-user">{{ctx.Locale.Tr "user.block.info.docs"}}</a></div>
@@ -10,7 +10,7 @@
</div>
<div class="field">
<label for="block-note">{{ctx.Locale.Tr "user.block.note.title"}}</label>
<input id="block-note" name="note">
<input id="block-note" name="note" autofocus>
<p class="help">{{ctx.Locale.Tr "user.block.note.info"}}</p>
</div>
<div class="actions">
@@ -19,4 +19,4 @@
</div>
</form>
</div>
</div>
</dialog>
+2 -2
View File
@@ -52,7 +52,7 @@
{{end}}
</div>
</div>
<div class="ui small modal" id="block-user-note-modal">
<dialog class="modal-dialog" id="block-user-note-modal">
<div class="header">{{ctx.Locale.Tr "user.block.note.edit"}}</div>
<div class="content">
<form class="ui form form-fetch-action" action="{{$.Link}}" method="post">
@@ -69,4 +69,4 @@
</div>
</form>
</div>
</div>
</dialog>
@@ -64,7 +64,7 @@
</div>
{{/** Edit variable dialog */}}
<div class="ui small modal" id="edit-variable-modal">
<dialog class="modal-dialog" id="edit-variable-modal">
<div class="header"></div>
<form class="ui form form-fetch-action" method="post">
<div class="content">
@@ -103,5 +103,5 @@
</div>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form>
</div>
</dialog>
+4 -4
View File
@@ -127,7 +127,7 @@
</button>
</div>
</form>
<div class="ui small modal" id="delete-account">
<dialog class="modal-dialog" id="delete-account">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "settings.delete_account_title"}}
@@ -136,12 +136,12 @@
<p>{{ctx.Locale.Tr "settings.delete_account_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
</div>
{{end}}
</div>
<div class="ui small modal" id="delete-email">
<dialog class="modal-dialog" id="delete-email">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "settings.email_deletion"}}
@@ -150,6 +150,6 @@
<p>{{ctx.Locale.Tr "settings.email_deletion_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
{{template "user/settings/layout_footer" .}}
@@ -35,7 +35,7 @@
{{end}}
</div>
<div class="ui small modal" id="remove-gitea-oauth2-application">
<dialog class="modal-dialog" id="remove-gitea-oauth2-application">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "settings.remove_oauth2_application"}}
@@ -44,7 +44,7 @@
<p>{{ctx.Locale.Tr "settings.oauth2_application_remove_description"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
</div>
<div class="ui bottom attached segment">
+2 -2
View File
@@ -27,7 +27,7 @@
{{end}}
</div>
<div class="ui small modal" id="revoke-gitea-oauth2-grant">
<dialog class="modal-dialog" id="revoke-gitea-oauth2-grant">
<div class="header">
{{svg "octicon-shield" 16 "tw-mr-1"}}
{{ctx.Locale.Tr "settings.revoke_oauth2_grant"}}
@@ -36,5 +36,5 @@
<p>{{ctx.Locale.Tr "settings.revoke_oauth2_grant_description"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
</div>
+2 -2
View File
@@ -123,7 +123,7 @@
{{end}}
{{end}}
</div>
<div class="ui small modal" id="delete-gpg">
<dialog class="modal-dialog" id="delete-gpg">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "settings.gpg_key_deletion"}}
@@ -132,5 +132,5 @@
<p>{{ctx.Locale.Tr "settings.gpg_key_deletion_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
</div>
+2 -2
View File
@@ -55,7 +55,7 @@
</div>
</div>
<div class="ui small modal" id="delete-principal">
<dialog class="modal-dialog" id="delete-principal">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "settings.ssh_principal_deletion"}}
@@ -64,5 +64,5 @@
<p>{{ctx.Locale.Tr "settings.ssh_principal_deletion_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
{{end}}
+2 -2
View File
@@ -103,7 +103,7 @@
{{end}}
{{end}}
</div>
<div class="ui small modal" id="delete-ssh">
<dialog class="modal-dialog" id="delete-ssh">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "settings.ssh_key_deletion"}}
@@ -112,5 +112,5 @@
<p>{{ctx.Locale.Tr "settings.ssh_key_deletion_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
</div>
+8 -6
View File
@@ -38,14 +38,16 @@
</div>
</div>
<form class="ui small modal form-fetch-action" method="post" id="leave-organization">
<dialog class="modal-dialog" id="leave-organization">
<div class="header">
{{ctx.Locale.Tr "org.members.leave"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "org.members.leave.detail" (HTMLFormat `<span class="%s"></span>` "organization-name")}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</form>
<form class="form-fetch-action" method="post">
<div class="content">
<p>{{ctx.Locale.Tr "org.members.leave.detail" (HTMLFormat `<span class="%s"></span>` "organization-name")}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</form>
</dialog>
{{template "user/settings/layout_footer" .}}
+4 -15
View File
@@ -35,7 +35,7 @@
<div class="tw-float-right">
{{if $.allowAdopt}}
<button type="button" class="ui button primary show-modal tw-p-2" data-modal="#adopt-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-plus"}}</span><span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting_label"}}</span></button>
<div class="ui g-modal-confirm modal" id="adopt-unadopted-modal-{{$dirI}}">
<dialog class="modal-dialog" id="adopt-unadopted-modal-{{$dirI}}">
<div class="header">
<span class="label">{{ctx.Locale.Tr "repo.adopt_preexisting"}}</span>
</div>
@@ -47,11 +47,11 @@
<input type="hidden" name="action" value="adopt">
{{template "base/modal_actions_confirm" $}}
</form>
</div>
</dialog>
{{end}}
{{if $.allowDelete}}
<button type="button" class="ui button red show-modal tw-p-2" data-modal="#delete-unadopted-modal-{{$dirI}}"><span class="icon">{{svg "octicon-x"}}</span><span class="label">{{ctx.Locale.Tr "repo.delete_preexisting_label"}}</span></button>
<div class="ui g-modal-confirm modal" id="delete-unadopted-modal-{{$dirI}}">
<dialog class="modal-dialog" id="delete-unadopted-modal-{{$dirI}}">
<div class="header">
<span class="label">{{ctx.Locale.Tr "repo.delete_preexisting"}}</span>
</div>
@@ -63,7 +63,7 @@
<input type="hidden" name="action" value="delete">
{{template "base/modal_actions_confirm" $}}
</form>
</div>
</dialog>
{{end}}
</div>
{{end}}
@@ -114,15 +114,4 @@
</div>
</div>
<div class="ui g-modal-confirm delete modal">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "settings.remove_account_link"}}
</div>
<div class="content">
<p>{{ctx.Locale.Tr "settings.remove_account_link_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
{{template "user/settings/layout_footer" .}}
@@ -48,7 +48,7 @@
{{end}}
</div>
<div class="ui small modal" id="delete-account-link">
<dialog class="modal-dialog" id="delete-account-link">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "settings.remove_account_link"}}
@@ -57,6 +57,6 @@
<p>{{ctx.Locale.Tr "settings.remove_account_link_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
</div>
{{end}}
+2 -2
View File
@@ -48,7 +48,7 @@
</button>
</form>
<div class="ui small modal" id="delete-openid">
<dialog class="modal-dialog" id="delete-openid">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "settings.openid_deletion"}}
@@ -57,5 +57,5 @@
<p>{{ctx.Locale.Tr "settings.openid_deletion_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
</div>
+2 -2
View File
@@ -22,7 +22,7 @@
</div>
{{end}}
<div class="ui small modal" id="disable-twofa">
<dialog class="modal-dialog" id="disable-twofa">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "settings.twofa_disable"}}
@@ -31,5 +31,5 @@
<p>{{ctx.Locale.Tr "settings.twofa_disable_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
</div>
@@ -35,7 +35,7 @@
</div>
<button type="button" id="register-webauthn" class="ui primary button">{{svg "octicon-key"}} {{ctx.Locale.Tr "settings.webauthn_register_key"}}</button>
</div>
<div class="ui small modal" id="rename-webauthn-nickname">
<dialog class="modal-dialog" id="rename-webauthn-nickname">
<div class="header">
{{svg "octicon-pencil"}}
{{ctx.Locale.Tr "rename"}}
@@ -48,8 +48,8 @@
</div>
{{template "base/modal_actions_confirm" (dict "ModalButtonTypes" "confirm")}}
</form>
</div>
<div class="ui small modal" id="delete-registration">
</dialog>
<dialog class="modal-dialog" id="delete-registration">
<div class="header">
{{svg "octicon-trash"}}
{{ctx.Locale.Tr "settings.webauthn_delete_key"}}
@@ -58,5 +58,5 @@
<p>{{ctx.Locale.Tr "settings.webauthn_delete_key_desc"}}</p>
</div>
{{template "base/modal_actions_confirm" .}}
</div>
</dialog>
</div>
+23 -1
View File
@@ -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);
});
+19
View File
@@ -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');
});
+20 -2
View File
@@ -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.');
});
+14
View File
@@ -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<string, unknown>) {
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<string, string>} = {}): Promise<number> {
const response = await apiRetry(() => requestContext.post(`${baseUrl()}/api/v1/repos/${owner}/${repo}/pulls`, {
+1 -1
View File
@@ -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;
+7 -10
View File
@@ -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;
}
+23 -27
View File
@@ -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;
+1 -12
View File
@@ -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;
}
File diff suppressed because it is too large. Load diff
-1
View File
@@ -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"
+3 -3
View File
@@ -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<HTMLDivElement>('#detail-modal')!;
const detailModal = document.querySelector<HTMLDialogElement>('#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
+4 -4
View File
@@ -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<HTMLDialogElement>(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());
+1 -1
View File
@@ -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<HTMLElement>('[autofocus]')?.focus();
// Also, apply our autoFocusEnd behavior
+7 -11
View File
@@ -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`<div class="header">${header}</div>` : '';
return createElementFromHTML(html`
<div class="ui g-modal-confirm modal">
<dialog class="modal-dialog">
${htmlRaw(headerHtml)}
<div class="content">${content}</div>
<div class="actions">
<button class="ui cancel button">${htmlRaw(svg('octicon-x'))} ${i18n.modal_cancel}</button>
<button class="ui ${confirmButtonColor} ok button" autofocus>${htmlRaw(svg('octicon-check'))} ${i18n.modal_confirm}</button>
</div>
</div>
</dialog>
`.trim());
}
export function confirmModal(modal: HTMLElement | ConfirmModalOptions): Promise<boolean> {
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<boolean> {
if (!(modal instanceof HTMLDialogElement)) modal = createConfirmModal(modal);
return new Promise((resolve) => {
showFomanticModal(modal, {
showModal(modal, {
onApprove() {
resolve(true);
},
+3 -3
View File
@@ -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<HTMLInputElement>('input[name="id"]')!;
const elNameInput = elModal.querySelector<HTMLInputElement>('.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();
+2 -2
View File
@@ -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')!)!);
});
}
}
+8 -9
View File
@@ -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`
<div class="ui modal content-history-detail-dialog">
const elDetailDialog = createElementFromHTML<HTMLDialogElement>(html`
<dialog class="modal-dialog content-history-detail-dialog">
<div class="header flex-left-right">
<div>${htmlRaw(itemTitleHtml)}</div>
<div class="ui dropdown dialog-header-options tw-mx-4 tw-hidden">
@@ -28,9 +29,8 @@ function showContentHistoryDetail(issueBaseUrl: string, commentId: string, histo
<button type="button" class="close-modal"></button>
</div>
<div class="comment-diff-data is-loading"></div>
</div>
</dialog>
`);
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();
+3 -3
View File
@@ -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<HTMLDialogElement>(modalSelector)!;
const textarea = modal.querySelector<HTMLTextAreaElement>('textarea[name="content"]')!;
textarea.value = `${content}\n\n_Originally posted by @${poster} in ${reference}_`;
showFomanticModal(modal);
showModal(modal);
});
}
+3 -3
View File
@@ -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<HTMLElement>('.ui.modal#project-column-modal-edit')!;
const elModal = document.querySelector<HTMLDialogElement>('#project-column-modal-edit')!;
const elForm = elModal.querySelector<HTMLFormElement>('form')!;
const elColumnId = elForm.querySelector<HTMLInputElement>('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');
}
+6 -6
View File
@@ -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<HTMLDialogElement>('#generate-release-notes-modal')!;
const doSubmit = async (tagName: string) => {
const elPreviousTag = elModal.querySelector<HTMLSelectElement>('[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;
-3
View File
@@ -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
}
+2
View File
@@ -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,
+3 -3
View File
@@ -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<HTMLDialogElement>('.modal-dialog:not([data-skip-button])')) {
const btn = createElementFromHTML(html`<button class="ui button">${el.id}</button`);
btn.addEventListener('click', () => showFomanticModal(el));
btn.addEventListener('click', () => showModal(el));
modalButtons.append(btn);
}
}
+2 -2
View File
@@ -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<HTMLElement>(`#${CSS.escape(dataModalConfirm.substring(1))}`);
elModal = document.querySelector<HTMLDialogElement>(`#${CSS.escape(dataModalConfirm.substring(1))}`);
if (elModal) {
elModal = createElementFromHTML(elModal.outerHTML);
elModal.removeAttribute('id');
-4
View File
@@ -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();
}
-31
View File
@@ -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;
};
}
-86
View File
@@ -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<void>;
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<FomanticInitFunction>) {
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<HTMLElement>(selectors.approve);
const elForm = elApprove?.closest<HTMLFormElement>('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;
}
+3 -13
View File
@@ -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<string, any>;
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);
});
+87
View File
@@ -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 = `
<button id="opener">Opener</button>
<dialog class="modal-dialog">
<div class="header">Header <button type="button" class="close-modal"></button></div>
<form class="content">
<input name="text">
<div class="actions"><button class="ui cancel button">Cancel</button><button type="button" class="ui ok button">OK</button></div>
</form>
</dialog>
`;
const opener = document.querySelector<HTMLElement>('#opener')!;
const modal = document.querySelector<HTMLDialogElement>('.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<HTMLElement>('.ok')!.click();
expect(modal.open).toBe(true);
approve = true;
modal.querySelector<HTMLElement>('.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<HTMLElement>(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<HTMLDialogElement>('<dialog class="modal-dialog"></dialog>');
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);
});
+112
View File
@@ -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<void>,
onHidden?: () => void,
};
const modalStates = new WeakMap<Element, {opts: ModalOpts, opener: Element | null}>();
const topModal = () => Array.from(document.querySelectorAll<HTMLDialogElement>('.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();
}
+8 -10
View File
@@ -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(() => {
<select><option>Value</option></select>
<div contenteditable="true"><span>Editable</span></div>
`;
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<HTMLElement>('#global-shortcut-help')!;
expect(modal.classList.contains('active')).toBe(true);
const modal = document.querySelector<HTMLDialogElement>('#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', '<b>Updated search</b>');
press('?');
expect(document.querySelectorAll('.ui.modal')).toHaveLength(1);
expect(document.querySelectorAll('.modal-dialog')).toHaveLength(1);
expect(modal.querySelector('.shortcut-list')!.textContent).toContain('<b>Updated search</b>');
expect(modal.querySelector('.shortcut-list b')).toBeNull();
expect(modal.querySelectorAll('.shortcut-list tr')).toHaveLength(3);
+7 -8
View File
@@ -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<HTMLElement>('#global-shortcut-help');
let modal = document.querySelector<HTMLDialogElement>('#global-shortcut-help');
if (!modal) {
modal = createElementFromHTML<HTMLElement>(html`
<div id="global-shortcut-help" class="ui small modal" role="dialog" aria-modal="true">
modal = createElementFromHTML<HTMLDialogElement>(html`
<dialog id="global-shortcut-help" class="modal-dialog">
<div class="header">
Keyboard Shortcuts
<button type="button" class="close-modal"></button>
@@ -82,9 +82,8 @@ function showShortcutHelp() {
<tbody class="shortcut-list"></tbody>
</table>
</div>
</div>
</dialog>
`);
document.body.append(modal);
}
const seenShortcuts = new Set<string>();
@@ -112,7 +111,7 @@ function showShortcutHelp() {
}
list.append(createElementFromHTML(html`<tr><td>${htmlRaw(keysHtml)}</td><td>${label}</td></tr>`));
}
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;
+1 -7
View File
@@ -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`
<div class='toast-icon'>${svgRaw(icon)}</div>
<div class='toast-body'><span class="toast-duplicate-number tw-hidden">1</span>${htmlRaw(bodyHtml)}</div>
@@ -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);
}