aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--routers/web/repo/compare.go15
-rw-r--r--routers/web/repo/issue.go181
-rw-r--r--routers/web/repo/issue_label.go6
-rw-r--r--routers/web/repo/issue_label_test.go2
-rw-r--r--routers/web/web.go2
-rw-r--r--templates/repo/issue/labels/label.tmpl7
-rw-r--r--templates/repo/issue/labels/labels_selector_field.tmpl46
-rw-r--r--templates/repo/issue/labels/labels_sidebar.tmpl11
-rw-r--r--templates/repo/issue/new_form.tmpl16
-rw-r--r--templates/repo/issue/sidebar/label_list.tmpl51
-rw-r--r--templates/repo/issue/sidebar/label_list_item.tmpl11
-rw-r--r--templates/repo/issue/sidebar/reviewer_list.tmpl16
-rw-r--r--templates/repo/issue/view_content/sidebar.tmpl5
-rw-r--r--web_src/css/repo.css8
-rw-r--r--web_src/css/repo/issue-label.css5
-rw-r--r--web_src/js/features/common-page.ts4
-rw-r--r--web_src/js/features/repo-issue-sidebar-combolist.ts46
-rw-r--r--web_src/js/features/repo-issue-sidebar.md27
-rw-r--r--web_src/js/features/repo-issue-sidebar.ts20
-rw-r--r--web_src/js/features/repo-issue.ts15
-rw-r--r--web_src/js/index.ts3
-rw-r--r--web_src/js/utils/dom.ts10
22 files changed, 275 insertions, 232 deletions
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index 3477ba36e8..9a7d3dfbf6 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -788,7 +788,11 @@ func CompareDiff(ctx *context.Context) {
if !nothingToCompare {
// Setup information for new form.
- RetrieveRepoMetas(ctx, ctx.Repo.Repository, true)
+ retrieveRepoMetasForIssueWriter(ctx, ctx.Repo.Repository, true)
+ if ctx.Written() {
+ return
+ }
+ labelsData := retrieveRepoLabels(ctx, ctx.Repo.Repository, 0, true)
if ctx.Written() {
return
}
@@ -796,6 +800,10 @@ func CompareDiff(ctx *context.Context) {
if ctx.Written() {
return
}
+ _, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates, labelsData)
+ if len(templateErrs) > 0 {
+ ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
+ }
}
}
beforeCommitID := ctx.Data["BeforeCommitID"].(string)
@@ -808,11 +816,6 @@ func CompareDiff(ctx *context.Context) {
ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + separator + base.ShortSha(afterCommitID)
ctx.Data["IsDiffCompare"] = true
- _, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates)
-
- if len(templateErrs) > 0 {
- ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
- }
if content, ok := ctx.Data["content"].(string); ok && content != "" {
// If a template content is set, prepend the "content". In this case that's only
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 7fa8d428d3..a4e2fd8cea 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -870,51 +870,112 @@ func RetrieveRepoReviewers(ctx *context.Context, repo *repo_model.Repository, is
ctx.Data["IssueSidebarReviewersData"] = data
}
-// RetrieveRepoMetas find all the meta information of a repository
-func RetrieveRepoMetas(ctx *context.Context, repo *repo_model.Repository, isPull bool) []*issues_model.Label {
- if !ctx.Repo.CanWriteIssuesOrPulls(isPull) {
- return nil
+type issueSidebarLabelsData struct {
+ Repository *repo_model.Repository
+ RepoLink string
+ IssueID int64
+ IsPullRequest bool
+ AllLabels []*issues_model.Label
+ RepoLabels []*issues_model.Label
+ OrgLabels []*issues_model.Label
+ SelectedLabelIDs string
+}
+
+func makeSelectedStringIDs[KeyType, ItemType comparable](
+ allLabels []*issues_model.Label, candidateKey func(candidate *issues_model.Label) KeyType,
+ selectedItems []ItemType, selectedKey func(selected ItemType) KeyType,
+) string {
+ selectedIDSet := make(container.Set[string])
+ allLabelMap := map[KeyType]*issues_model.Label{}
+ for _, label := range allLabels {
+ allLabelMap[candidateKey(label)] = label
+ }
+ for _, item := range selectedItems {
+ if label, ok := allLabelMap[selectedKey(item)]; ok {
+ label.IsChecked = true
+ selectedIDSet.Add(strconv.FormatInt(label.ID, 10))
+ }
+ }
+ ids := selectedIDSet.Values()
+ sort.Strings(ids)
+ return strings.Join(ids, ",")
+}
+
+func (d *issueSidebarLabelsData) SetSelectedLabels(labels []*issues_model.Label) {
+ d.SelectedLabelIDs = makeSelectedStringIDs(
+ d.AllLabels, func(label *issues_model.Label) int64 { return label.ID },
+ labels, func(label *issues_model.Label) int64 { return label.ID },
+ )
+}
+
+func (d *issueSidebarLabelsData) SetSelectedLabelNames(labelNames []string) {
+ d.SelectedLabelIDs = makeSelectedStringIDs(
+ d.AllLabels, func(label *issues_model.Label) string { return strings.ToLower(label.Name) },
+ labelNames, strings.ToLower,
+ )
+}
+
+func (d *issueSidebarLabelsData) SetSelectedLabelIDs(labelIDs []int64) {
+ d.SelectedLabelIDs = makeSelectedStringIDs(
+ d.AllLabels, func(label *issues_model.Label) int64 { return label.ID },
+ labelIDs, func(labelID int64) int64 { return labelID },
+ )
+}
+
+func retrieveRepoLabels(ctx *context.Context, repo *repo_model.Repository, issueID int64, isPull bool) *issueSidebarLabelsData {
+ labelsData := &issueSidebarLabelsData{
+ Repository: repo,
+ RepoLink: ctx.Repo.RepoLink,
+ IssueID: issueID,
+ IsPullRequest: isPull,
}
+ ctx.Data["IssueSidebarLabelsData"] = labelsData
labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
if err != nil {
ctx.ServerError("GetLabelsByRepoID", err)
return nil
}
- ctx.Data["Labels"] = labels
+ labelsData.RepoLabels = labels
+
if repo.Owner.IsOrganization() {
orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
if err != nil {
return nil
}
+ labelsData.OrgLabels = orgLabels
+ }
+ labelsData.AllLabels = append(labelsData.AllLabels, labelsData.RepoLabels...)
+ labelsData.AllLabels = append(labelsData.AllLabels, labelsData.OrgLabels...)
+ return labelsData
+}
- ctx.Data["OrgLabels"] = orgLabels
- labels = append(labels, orgLabels...)
+// retrieveRepoMetasForIssueWriter finds some the meta information of a repository for an issue/pr writer
+func retrieveRepoMetasForIssueWriter(ctx *context.Context, repo *repo_model.Repository, isPull bool) {
+ if !ctx.Repo.CanWriteIssuesOrPulls(isPull) {
+ return
}
RetrieveRepoMilestonesAndAssignees(ctx, repo)
if ctx.Written() {
- return nil
+ return
}
retrieveProjects(ctx, repo)
if ctx.Written() {
- return nil
+ return
}
PrepareBranchList(ctx)
if ctx.Written() {
- return nil
+ return
}
-
// Contains true if the user can create issue dependencies
ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx, ctx.Doer, isPull)
-
- return labels
}
// Tries to load and set an issue template. The first return value indicates if a template was loaded.
-func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string) (bool, map[string]error) {
+func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string, labelsData *issueSidebarLabelsData) (bool, map[string]error) {
commit, err := ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
if err != nil {
return false, nil
@@ -951,26 +1012,9 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles
ctx.Data["Fields"] = template.Fields
ctx.Data["TemplateFile"] = template.FileName
}
- labelIDs := make([]string, 0, len(template.Labels))
- if repoLabels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, "", db.ListOptions{}); err == nil {
- ctx.Data["Labels"] = repoLabels
- if ctx.Repo.Owner.IsOrganization() {
- if orgLabels, err := issues_model.GetLabelsByOrgID(ctx, ctx.Repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{}); err == nil {
- ctx.Data["OrgLabels"] = orgLabels
- repoLabels = append(repoLabels, orgLabels...)
- }
- }
- for _, metaLabel := range template.Labels {
- for _, repoLabel := range repoLabels {
- if strings.EqualFold(repoLabel.Name, metaLabel) {
- repoLabel.IsChecked = true
- labelIDs = append(labelIDs, strconv.FormatInt(repoLabel.ID, 10))
- break
- }
- }
- }
- }
+ labelsData.SetSelectedLabelNames(template.Labels)
+
selectedAssigneeIDs := make([]int64, 0, len(template.Assignees))
selectedAssigneeIDStrings := make([]string, 0, len(template.Assignees))
if userIDs, err := user_model.GetUserIDsByNames(ctx, template.Assignees, false); err == nil {
@@ -983,8 +1027,7 @@ func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles
if template.Ref != "" && !strings.HasPrefix(template.Ref, "refs/") { // Assume that the ref intended is always a branch - for tags users should use refs/tags/<ref>
template.Ref = git.BranchPrefix + template.Ref
}
- ctx.Data["HasSelectedLabel"] = len(labelIDs) > 0
- ctx.Data["label_ids"] = strings.Join(labelIDs, ",")
+
ctx.Data["HasSelectedAssignee"] = len(selectedAssigneeIDs) > 0
ctx.Data["assignee_ids"] = strings.Join(selectedAssigneeIDStrings, ",")
ctx.Data["SelectedAssigneeIDs"] = selectedAssigneeIDs
@@ -1042,8 +1085,14 @@ func NewIssue(ctx *context.Context) {
}
}
- RetrieveRepoMetas(ctx, ctx.Repo.Repository, false)
-
+ retrieveRepoMetasForIssueWriter(ctx, ctx.Repo.Repository, false)
+ if ctx.Written() {
+ return
+ }
+ labelsData := retrieveRepoLabels(ctx, ctx.Repo.Repository, 0, false)
+ if ctx.Written() {
+ return
+ }
tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
if err != nil {
ctx.ServerError("GetTagNamesByRepoID", err)
@@ -1052,7 +1101,7 @@ func NewIssue(ctx *context.Context) {
ctx.Data["Tags"] = tags
ret := issue_service.ParseTemplatesFromDefaultBranch(ctx.Repo.Repository, ctx.Repo.GitRepo)
- templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates)
+ templateLoaded, errs := setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates, labelsData)
for k, v := range errs {
ret.TemplateErrors[k] = v
}
@@ -1161,34 +1210,25 @@ func ValidateRepoMetas(ctx *context.Context, form forms.CreateIssueForm, isPull
err error
)
- labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository, isPull)
+ retrieveRepoMetasForIssueWriter(ctx, ctx.Repo.Repository, isPull)
+ if ctx.Written() {
+ return ret
+ }
+ labelsData := retrieveRepoLabels(ctx, ctx.Repo.Repository, 0, isPull)
if ctx.Written() {
return ret
}
var labelIDs []int64
- hasSelected := false
// Check labels.
if len(form.LabelIDs) > 0 {
labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
if err != nil {
return ret
}
- labelIDMark := make(container.Set[int64])
- labelIDMark.AddMultiple(labelIDs...)
-
- for i := range labels {
- if labelIDMark.Contains(labels[i].ID) {
- labels[i].IsChecked = true
- hasSelected = true
- }
- }
+ labelsData.SetSelectedLabelIDs(labelIDs)
}
- ctx.Data["Labels"] = labels
- ctx.Data["HasSelectedLabel"] = hasSelected
- ctx.Data["label_ids"] = form.LabelIDs
-
// Check milestone.
milestoneID := form.MilestoneID
if milestoneID > 0 {
@@ -1579,38 +1619,15 @@ func ViewIssue(ctx *context.Context) {
}
}
- // Metas.
- // Check labels.
- labelIDMark := make(container.Set[int64])
- for _, label := range issue.Labels {
- labelIDMark.Add(label.ID)
- }
- labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
- if err != nil {
- ctx.ServerError("GetLabelsByRepoID", err)
+ retrieveRepoMetasForIssueWriter(ctx, repo, issue.IsPull)
+ if ctx.Written() {
return
}
- ctx.Data["Labels"] = labels
-
- if repo.Owner.IsOrganization() {
- orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
- if err != nil {
- ctx.ServerError("GetLabelsByOrgID", err)
- return
- }
- ctx.Data["OrgLabels"] = orgLabels
-
- labels = append(labels, orgLabels...)
- }
-
- hasSelected := false
- for i := range labels {
- if labelIDMark.Contains(labels[i].ID) {
- labels[i].IsChecked = true
- hasSelected = true
- }
+ labelsData := retrieveRepoLabels(ctx, repo, issue.ID, issue.IsPull)
+ if ctx.Written() {
+ return
}
- ctx.Data["HasSelectedLabel"] = hasSelected
+ labelsData.SetSelectedLabels(issue.Labels)
// Check milestone and assignee.
if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go
index 81bee4dbb5..4874baaa54 100644
--- a/routers/web/repo/issue_label.go
+++ b/routers/web/repo/issue_label.go
@@ -53,11 +53,11 @@ func InitializeLabels(ctx *context.Context) {
ctx.Redirect(ctx.Repo.RepoLink + "/labels")
}
-// RetrieveLabels find all the labels of a repository and organization
-func RetrieveLabels(ctx *context.Context) {
+// RetrieveLabelsForList find all the labels of a repository and organization, it is only used by "/labels" page to list all labels
+func RetrieveLabelsForList(ctx *context.Context) {
labels, err := issues_model.GetLabelsByRepoID(ctx, ctx.Repo.Repository.ID, ctx.FormString("sort"), db.ListOptions{})
if err != nil {
- ctx.ServerError("RetrieveLabels.GetLabels", err)
+ ctx.ServerError("RetrieveLabelsForList.GetLabels", err)
return
}
diff --git a/routers/web/repo/issue_label_test.go b/routers/web/repo/issue_label_test.go
index 93fc72300b..c86a03da51 100644
--- a/routers/web/repo/issue_label_test.go
+++ b/routers/web/repo/issue_label_test.go
@@ -62,7 +62,7 @@ func TestRetrieveLabels(t *testing.T) {
contexttest.LoadUser(t, ctx, 2)
contexttest.LoadRepo(t, ctx, testCase.RepoID)
ctx.Req.Form.Set("sort", testCase.Sort)
- RetrieveLabels(ctx)
+ RetrieveLabelsForList(ctx)
assert.False(t, ctx.Written())
labels, ok := ctx.Data["Labels"].([]*issues_model.Label)
assert.True(t, ok)
diff --git a/routers/web/web.go b/routers/web/web.go
index 29dd8a8edc..907bf88f6f 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1163,7 +1163,7 @@ func registerRoutes(m *web.Router) {
m.Get("/issues/posters", repo.IssuePosters) // it can't use {type:issues|pulls} because it would conflict with other routes like "/pulls/{index}"
m.Get("/pulls/posters", repo.PullPosters)
m.Get("/comments/{id}/attachments", repo.GetCommentAttachments)
- m.Get("/labels", repo.RetrieveLabels, repo.Labels)
+ m.Get("/labels", repo.RetrieveLabelsForList, repo.Labels)
m.Get("/milestones", repo.Milestones)
m.Get("/milestone/{id}", context.RepoRef(), repo.MilestoneIssuesAndPulls)
m.Group("/{type:issues|pulls}", func() {
diff --git a/templates/repo/issue/labels/label.tmpl b/templates/repo/issue/labels/label.tmpl
deleted file mode 100644
index f40c792da7..0000000000
--- a/templates/repo/issue/labels/label.tmpl
+++ /dev/null
@@ -1,7 +0,0 @@
-<a
- class="item {{if not .label.IsChecked}}tw-hidden{{end}}"
- id="label_{{.label.ID}}"
- href="{{.root.RepoLink}}/{{if or .root.IsPull .root.Issue.IsPull}}pulls{{else}}issues{{end}}?labels={{.label.ID}}"{{/* FIXME: use .root.Issue.Link or create .root.Link */}}
->
- {{- ctx.RenderUtils.RenderLabel .label -}}
-</a>
diff --git a/templates/repo/issue/labels/labels_selector_field.tmpl b/templates/repo/issue/labels/labels_selector_field.tmpl
deleted file mode 100644
index 96fb658664..0000000000
--- a/templates/repo/issue/labels/labels_selector_field.tmpl
+++ /dev/null
@@ -1,46 +0,0 @@
-<div class="ui {{if or (not .HasIssuesOrPullsWritePermission) .Repository.IsArchived}}disabled{{end}} floating jump select-label dropdown">
- <span class="text muted flex-text-block">
- <strong>{{ctx.Locale.Tr "repo.issues.new.labels"}}</strong>
- {{if and .HasIssuesOrPullsWritePermission (not .Repository.IsArchived)}}
- {{svg "octicon-gear" 16 "tw-ml-1"}}
- {{end}}
- </span>
- <div class="filter menu" {{if .Issue}}data-action="update" data-issue-id="{{$.Issue.ID}}" data-update-url="{{$.RepoLink}}/issues/labels"{{else}}data-id="#label_ids"{{end}}>
- {{if or .Labels .OrgLabels}}
- <div class="ui icon search input">
- <i class="icon">{{svg "octicon-search" 16}}</i>
- <input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_labels"}}">
- </div>
- {{end}}
- <a class="no-select item" href="#">{{ctx.Locale.Tr "repo.issues.new.clear_labels"}}</a>
- {{if or .Labels .OrgLabels}}
- {{$previousExclusiveScope := "_no_scope"}}
- {{range .Labels}}
- {{$exclusiveScope := .ExclusiveScope}}
- {{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}}
- <div class="divider"></div>
- {{end}}
- {{$previousExclusiveScope = $exclusiveScope}}
- <a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}tw-invisible{{end}}">{{svg (Iif $exclusiveScope "octicon-dot-fill" "octicon-check")}}</span>&nbsp;&nbsp;{{ctx.RenderUtils.RenderLabel .}}
- {{if .Description}}<br><small class="desc">{{.Description | ctx.RenderUtils.RenderEmoji}}</small>{{end}}
- <p class="archived-label-hint">{{template "repo/issue/labels/label_archived" .}}</p>
- </a>
- {{end}}
- <div class="divider"></div>
- {{$previousExclusiveScope = "_no_scope"}}
- {{range .OrgLabels}}
- {{$exclusiveScope := .ExclusiveScope}}
- {{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}}
- <div class="divider"></div>
- {{end}}
- {{$previousExclusiveScope = $exclusiveScope}}
- <a class="{{if .IsChecked}}checked{{end}} item" href="#" data-id="{{.ID}}" {{if .IsArchived}}data-is-archived{{end}} data-id-selector="#label_{{.ID}}" data-scope="{{$exclusiveScope}}"><span class="octicon-check {{if not .IsChecked}}tw-invisible{{end}}">{{svg (Iif $exclusiveScope "octicon-dot-fill" "octicon-check")}}</span>&nbsp;&nbsp;{{ctx.RenderUtils.RenderLabel .}}
- {{if .Description}}<br><small class="desc">{{.Description | ctx.RenderUtils.RenderEmoji}}</small>{{end}}
- <p class="archived-label-hint">{{template "repo/issue/labels/label_archived" .}}</p>
- </a>
- {{end}}
- {{else}}
- <div class="disabled item">{{ctx.Locale.Tr "repo.issues.new.no_items"}}</div>
- {{end}}
- </div>
-</div>
diff --git a/templates/repo/issue/labels/labels_sidebar.tmpl b/templates/repo/issue/labels/labels_sidebar.tmpl
deleted file mode 100644
index 0b7b9b8969..0000000000
--- a/templates/repo/issue/labels/labels_sidebar.tmpl
+++ /dev/null
@@ -1,11 +0,0 @@
-<div class="ui labels list">
- <span class="labels-list">
- <span class="no-select {{if .root.HasSelectedLabel}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_label"}}</span>
- {{range .root.Labels}}
- {{template "repo/issue/labels/label" dict "root" $.root "label" .}}
- {{end}}
- {{range .root.OrgLabels}}
- {{template "repo/issue/labels/label" dict "root" $.root "label" .}}
- {{end}}
- </span>
-</div>
diff --git a/templates/repo/issue/new_form.tmpl b/templates/repo/issue/new_form.tmpl
index 190d52cf47..65d359e9dc 100644
--- a/templates/repo/issue/new_form.tmpl
+++ b/templates/repo/issue/new_form.tmpl
@@ -18,15 +18,15 @@
<input type="hidden" name="template-file" value="{{.TemplateFile}}">
{{range .Fields}}
{{if eq .Type "input"}}
- {{template "repo/issue/fields/input" "item" .}}
+ {{template "repo/issue/fields/input" dict "item" .}}
{{else if eq .Type "markdown"}}
- {{template "repo/issue/fields/markdown" "item" .}}
+ {{template "repo/issue/fields/markdown" dict "item" .}}
{{else if eq .Type "textarea"}}
- {{template "repo/issue/fields/textarea" "item" . "root" $}}
+ {{template "repo/issue/fields/textarea" dict "item" . "root" $}}
{{else if eq .Type "dropdown"}}
- {{template "repo/issue/fields/dropdown" "item" .}}
+ {{template "repo/issue/fields/dropdown" dict "item" .}}
{{else if eq .Type "checkboxes"}}
- {{template "repo/issue/fields/checkboxes" "item" .}}
+ {{template "repo/issue/fields/checkboxes" dict "item" .}}
{{end}}
{{end}}
{{else}}
@@ -49,13 +49,11 @@
<div class="issue-content-right ui segment">
{{template "repo/issue/branch_selector_field" $}}
{{if .PageIsComparePull}}
- {{template "repo/issue/sidebar/reviewer_list" dict "IssueSidebarReviewersData" $.IssueSidebarReviewersData}}
+ {{template "repo/issue/sidebar/reviewer_list" $.IssueSidebarReviewersData}}
<div class="divider"></div>
{{end}}
- <input id="label_ids" name="label_ids" type="hidden" value="{{.label_ids}}">
- {{template "repo/issue/labels/labels_selector_field" .}}
- {{template "repo/issue/labels/labels_sidebar" dict "root" $}}
+ {{template "repo/issue/sidebar/label_list" $.IssueSidebarLabelsData}}
<div class="divider"></div>
diff --git a/templates/repo/issue/sidebar/label_list.tmpl b/templates/repo/issue/sidebar/label_list.tmpl
new file mode 100644
index 0000000000..e9f4baa433
--- /dev/null
+++ b/templates/repo/issue/sidebar/label_list.tmpl
@@ -0,0 +1,51 @@
+{{$data := .}}
+{{$canChange := and ctx.RootData.HasIssuesOrPullsWritePermission (not $data.Repository.IsArchived)}}
+<div class="issue-sidebar-combo" {{if $data.IssueID}}data-update-url="{{$data.RepoLink}}/issues/labels?issue_ids={{$data.IssueID}}"{{end}}>
+ <input class="combo-value" name="label_ids" type="hidden" value="{{$data.SelectedLabelIDs}}">
+ <div class="ui dropdown {{if not $canChange}}disabled{{end}}">
+ <a class="text muted">
+ <strong>{{ctx.Locale.Tr "repo.issues.new.labels"}}</strong> {{if $canChange}}{{svg "octicon-gear"}}{{end}}
+ </a>
+ <div class="menu">
+ {{if not $data.AllLabels}}
+ <div class="item disabled">{{ctx.Locale.Tr "repo.issues.new.no_items"}}</div>
+ {{else}}
+ <div class="ui icon search input">
+ <i class="icon">{{svg "octicon-search" 16}}</i>
+ <input type="text" placeholder="{{ctx.Locale.Tr "repo.issues.filter_labels"}}">
+ </div>
+ <a class="item clear-selection" href="#">{{ctx.Locale.Tr "repo.issues.new.clear_labels"}}</a>
+ {{$previousExclusiveScope := "_no_scope"}}
+ {{range .RepoLabels}}
+ {{$exclusiveScope := .ExclusiveScope}}
+ {{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}}
+ <div class="divider"></div>
+ {{end}}
+ {{$previousExclusiveScope = $exclusiveScope}}
+ {{template "repo/issue/sidebar/label_list_item" dict "Label" .}}
+ {{end}}
+ <div class="divider"></div>
+ {{$previousExclusiveScope = "_no_scope"}}
+ {{range .OrgLabels}}
+ {{$exclusiveScope := .ExclusiveScope}}
+ {{if and (ne $previousExclusiveScope "_no_scope") (ne $previousExclusiveScope $exclusiveScope)}}
+ <div class="divider"></div>
+ {{end}}
+ {{$previousExclusiveScope = $exclusiveScope}}
+ {{template "repo/issue/sidebar/label_list_item" dict "Label" .}}
+ {{end}}
+ {{end}}
+ </div>
+ </div>
+
+ <div class="ui list labels-list tw-my-2 tw-flex tw-gap-2">
+ <span class="item empty-list {{if $data.SelectedLabelIDs}}tw-hidden{{end}}">{{ctx.Locale.Tr "repo.issues.new.no_label"}}</span>
+ {{range $data.AllLabels}}
+ {{if .IsChecked}}
+ <a class="item" href="{{$data.RepoLink}}/{{if $data.IsPullRequest}}pulls{{else}}issues{{end}}?labels={{.ID}}">
+ {{- ctx.RenderUtils.RenderLabel . -}}
+ </a>
+ {{end}}
+ {{end}}
+ </div>
+</div>
diff --git a/templates/repo/issue/sidebar/label_list_item.tmpl b/templates/repo/issue/sidebar/label_list_item.tmpl
new file mode 100644
index 0000000000..ad878e918b
--- /dev/null
+++ b/templates/repo/issue/sidebar/label_list_item.tmpl
@@ -0,0 +1,11 @@
+{{$label := .Label}}
+<a class="item {{if $label.IsChecked}}checked{{else if $label.IsArchived}}tw-hidden{{end}}" href="#"
+ data-scope="{{$label.ExclusiveScope}}" data-value="{{$label.ID}}" {{if $label.IsArchived}}data-is-archived{{end}}
+>
+ <span class="item-check-mark">{{svg (Iif $label.ExclusiveScope "octicon-dot-fill" "octicon-check")}}</span>
+ {{ctx.RenderUtils.RenderLabel $label}}
+ <div class="item-secondary-info">
+ {{if $label.Description}}<div class="tw-pl-[20px]"><small>{{$label.Description | ctx.RenderUtils.RenderEmoji}}</small></div>{{end}}
+ <div class="archived-label-hint">{{template "repo/issue/labels/label_archived" $label}}</div>
+ </div>
+</a>
diff --git a/templates/repo/issue/sidebar/reviewer_list.tmpl b/templates/repo/issue/sidebar/reviewer_list.tmpl
index 2d3218e927..cf7b97c02b 100644
--- a/templates/repo/issue/sidebar/reviewer_list.tmpl
+++ b/templates/repo/issue/sidebar/reviewer_list.tmpl
@@ -1,11 +1,9 @@
-{{$data := .IssueSidebarReviewersData}}
+{{$data := .}}
{{$hasCandidates := or $data.Reviewers $data.TeamReviewers}}
-<div class="issue-sidebar-combo" data-sidebar-combo-for="reviewers"
- {{if $data.IssueID}}data-update-url="{{$data.RepoLink}}/issues/request_review?issue_ids={{$data.IssueID}}"{{end}}
->
+<div class="issue-sidebar-combo" {{if $data.IssueID}}data-update-url="{{$data.RepoLink}}/issues/request_review?issue_ids={{$data.IssueID}}"{{end}}>
<input type="hidden" class="combo-value" name="reviewer_ids">{{/* match CreateIssueForm */}}
- <div class="ui dropdown custom {{if or (not $hasCandidates) (not $data.CanChooseReviewer)}}disabled{{end}}">
- <a class="muted text">
+ <div class="ui dropdown {{if or (not $hasCandidates) (not $data.CanChooseReviewer)}}disabled{{end}}">
+ <a class="text muted">
<strong>{{ctx.Locale.Tr "repo.issues.review.reviewers"}}</strong> {{if and $data.CanChooseReviewer}}{{svg "octicon-gear"}}{{end}}
</a>
<div class="menu flex-items-menu">
@@ -19,7 +17,8 @@
{{if .User}}
<a class="item muted {{if .Requested}}checked{{end}}" href="{{.User.HomeLink}}" data-value="{{.ItemID}}" data-can-change="{{.CanChange}}"
{{if not .CanChange}}data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
- {{svg "octicon-check"}} {{ctx.AvatarUtils.Avatar .User 20}} {{template "repo/search_name" .User}}
+ <span class="item-check-mark">{{svg "octicon-check"}}</span>
+ {{ctx.AvatarUtils.Avatar .User 20}} {{template "repo/search_name" .User}}
</a>
{{end}}
{{end}}
@@ -29,7 +28,8 @@
{{if .Team}}
<a class="item muted {{if .Requested}}checked{{end}}" href="#" data-value="{{.ItemID}}" data-can-change="{{.CanChange}}"
{{if not .CanChange}} data-tooltip-content="{{ctx.Locale.Tr "repo.issues.remove_request_review_block"}}"{{end}}>
- {{svg "octicon-check"}} {{svg "octicon-people" 20}} {{$data.RepoOwnerName}}/{{.Team.Name}}
+ <span class="item-check-mark">{{svg "octicon-check"}}</span>
+ {{svg "octicon-people" 20}} {{$data.RepoOwnerName}}/{{.Team.Name}}
</a>
{{end}}
{{end}}
diff --git a/templates/repo/issue/view_content/sidebar.tmpl b/templates/repo/issue/view_content/sidebar.tmpl
index 7a40274759..0fae1e9e1c 100644
--- a/templates/repo/issue/view_content/sidebar.tmpl
+++ b/templates/repo/issue/view_content/sidebar.tmpl
@@ -2,13 +2,12 @@
{{template "repo/issue/branch_selector_field" $}}
{{if .Issue.IsPull}}
- {{template "repo/issue/sidebar/reviewer_list" dict "IssueSidebarReviewersData" $.IssueSidebarReviewersData}}
+ {{template "repo/issue/sidebar/reviewer_list" $.IssueSidebarReviewersData}}
{{template "repo/issue/sidebar/wip_switch" $}}
<div class="divider"></div>
{{end}}
- {{template "repo/issue/labels/labels_selector_field" $}}
- {{template "repo/issue/labels/labels_sidebar" dict "root" $}}
+ {{template "repo/issue/sidebar/label_list" $.IssueSidebarLabelsData}}
{{template "repo/issue/sidebar/milestone_list" $}}
{{template "repo/issue/sidebar/project_list" $}}
diff --git a/web_src/css/repo.css b/web_src/css/repo.css
index 185a5f6f55..ff8342d29a 100644
--- a/web_src/css/repo.css
+++ b/web_src/css/repo.css
@@ -50,7 +50,7 @@
width: 300px;
}
-.issue-sidebar-combo .ui.dropdown .item:not(.checked) svg.octicon-check {
+.issue-sidebar-combo .ui.dropdown .item:not(.checked) .item-check-mark {
visibility: hidden;
}
/* ideally, we should move these styles to ".ui.dropdown .menu.flex-items-menu > .item ...", could be done later */
@@ -62,6 +62,8 @@
.issue-content-right .dropdown > .menu {
max-width: 270px;
min-width: 0;
+ max-height: 500px;
+ overflow-x: auto;
}
@media (max-width: 767.98px) {
@@ -110,10 +112,6 @@
left: 0;
}
-.repository .select-label .desc {
- padding-left: 23px;
-}
-
/* For the secondary pointing menu, respect its own border-bottom */
/* style reference: https://semantic-ui.com/collections/menu.html#pointing */
.repository .ui.tabs.container .ui.menu:not(.secondary.pointing) {
diff --git a/web_src/css/repo/issue-label.css b/web_src/css/repo/issue-label.css
index 9b4b144a00..0a25d31da9 100644
--- a/web_src/css/repo/issue-label.css
+++ b/web_src/css/repo/issue-label.css
@@ -47,6 +47,7 @@
}
.archived-label-hint {
- float: right;
- margin: -12px;
+ position: absolute;
+ top: 10px;
+ right: 5px;
}
diff --git a/web_src/js/features/common-page.ts b/web_src/js/features/common-page.ts
index beec92d152..56c5915b6d 100644
--- a/web_src/js/features/common-page.ts
+++ b/web_src/js/features/common-page.ts
@@ -32,13 +32,13 @@ export function initGlobalDropdown() {
const $uiDropdowns = fomanticQuery('.ui.dropdown');
// do not init "custom" dropdowns, "custom" dropdowns are managed by their own code.
- $uiDropdowns.filter(':not(.custom)').dropdown();
+ $uiDropdowns.filter(':not(.custom)').dropdown({hideDividers: 'empty'});
// The "jump" means this dropdown is mainly used for "menu" purpose,
// clicking an item will jump to somewhere else or trigger an action/function.
// When a dropdown is used for non-refresh actions with tippy,
// it must have this "jump" class to hide the tippy when dropdown is closed.
- $uiDropdowns.filter('.jump').dropdown({
+ $uiDropdowns.filter('.jump').dropdown('setting', {
action: 'hide',
onShow() {
// hide associated tooltip while dropdown is open
diff --git a/web_src/js/features/repo-issue-sidebar-combolist.ts b/web_src/js/features/repo-issue-sidebar-combolist.ts
index d541615988..f408eb43ba 100644
--- a/web_src/js/features/repo-issue-sidebar-combolist.ts
+++ b/web_src/js/features/repo-issue-sidebar-combolist.ts
@@ -1,6 +1,6 @@
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {POST} from '../modules/fetch.ts';
-import {queryElemChildren, toggleElem} from '../utils/dom.ts';
+import {queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts';
// if there are draft comments, confirm before reloading, to avoid losing comments
export function issueSidebarReloadConfirmDraftComment() {
@@ -27,20 +27,37 @@ function collectCheckedValues(elDropdown: HTMLElement) {
}
export function initIssueSidebarComboList(container: HTMLElement) {
- if (!container) return;
-
const updateUrl = container.getAttribute('data-update-url');
const elDropdown = container.querySelector<HTMLElement>(':scope > .ui.dropdown');
const elList = container.querySelector<HTMLElement>(':scope > .ui.list');
const elComboValue = container.querySelector<HTMLInputElement>(':scope > .combo-value');
- const initialValues = collectCheckedValues(elDropdown);
+ let initialValues = collectCheckedValues(elDropdown);
elDropdown.addEventListener('click', (e) => {
const elItem = (e.target as HTMLElement).closest('.item');
if (!elItem) return;
e.preventDefault();
- if (elItem.getAttribute('data-can-change') !== 'true') return;
- elItem.classList.toggle('checked');
+ if (elItem.hasAttribute('data-can-change') && elItem.getAttribute('data-can-change') !== 'true') return;
+
+ if (elItem.matches('.clear-selection')) {
+ queryElems(elDropdown, '.menu > .item', (el) => el.classList.remove('checked'));
+ elComboValue.value = '';
+ return;
+ }
+
+ const scope = elItem.getAttribute('data-scope');
+ if (scope) {
+ // scoped items could only be checked one at a time
+ const elSelected = elDropdown.querySelector<HTMLElement>(`.menu > .item.checked[data-scope="${CSS.escape(scope)}"]`);
+ if (elSelected === elItem) {
+ elItem.classList.toggle('checked');
+ } else {
+ queryElems(elDropdown, `.menu > .item[data-scope="${CSS.escape(scope)}"]`, (el) => el.classList.remove('checked'));
+ elItem.classList.toggle('checked', true);
+ }
+ } else {
+ elItem.classList.toggle('checked');
+ }
elComboValue.value = collectCheckedValues(elDropdown).join(',');
});
@@ -61,29 +78,28 @@ export function initIssueSidebarComboList(container: HTMLElement) {
if (changed) issueSidebarReloadConfirmDraftComment();
};
- const syncList = (changedValues) => {
+ const syncUiList = (changedValues) => {
const elEmptyTip = elList.querySelector('.item.empty-list');
queryElemChildren(elList, '.item:not(.empty-list)', (el) => el.remove());
for (const value of changedValues) {
- const el = elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${value}"]`);
+ const el = elDropdown.querySelector<HTMLElement>(`.menu > .item[data-value="${CSS.escape(value)}"]`);
const listItem = el.cloneNode(true) as HTMLElement;
- listItem.querySelector('svg.octicon-check')?.remove();
+ queryElems(listItem, '.item-check-mark, .item-secondary-info', (el) => el.remove());
elList.append(listItem);
}
const hasItems = Boolean(elList.querySelector('.item:not(.empty-list)'));
toggleElem(elEmptyTip, !hasItems);
};
- fomanticQuery(elDropdown).dropdown({
+ fomanticQuery(elDropdown).dropdown('setting', {
action: 'nothing', // do not hide the menu if user presses Enter
fullTextSearch: 'exact',
async onHide() {
+ // TODO: support "Esc" to cancel the selection. Use partial page loading to avoid losing inputs.
const changedValues = collectCheckedValues(elDropdown);
- if (updateUrl) {
- await updateToBackend(changedValues); // send requests to backend and reload the page
- } else {
- syncList(changedValues); // only update the list in the sidebar
- }
+ syncUiList(changedValues);
+ if (updateUrl) await updateToBackend(changedValues);
+ initialValues = changedValues;
},
});
}
diff --git a/web_src/js/features/repo-issue-sidebar.md b/web_src/js/features/repo-issue-sidebar.md
new file mode 100644
index 0000000000..3022b52d05
--- /dev/null
+++ b/web_src/js/features/repo-issue-sidebar.md
@@ -0,0 +1,27 @@
+A sidebar combo (dropdown+list) is like this:
+
+```html
+<div class="issue-sidebar-combo" data-update-url="...">
+ <input class="combo-value" name="..." type="hidden" value="...">
+ <div class="ui dropdown">
+ <div class="menu">
+ <div class="item clear-selection">clear</div>
+ <div class="item" data-value="..." data-scope="...">
+ <span class="item-check-mark">...</span>
+ ...
+ </div>
+ </div>
+ </div>
+ <div class="ui list">
+ <span class="item empty-list">no item</span>
+ <span class="item">...</span>
+ </div>
+</div>
+```
+
+When the selected items change, the `combo-value` input will be updated.
+If there is `data-update-url`, it also calls backend to attach/detach the changed items.
+
+Also, the changed items will be syncronized to the `ui list` items.
+
+The items with the same data-scope only allow one selected at a time.
diff --git a/web_src/js/features/repo-issue-sidebar.ts b/web_src/js/features/repo-issue-sidebar.ts
index 4a1ef02aab..52878848e8 100644
--- a/web_src/js/features/repo-issue-sidebar.ts
+++ b/web_src/js/features/repo-issue-sidebar.ts
@@ -3,7 +3,7 @@ import {POST} from '../modules/fetch.ts';
import {updateIssuesMeta} from './repo-common.ts';
import {svg} from '../svg.ts';
import {htmlEscape} from 'escape-goat';
-import {toggleElem} from '../utils/dom.ts';
+import {queryElems, toggleElem} from '../utils/dom.ts';
import {initIssueSidebarComboList, issueSidebarReloadConfirmDraftComment} from './repo-issue-sidebar-combolist.ts';
function initBranchSelector() {
@@ -28,7 +28,7 @@ function initBranchSelector() {
} else {
// for new issue, only update UI&form, do not send request/reload
const selectedHiddenSelector = this.getAttribute('data-id-selector');
- document.querySelector(selectedHiddenSelector).value = selectedValue;
+ document.querySelector<HTMLInputElement>(selectedHiddenSelector).value = selectedValue;
elSelectBranch.querySelector('.text-branch-name').textContent = selectedText;
}
});
@@ -53,7 +53,7 @@ function initListSubmits(selector, outerSelector) {
for (const [elementId, item] of itemEntries) {
await updateIssuesMeta(
item['update-url'],
- item.action,
+ item['action'],
item['issue-id'],
elementId,
);
@@ -80,14 +80,14 @@ function initListSubmits(selector, outerSelector) {
if (scope) {
// Enable only clicked item for scoped labels
if (this.getAttribute('data-scope') !== scope) {
- return true;
+ return;
}
if (this !== clickedItem && !this.classList.contains('checked')) {
- return true;
+ return;
}
} else if (this !== clickedItem) {
// Toggle for other labels
- return true;
+ return;
}
if (this.classList.contains('checked')) {
@@ -258,13 +258,13 @@ export function initRepoIssueSidebar() {
initRepoIssueDue();
// TODO: refactor the legacy initListSubmits&selectItem to initIssueSidebarComboList
- initListSubmits('select-label', 'labels');
initListSubmits('select-assignees', 'assignees');
initListSubmits('select-assignees-modify', 'assignees');
+ selectItem('.select-assignee', '#assignee_id');
+
selectItem('.select-project', '#project_id');
selectItem('.select-milestone', '#milestone_id');
- selectItem('.select-assignee', '#assignee_id');
- // init the combo list: a dropdown for selecting reviewers, and a list for showing selected reviewers and related actions
- initIssueSidebarComboList(document.querySelector('.issue-sidebar-combo[data-sidebar-combo-for="reviewers"]'));
+ // init the combo list: a dropdown for selecting items, and a list for showing selected items and related actions
+ queryElems<HTMLElement>(document, '.issue-sidebar-combo', (el) => initIssueSidebarComboList(el));
}
diff --git a/web_src/js/features/repo-issue.ts b/web_src/js/features/repo-issue.ts
index 92916ec8d7..7457531ece 100644
--- a/web_src/js/features/repo-issue.ts
+++ b/web_src/js/features/repo-issue.ts
@@ -98,6 +98,7 @@ export function initRepoIssueSidebarList() {
});
});
+ // FIXME: it is wrong place to init ".ui.dropdown.label-filter"
$('.menu .ui.dropdown.label-filter').on('keydown', (e) => {
if (e.altKey && e.key === 'Enter') {
const selectedItem = document.querySelector('.menu .ui.dropdown.label-filter .menu .item.selected');
@@ -106,7 +107,6 @@ export function initRepoIssueSidebarList() {
}
}
});
- $('.ui.dropdown.label-filter, .ui.dropdown.select-label').dropdown('setting', {'hideDividers': 'empty'}).dropdown('refreshItems');
}
export function initRepoIssueCommentDelete() {
@@ -652,19 +652,6 @@ function initIssueTemplateCommentEditors($commentForm) {
}
}
-// This function used to show and hide archived label on issue/pr
-// page in the sidebar where we select the labels
-// If we have any archived label tagged to issue and pr. We will show that
-// archived label with checked classed otherwise we will hide it
-// with the help of this function.
-// This function runs globally.
-export function initArchivedLabelHandler() {
- if (!document.querySelector('.archived-label-hint')) return;
- for (const label of document.querySelectorAll('[data-is-archived]')) {
- toggleElem(label, label.classList.contains('checked'));
- }
-}
-
export function initRepoCommentFormAndSidebar() {
const $commentForm = $('.comment.form');
if (!$commentForm.length) return;
diff --git a/web_src/js/index.ts b/web_src/js/index.ts
index 487aac97aa..eeead37333 100644
--- a/web_src/js/index.ts
+++ b/web_src/js/index.ts
@@ -30,7 +30,7 @@ import {
initRepoIssueWipTitle,
initRepoPullRequestMergeInstruction,
initRepoPullRequestAllowMaintainerEdit,
- initRepoPullRequestReview, initRepoIssueSidebarList, initArchivedLabelHandler,
+ initRepoPullRequestReview, initRepoIssueSidebarList,
} from './features/repo-issue.ts';
import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts';
import {initRepoTopicBar} from './features/repo-home.ts';
@@ -182,7 +182,6 @@ onDomReady(() => {
initRepoIssueContentHistory,
initRepoIssueList,
initRepoIssueSidebarList,
- initArchivedLabelHandler,
initRepoIssueReferenceRepositorySearch,
initRepoIssueTimeTracking,
initRepoIssueWipTitle,
diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts
index 79ce05a7ad..29b34dd1e3 100644
--- a/web_src/js/utils/dom.ts
+++ b/web_src/js/utils/dom.ts
@@ -3,7 +3,7 @@ import type {Promisable} from 'type-fest';
import type $ from 'jquery';
type ElementArg = Element | string | NodeListOf<Element> | Array<Element> | ReturnType<typeof $>;
-type ElementsCallback = (el: Element) => Promisable<any>;
+type ElementsCallback<T extends Element> = (el: T) => Promisable<any>;
type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>;
type ArrayLikeIterable<T> = ArrayLike<T> & Iterable<T>; // for NodeListOf and Array
@@ -58,7 +58,7 @@ export function isElemHidden(el: ElementArg) {
return res[0];
}
-function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?: ElementsCallback): ArrayLikeIterable<T> {
+function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
if (fn) {
for (const el of elems) {
fn(el);
@@ -67,7 +67,7 @@ function applyElemsCallback<T extends Element>(elems: ArrayLikeIterable<T>, fn?:
return elems;
}
-export function queryElemSiblings<T extends Element>(el: Element, selector = '*', fn?: ElementsCallback): ArrayLikeIterable<T> {
+export function queryElemSiblings<T extends Element>(el: Element, selector = '*', fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
const elems = Array.from(el.parentNode.children) as T[];
return applyElemsCallback<T>(elems.filter((child: Element) => {
return child !== el && child.matches(selector);
@@ -75,13 +75,13 @@ export function queryElemSiblings<T extends Element>(el: Element, selector = '*'
}
// it works like jQuery.children: only the direct children are selected
-export function queryElemChildren<T extends Element>(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback): ArrayLikeIterable<T> {
+export function queryElemChildren<T extends Element>(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
return applyElemsCallback<T>(parent.querySelectorAll(`:scope > ${selector}`), fn);
}
// it works like parent.querySelectorAll: all descendants are selected
// in the future, all "queryElems(document, ...)" should be refactored to use a more specific parent
-export function queryElems<T extends Element>(parent: Element | ParentNode, selector: string, fn?: ElementsCallback): ArrayLikeIterable<T> {
+export function queryElems<T extends Element>(parent: Element | ParentNode, selector: string, fn?: ElementsCallback<T>): ArrayLikeIterable<T> {
return applyElemsCallback<T>(parent.querySelectorAll(selector), fn);
}