aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--package-lock.json6
-rw-r--r--package.json1
-rw-r--r--routers/web/devtest/devtest.go35
-rw-r--r--routers/web/misc/markup.go18
-rw-r--r--routers/web/org/setting.go1
-rw-r--r--routers/web/repo/commit.go1
-rw-r--r--routers/web/repo/compare.go1
-rw-r--r--routers/web/repo/editor.go2
-rw-r--r--routers/web/repo/issue.go2
-rw-r--r--routers/web/repo/issue_label.go1
-rw-r--r--routers/web/repo/pull.go2
-rw-r--r--routers/web/repo/release.go4
-rw-r--r--routers/web/web.go7
-rw-r--r--templates/base/head_script.tmpl24
-rw-r--r--templates/devtest/gitea-ui.tmpl12
-rw-r--r--templates/devtest/list.tmpl5
-rw-r--r--templates/repo/diff/box.tmpl21
-rw-r--r--templates/repo/diff/comment_form.tmpl22
-rw-r--r--templates/repo/diff/new_review.tmpl9
-rw-r--r--templates/repo/issue/comment_tab.tmpl28
-rw-r--r--templates/repo/issue/fields/textarea.tmpl2
-rw-r--r--templates/repo/issue/labels/edit_delete_label.tmpl2
-rw-r--r--templates/repo/issue/labels/label_new.tmpl2
-rw-r--r--templates/repo/issue/view_content.tmpl25
-rw-r--r--templates/repo/release/new.tmpl21
-rw-r--r--templates/repo/wiki/new.tmpl21
-rw-r--r--templates/shared/combomarkdowneditor.tmpl47
-rw-r--r--web_src/css/editor-markdown.css25
-rw-r--r--web_src/css/editor.css1
-rw-r--r--web_src/css/index.css1
-rw-r--r--web_src/css/repository.css4
-rw-r--r--web_src/css/review.css5
-rw-r--r--web_src/js/features/comp/ComboMarkdownEditor.js277
-rw-r--r--web_src/js/features/comp/EasyMDE.js181
-rw-r--r--web_src/js/features/comp/ImagePaste.js47
-rw-r--r--web_src/js/features/comp/MarkupContentPreview.js25
-rw-r--r--web_src/js/features/contextpopup.js13
-rw-r--r--web_src/js/features/repo-diff.js2
-rw-r--r--web_src/js/features/repo-issue.js74
-rw-r--r--web_src/js/features/repo-legacy.js331
-rw-r--r--web_src/js/features/repo-release.js17
-rw-r--r--web_src/js/features/repo-wiki.js220
-rw-r--r--web_src/js/features/tribute.js33
-rw-r--r--web_src/js/index.js3
-rw-r--r--web_src/js/utils.js5
-rw-r--r--web_src/js/utils.test.js6
46 files changed, 761 insertions, 831 deletions
diff --git a/package-lock.json b/package-lock.json
index 9d0c83f656..b9d998a69d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"@citation-js/plugin-csl": "0.6.7",
"@citation-js/plugin-software-formats": "0.6.1",
"@claviska/jquery-minicolors": "2.3.6",
+ "@github/markdown-toolbar-element": "2.1.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "18.3.0",
"@vue/compiler-sfc": "3.2.47",
@@ -838,6 +839,11 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@github/markdown-toolbar-element": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@github/markdown-toolbar-element/-/markdown-toolbar-element-2.1.1.tgz",
+ "integrity": "sha512-J++rpd5H9baztabJQB82h26jtueOeBRSTqetk9Cri+Lj/s28ndu6Tovn0uHQaOKtBWDobFunk9b5pP5vcqt7cA=="
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.8",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz",
diff --git a/package.json b/package.json
index 8ac5c312f6..3ccf0c0840 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"@citation-js/plugin-csl": "0.6.7",
"@citation-js/plugin-software-formats": "0.6.1",
"@claviska/jquery-minicolors": "2.3.6",
+ "@github/markdown-toolbar-element": "2.1.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@primer/octicons": "18.3.0",
"@vue/compiler-sfc": "3.2.47",
diff --git a/routers/web/devtest/devtest.go b/routers/web/devtest/devtest.go
new file mode 100644
index 0000000000..eb77d0b927
--- /dev/null
+++ b/routers/web/devtest/devtest.go
@@ -0,0 +1,35 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package devtest
+
+import (
+ "net/http"
+ "path"
+ "strings"
+
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/templates"
+)
+
+// List all devtest templates, they will be used for e2e tests for the UI components
+func List(ctx *context.Context) {
+ templateNames := templates.GetTemplateAssetNames()
+ var subNames []string
+ const prefix = "templates/devtest/"
+ for _, tmplName := range templateNames {
+ if strings.HasPrefix(tmplName, prefix) {
+ subName := strings.TrimSuffix(strings.TrimPrefix(tmplName, prefix), ".tmpl")
+ if subName != "list" {
+ subNames = append(subNames, subName)
+ }
+ }
+ }
+ ctx.Data["SubNames"] = subNames
+ ctx.HTML(http.StatusOK, "devtest/list")
+}
+
+func Tmpl(ctx *context.Context) {
+ ctx.HTML(http.StatusOK, base.TplName("devtest"+path.Clean("/"+ctx.Params("sub"))))
+}
diff --git a/routers/web/misc/markup.go b/routers/web/misc/markup.go
index f678316f44..1690378945 100644
--- a/routers/web/misc/markup.go
+++ b/routers/web/misc/markup.go
@@ -15,24 +15,6 @@ import (
// Markup render markup document to HTML
func Markup(ctx *context.Context) {
- // swagger:operation POST /markup miscellaneous renderMarkup
- // ---
- // summary: Render a markup document as HTML
- // parameters:
- // - name: body
- // in: body
- // schema:
- // "$ref": "#/definitions/MarkupOption"
- // consumes:
- // - application/json
- // produces:
- // - text/html
- // responses:
- // "200":
- // "$ref": "#/responses/MarkupRender"
- // "422":
- // "$ref": "#/responses/validationError"
-
form := web.GetForm(ctx).(*api.MarkupOption)
if ctx.HasAPIError() {
diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go
index 654e9000fa..7d84c101d8 100644
--- a/routers/web/org/setting.go
+++ b/routers/web/org/setting.go
@@ -246,7 +246,6 @@ func Labels(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.labels")
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsOrgSettingsLabels"] = true
- ctx.Data["RequireTribute"] = true
ctx.Data["LabelTemplates"] = repo_module.LabelTemplates
ctx.HTML(http.StatusOK, tplSettingsLabels)
}
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index 843b1d8dfd..7439c2411b 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -253,7 +253,6 @@ func FileHistory(ctx *context.Context) {
// Diff show different from current commit to previous commit
func Diff(ctx *context.Context) {
ctx.Data["PageIsDiff"] = true
- ctx.Data["RequireTribute"] = true
userName := ctx.Repo.Owner.Name
repoName := ctx.Repo.Repository.Name
diff --git a/routers/web/repo/compare.go b/routers/web/repo/compare.go
index d7e7bac7b7..c49eb762d8 100644
--- a/routers/web/repo/compare.go
+++ b/routers/web/repo/compare.go
@@ -781,7 +781,6 @@ func CompareDiff(ctx *context.Context) {
ctx.Data["IsRepoToolbarCommits"] = true
ctx.Data["IsDiffCompare"] = true
- ctx.Data["RequireTribute"] = true
templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates)
if len(templateErrs) > 0 {
diff --git a/routers/web/repo/editor.go b/routers/web/repo/editor.go
index 2b66be22ae..f65e1ad3d8 100644
--- a/routers/web/repo/editor.go
+++ b/routers/web/repo/editor.go
@@ -538,7 +538,6 @@ func DeleteFilePost(ctx *context.Context) {
// UploadFile render upload file page
func UploadFile(ctx *context.Context) {
ctx.Data["PageIsUpload"] = true
- ctx.Data["RequireTribute"] = true
upload.AddUploadContext(ctx, "repo")
canCommit := renderCommitRights(ctx)
treePath := cleanUploadFileName(ctx.Repo.TreePath)
@@ -573,7 +572,6 @@ func UploadFile(ctx *context.Context) {
func UploadFilePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.UploadRepoFileForm)
ctx.Data["PageIsUpload"] = true
- ctx.Data["RequireTribute"] = true
upload.AddUploadContext(ctx, "repo")
canCommit := renderCommitRights(ctx)
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 612222598f..e4f1172dd9 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -849,7 +849,6 @@ func NewIssue(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.issues.new")
ctx.Data["PageIsIssueList"] = true
ctx.Data["NewIssueChooseTemplate"] = ctx.HasIssueTemplatesOrContactLinks()
- ctx.Data["RequireTribute"] = true
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
title := ctx.FormString("title")
ctx.Data["TitleQuery"] = title
@@ -1295,7 +1294,6 @@ func ViewIssue(ctx *context.Context) {
ctx.Data["IssueType"] = "all"
}
- ctx.Data["RequireTribute"] = true
ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(unit.TypeProjects)
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")
diff --git a/routers/web/repo/issue_label.go b/routers/web/repo/issue_label.go
index 31bf85fedb..3123359a65 100644
--- a/routers/web/repo/issue_label.go
+++ b/routers/web/repo/issue_label.go
@@ -28,7 +28,6 @@ func Labels(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.labels")
ctx.Data["PageIsIssueList"] = true
ctx.Data["PageIsLabels"] = true
- ctx.Data["RequireTribute"] = true
ctx.Data["LabelTemplates"] = repo_module.LabelTemplates
ctx.HTML(http.StatusOK, tplLabels)
}
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index 4f99687738..c37d52640f 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -791,7 +791,6 @@ func ViewPullFiles(ctx *context.Context) {
setCompareContext(ctx, baseCommit, commit, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
- ctx.Data["RequireTribute"] = true
if ctx.Data["Assignees"], err = repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository); err != nil {
ctx.ServerError("GetAssignees", err)
return
@@ -1160,7 +1159,6 @@ func CompareAndPullRequestPost(ctx *context.Context) {
ctx.Data["PageIsComparePull"] = true
ctx.Data["IsDiffCompare"] = true
ctx.Data["IsRepoToolbarCommits"] = true
- ctx.Data["RequireTribute"] = true
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")
diff --git a/routers/web/repo/release.go b/routers/web/repo/release.go
index 3ffadd34ac..b8c5f67f45 100644
--- a/routers/web/repo/release.go
+++ b/routers/web/repo/release.go
@@ -308,7 +308,6 @@ func LatestRelease(ctx *context.Context) {
func NewRelease(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.release.new_release")
ctx.Data["PageIsReleaseList"] = true
- ctx.Data["RequireTribute"] = true
ctx.Data["tag_target"] = ctx.Repo.Repository.DefaultBranch
if tagName := ctx.FormString("tag"); len(tagName) > 0 {
rel, err := repo_model.GetRelease(ctx.Repo.Repository.ID, tagName)
@@ -351,7 +350,6 @@ func NewReleasePost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.NewReleaseForm)
ctx.Data["Title"] = ctx.Tr("repo.release.new_release")
ctx.Data["PageIsReleaseList"] = true
- ctx.Data["RequireTribute"] = true
if ctx.HasError() {
ctx.HTML(http.StatusOK, tplReleaseNew)
@@ -469,7 +467,6 @@ func EditRelease(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.release.edit_release")
ctx.Data["PageIsReleaseList"] = true
ctx.Data["PageIsEditRelease"] = true
- ctx.Data["RequireTribute"] = true
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "release")
@@ -514,7 +511,6 @@ func EditReleasePost(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.release.edit_release")
ctx.Data["PageIsReleaseList"] = true
ctx.Data["PageIsEditRelease"] = true
- ctx.Data["RequireTribute"] = true
tagName := ctx.Params("*")
rel, err := repo_model.GetRelease(ctx.Repo.Repository.ID, tagName)
diff --git a/routers/web/web.go b/routers/web/web.go
index 4bd2f76c57..6b62ff6f83 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -27,6 +27,7 @@ import (
"code.gitea.io/gitea/modules/web/routing"
"code.gitea.io/gitea/routers/web/admin"
"code.gitea.io/gitea/routers/web/auth"
+ "code.gitea.io/gitea/routers/web/devtest"
"code.gitea.io/gitea/routers/web/events"
"code.gitea.io/gitea/routers/web/explore"
"code.gitea.io/gitea/routers/web/feed"
@@ -1491,6 +1492,12 @@ func RegisterRoutes(m *web.Route) {
if setting.API.EnableSwagger {
m.Get("/swagger.v1.json", SwaggerV1Json)
}
+
+ if !setting.IsProd {
+ m.Any("/devtest", devtest.List)
+ m.Any("/devtest/{sub}", devtest.Tmpl)
+ }
+
m.NotFound(func(w http.ResponseWriter, req *http.Request) {
ctx := context.GetContext(req)
ctx.NotFound("", nil)
diff --git a/templates/base/head_script.tmpl b/templates/base/head_script.tmpl
index 62fb10d89f..670d146b56 100644
--- a/templates/base/head_script.tmpl
+++ b/templates/base/head_script.tmpl
@@ -15,23 +15,19 @@ If you introduce mistakes in it, Gitea JavaScript code wouldn't run correctly.
useServiceWorker: {{UseServiceWorker}},
csrfToken: '{{.CsrfToken}}',
pageData: {{.PageData}},
- requireTribute: {{.RequireTribute}},
notificationSettings: {{NotificationSettings}}, {{/*a map provided by NewFuncMap in helper.go*/}}
enableTimeTracking: {{EnableTimetracking}},
- {{if .RequireTribute}}
+ {{if or .Participants .Assignees .MentionableTeams}}
tributeValues: Array.from(new Map([
- {{range .Participants}}
- ['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}',
- name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.AvatarLink $.Context}}'}],
- {{end}}
- {{range .Assignees}}
- ['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}',
- name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.AvatarLink $.Context}}'}],
- {{end}}
- {{range .MentionableTeams}}
- ['{{$.MentionableTeamsOrg}}/{{.Name}}', {key: '{{$.MentionableTeamsOrg}}/{{.Name}}', value: '{{$.MentionableTeamsOrg}}/{{.Name}}',
- name: '{{$.MentionableTeamsOrg}}/{{.Name}}', avatar: '{{$.MentionableTeamsOrgAvatar}}'}],
- {{end}}
+ {{- range .Participants -}}
+ ['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}', name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.AvatarLink $.Context}}'}],
+ {{- end -}}
+ {{- range .Assignees -}}
+ ['{{.Name}}', {key: '{{.Name}} {{.FullName}}', value: '{{.Name}}', name: '{{.Name}}', fullname: '{{.FullName}}', avatar: '{{.AvatarLink $.Context}}'}],
+ {{- end -}}
+ {{- range .MentionableTeams -}}
+ ['{{$.MentionableTeamsOrg}}/{{.Name}}', {key: '{{$.MentionableTeamsOrg}}/{{.Name}}', value: '{{$.MentionableTeamsOrg}}/{{.Name}}', name: '{{$.MentionableTeamsOrg}}/{{.Name}}', avatar: '{{$.MentionableTeamsOrgAvatar}}'}],
+ {{- end -}}
]).values()),
{{end}}
mermaidMaxSourceCharacters: {{MermaidMaxSourceCharacters}},
diff --git a/templates/devtest/gitea-ui.tmpl b/templates/devtest/gitea-ui.tmpl
new file mode 100644
index 0000000000..c5ab863d00
--- /dev/null
+++ b/templates/devtest/gitea-ui.tmpl
@@ -0,0 +1,12 @@
+{{template "base/head" .}}
+<div class="page-content devtest">
+ <div>
+ <gitea-origin-url data-url="test/url"></gitea-origin-url>
+ <gitea-origin-url data-url="/test/url"></gitea-origin-url>
+ </div>
+ <div>
+ <span data-tooltip-content="test tooltip">text with tooltip</span>
+ </div>
+ {{template "shared/combomarkdowneditor" .}}
+</div>
+{{template "base/footer" .}}
diff --git a/templates/devtest/list.tmpl b/templates/devtest/list.tmpl
new file mode 100644
index 0000000000..3a519c328e
--- /dev/null
+++ b/templates/devtest/list.tmpl
@@ -0,0 +1,5 @@
+<ul>
+ {{range .SubNames}}
+ <li><a href="{{AppSubUrl}}/devtest/{{.}}">{{.}}</a></li>
+ {{end}}
+</ul>
diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl
index 36e669276e..21ea63cc0a 100644
--- a/templates/repo/diff/box.tmpl
+++ b/templates/repo/diff/box.tmpl
@@ -198,24 +198,21 @@
</div>
{{if not $.Repository.IsArchived}}
- <div class="gt-hidden" id="edit-content-form">
+ <template id="issue-comment-editor-template">
<div class="ui comment form">
- <div class="ui top attached tabular menu">
- <a class="active write item">{{$.locale.Tr "write"}}</a>
- <a class="preview item" data-url="{{$.Repository.Link}}/markup" data-context="{{$.RepoLink}}">{{$.locale.Tr "preview"}}</a>
- </div>
- <div class="ui bottom attached active write tab segment">
- <textarea class="review-textarea js-quick-submit" tabindex="1" name="content"></textarea>
- </div>
- <div class="ui bottom attached tab preview segment markup">
- {{$.locale.Tr "loading"}}
- </div>
+ {{template "shared/combomarkdowneditor" (dict
+ "locale" $.locale
+ "MarkdownPreviewUrl" (print $.Repository.Link "/markup")
+ "MarkdownPreviewContext" $.RepoLink
+ "TextareaName" "content"
+ "DropzoneParentContainer" ".ui.form"
+ )}}
<div class="text right edit buttons">
<button class="ui basic primary cancel button" tabindex="3">{{.locale.Tr "repo.issues.cancel"}}</button>
<button class="ui green save button" tabindex="2">{{.locale.Tr "repo.issues.save"}}</button>
</div>
</div>
- </div>
+ </template>
{{end}}
{{template "repo/issue/view_content/reference_issue_dialog" .}}
diff --git a/templates/repo/diff/comment_form.tmpl b/templates/repo/diff/comment_form.tmpl
index 394a392bb9..109f167967 100644
--- a/templates/repo/diff/comment_form.tmpl
+++ b/templates/repo/diff/comment_form.tmpl
@@ -9,18 +9,16 @@
<input type="hidden" name="diff_start_cid">
<input type="hidden" name="diff_end_cid">
<input type="hidden" name="diff_base_cid">
- <div class="ui top tabular menu" data-write="write" data-preview="preview">
- <a class="active item" data-tab="write">{{$.root.locale.Tr "write"}}</a>
- <a class="item" data-tab="preview" data-url="{{$.root.Repository.Link}}/markup" data-context="{{$.root.RepoLink}}">{{$.root.locale.Tr "preview"}}</a>
- </div>
- <div class="field">
- <div class="ui active tab" data-tab="write">
- <textarea name="content" placeholder="{{$.root.locale.Tr "repo.diff.comment.placeholder"}}"></textarea>
- </div>
- <div class="ui tab markup" data-tab="preview">
- {{.locale.Tr "loading"}}
- </div>
- </div>
+
+ {{template "shared/combomarkdowneditor" (dict
+ "locale" $.root.locale
+ "MarkdownPreviewUrl" (print $.root.Repository.Link "/markup")
+ "MarkdownPreviewContext" $.root.RepoLink
+ "TextareaName" "content"
+ "TextareaPlaceholder" ($.locale.Tr "repo.diff.comment.placeholder")
+ "DropzoneParentContainer" "form"
+ )}}
+
<div class="field footer gt-mx-3">
<span class="markup-info">{{svg "octicon-markup"}} {{$.root.locale.Tr "repo.diff.comment.markdown_info"}}</span>
<div class="ui right">
diff --git a/templates/repo/diff/new_review.tmpl b/templates/repo/diff/new_review.tmpl
index 9d2208e289..bb97303034 100644
--- a/templates/repo/diff/new_review.tmpl
+++ b/templates/repo/diff/new_review.tmpl
@@ -7,14 +7,19 @@
<div class="review-box-panel tippy-target">
<div class="ui segment">
<form class="ui form" action="{{.Link}}/reviews/submit" method="post">
- {{.CsrfTokenHtml}}
+ {{.CsrfTokenHtml}}
<input type="hidden" name="commit_id" value="{{.AfterCommitID}}">
<div class="header gt-df gt-ac gt-pb-3">
<div class="gt-f1">{{$.locale.Tr "repo.diff.review.header"}}</div>
<a class="muted close gt-px-3">{{svg "octicon-x" 16}}</a>
</div>
<div class="ui field">
- <textarea name="content" tabindex="0" rows="2" placeholder="{{$.locale.Tr "repo.diff.review.placeholder"}}"></textarea>
+ {{template "shared/combomarkdowneditor" (dict
+ "locale" $.locale
+ "TextareaName" "content"
+ "TextareaPlaceholder" ($.locale.Tr "repo.diff.review.placeholder")
+ "DropzoneParentContainer" "form"
+ )}}
</div>
{{if .IsAttachmentEnabled}}
<div class="field">
diff --git a/templates/repo/issue/comment_tab.tmpl b/templates/repo/issue/comment_tab.tmpl
index 47d6ca9587..2212d99a10 100644
--- a/templates/repo/issue/comment_tab.tmpl
+++ b/templates/repo/issue/comment_tab.tmpl
@@ -1,17 +1,17 @@
- <div class="ui top tabular menu" data-write="write" data-preview="preview">
- <a class="active item" data-tab="write">{{.locale.Tr "write"}}</a>
- <a class="item" data-tab="preview" data-url="{{.Repository.Link}}/markup" data-context="{{.RepoLink}}">{{.locale.Tr "preview"}}</a>
- </div>
- <div class="field">
- <div class="ui bottom active tab" data-tab="write">
- <textarea id="content" class="edit_area js-quick-submit" name="content" tabindex="4" data-id="issue-{{.RepoName}}" data-url="{{.Repository.Link}}/markup" data-context="{{.Repo.RepoLink}}">
- {{- if .BodyQuery}}{{.BodyQuery}}{{else if .IssueTemplate}}{{.IssueTemplate}}{{else if .PullRequestTemplate}}{{.PullRequestTemplate}}{{else}}{{.content}}{{end -}}
- </textarea>
- </div>
- <div class="ui bottom tab markup" data-tab="preview">
- {{.locale.Tr "loading"}}
- </div>
- </div>
+{{$textareaContent := .BodyQuery}}
+{{if not $textareaContent}}{{$textareaContent = .IssueTemplate}}{{end}}
+{{if not $textareaContent}}{{$textareaContent = .PullRequestTemplate}}{{end}}
+{{if not $textareaContent}}{{$textareaContent = .content}}{{end}}
+
+{{template "shared/combomarkdowneditor" (dict
+ "locale" $.locale
+ "MarkdownPreviewUrl" (print .Repository.Link "/markup")
+ "MarkdownPreviewContext" .RepoLink
+ "TextareaName" "content"
+ "TextareaContent" $textareaContent
+ "DropzoneParentContainer" "form, .ui.form"
+)}}
+
{{if .IsAttachmentEnabled}}
<div class="field">
{{template "repo/upload" .}}
diff --git a/templates/repo/issue/fields/textarea.tmpl b/templates/repo/issue/fields/textarea.tmpl
index 4b390fc9d6..ad3c5efa04 100644
--- a/templates/repo/issue/fields/textarea.tmpl
+++ b/templates/repo/issue/fields/textarea.tmpl
@@ -2,5 +2,5 @@
{{template "repo/issue/fields/header" .}}
{{/* FIXME: preview markdown result */}}
{{/* FIXME: required validation for markdown editor */}}
- <textarea name="form-field-{{.item.ID}}" placeholder="{{.item.Attributes.placeholder}}" class="edit_area {{if .item.Attributes.render}}no-easymde{{end}}" {{if and .item.Validations.required .item.Attributes.render}}required{{end}}>{{.item.Attributes.value}}</textarea>
+ <textarea name="form-field-{{.item.ID}}" placeholder="{{.item.Attributes.placeholder}}" {{if and .item.Validations.required .item.Attributes.render}}required{{end}}>{{.item.Attributes.value}}</textarea>
</div>
diff --git a/templates/repo/issue/labels/edit_delete_label.tmpl b/templates/repo/issue/labels/edit_delete_label.tmpl
index 568195880d..52e586b783 100644
--- a/templates/repo/issue/labels/edit_delete_label.tmpl
+++ b/templates/repo/issue/labels/edit_delete_label.tmpl
@@ -20,7 +20,7 @@
<div class="required field">
<label for="name">{{.locale.Tr "repo.issues.label_title"}}</label>
<div class="ui small input">
- <input class="label-name-input emoji-input" name="title" placeholder="{{.locale.Tr "repo.issues.new_label_placeholder"}}" autofocus required maxlength="50">
+ <input class="label-name-input" name="title" placeholder="{{.locale.Tr "repo.issues.new_label_placeholder"}}" autofocus required maxlength="50">
</div>
</div>
<div class="field label-exclusive-input-field">
diff --git a/templates/repo/issue/labels/label_new.tmpl b/templates/repo/issue/labels/label_new.tmpl
index 0286f8f228..65b1cbe886 100644
--- a/templates/repo/issue/labels/label_new.tmpl
+++ b/templates/repo/issue/labels/label_new.tmpl
@@ -8,7 +8,7 @@
<div class="required field">
<label for="name">{{.locale.Tr "repo.issues.label_title"}}</label>
<div class="ui small input">
- <input class="label-name-input emoji-input" name="title" placeholder="{{.locale.Tr "repo.issues.new_label_placeholder"}}" autofocus required maxlength="50">
+ <input class="label-name-input" name="title" placeholder="{{.locale.Tr "repo.issues.new_label_placeholder"}}" autofocus required maxlength="50">
</div>
</div>
<div class="field label-exclusive-input-field">
diff --git a/templates/repo/issue/view_content.tmpl b/templates/repo/issue/view_content.tmpl
index 99337c531f..2781db4329 100644
--- a/templates/repo/issue/view_content.tmpl
+++ b/templates/repo/issue/view_content.tmpl
@@ -164,25 +164,22 @@
{{template "repo/issue/view_content/sidebar" .}}
</div>
-<div class="gt-hidden" id="edit-content-form">
+<template id="issue-comment-editor-template">
<div class="ui comment form">
- <div class="ui top tabular menu">
- <a class="active write item">{{$.locale.Tr "write"}}</a>
- <a class="preview item" data-url="{{$.Repository.Link}}/markup" data-context="{{$.RepoLink}}">{{$.locale.Tr "preview"}}</a>
- </div>
- <div class="field">
- <div class="ui bottom active tab write">
- <textarea tabindex="1" name="content" class="js-quick-submit"></textarea>
- </div>
- <div class="ui bottom tab preview markup">
- {{$.locale.Tr "loading"}}
- </div>
- </div>
+ {{template "shared/combomarkdowneditor" (dict
+ "locale" $.locale
+ "MarkdownPreviewUrl" (print .Repository.Link "/markup")
+ "MarkdownPreviewContext" .RepoLink
+ "TextareaName" "content"
+ "DropzoneParentContainer" ".ui.form"
+ )}}
+
{{if .IsAttachmentEnabled}}
<div class="field">
{{template "repo/upload" .}}
</div>
{{end}}
+
<div class="field footer">
<div class="text right edit">
<button class="ui basic secondary cancel button" tabindex="3">{{.locale.Tr "repo.issues.cancel"}}</button>
@@ -190,7 +187,7 @@
</div>
</div>
</div>
-</div>
+</template>
{{template "repo/issue/view_content/reference_issue_dialog" .}}
diff --git a/templates/repo/release/new.tmpl b/templates/repo/release/new.tmpl
index 1cd37d2dd3..589fe12cea 100644
--- a/templates/repo/release/new.tmpl
+++ b/templates/repo/release/new.tmpl
@@ -49,18 +49,17 @@
<label>{{.locale.Tr "repo.release.title"}}</label>
<input name="title" placeholder="{{.locale.Tr "repo.release.title"}}" value="{{.title}}" autofocus required maxlength="255">
</div>
- <div class="field content-editor">
+ <div class="field">
<label>{{.locale.Tr "repo.release.content"}}</label>
- <div class="ui top tabular menu" data-write="write" data-preview="preview">
- <a class="active write item" data-tab="write">{{$.locale.Tr "write"}}</a>
- <a class="preview item" data-tab="preview" data-url="{{$.Repository.Link}}/markup" data-context="{{$.RepoLink}}">{{$.locale.Tr "preview"}}</a>
- </div>
- <div class="ui bottom active tab" data-tab="write">
- <textarea name="content">{{.content}}</textarea>
- </div>
- <div class="ui bottom tab markup" data-tab="preview">
- {{$.locale.Tr "loading"}}
- </div>
+
+ {{template "shared/combomarkdowneditor" (dict
+ "locale" $.locale
+ "MarkdownPreviewUrl" (print .Repository.Link "/markup")
+ "MarkdownPreviewContext" .RepoLink
+ "TextareaName" "content"
+ "TextareaContent" .content
+ "DropzoneParentContainer" "form"
+ )}}
</div>
{{range .attachments}}
<div class="field" id="attachment-{{.ID}}">
diff --git a/templates/repo/wiki/new.tmpl b/templates/repo/wiki/new.tmpl
index 085af4cbc9..03d710bb20 100644
--- a/templates/repo/wiki/new.tmpl
+++ b/templates/repo/wiki/new.tmpl
@@ -19,15 +19,18 @@
<div class="help">
{{.locale.Tr "repo.wiki.page_name_desc"}}
</div>
- <div class="ui top attached tabular menu previewtabs" data-write="write" data-preview="preview">
- <a class="active item" data-tab="write">{{.locale.Tr "write"}}</a>
- <a class="item" data-tab="preview" data-url="{{$.Repository.Link}}/markup" data-context="{{$.RepoLink}}">{{$.locale.Tr "preview"}}</a>
- </div>
- <div class="field content" data-loading="{{.locale.Tr "loading"}}">
- <div class="ui bottom active tab" data-tab="write">
- <textarea class="js-quick-submit" id="edit_area" name="content" data-id="wiki-{{.title}}" data-url="{{.Repository.Link}}/markup" data-context="{{.RepoLink}}">{{if .PageIsWikiEdit}}{{.content}}{{else}}{{.locale.Tr "repo.wiki.welcome"}}{{end}}</textarea>
- </div>
- </div>
+
+ {{$content := .content}}
+ {{if not .PageIsWikiEdit}}
+ {{$content = .locale.Tr "repo.wiki.welcome"}}
+ {{end}}
+ {{template "shared/combomarkdowneditor" (dict
+ "locale" $.locale
+ "MarkdownPreviewUrl" (print .Repository.Link "/markup")
+ "MarkdownPreviewContext" .RepoLink
+ "TextareaName" "content"
+ )}}
+
<div class="field">
<input name="message" placeholder="{{.locale.Tr "repo.wiki.default_commit_message"}}">
</div>
diff --git a/templates/shared/combomarkdowneditor.tmpl b/templates/shared/combomarkdowneditor.tmpl
new file mode 100644
index 0000000000..0027ce8427
--- /dev/null
+++ b/templates/shared/combomarkdowneditor.tmpl
@@ -0,0 +1,47 @@
+{{/*
+Template Attributes:
+* locale
+* ContainerId / ContainerClasses : for the container element
+* MarkdownPreviewUrl / MarkdownPreviewContext: for the preview tab
+* TextareaName / TextareaContent / TextareaPlaceholder: for the main textarea
+* DropzoneParentContainer: for file upload (leave it empty if no upload)
+*/}}
+<div {{if .ContainerId}}id="{{.ContainerId}}"{{end}} class="combo-markdown-editor {{.ContainerClasses}}" data-dropzone-parent-container="{{.DropzoneParentContainer}}">
+ {{if .MarkdownPreviewUrl}}
+ <div class="ui top tabular menu">
+ <a class="active item" data-tab-for="markdown-writer">{{.locale.Tr "write"}}</a>
+ <a class="item" data-tab-for="markdown-previewer" data-preview-url="{{.MarkdownPreviewUrl}}" data-preview-context="{{.MarkdownPreviewContext}}">{{.locale.Tr "preview"}}</a>
+ </div>
+ {{end}}
+ <div class="ui tab active" data-tab-panel="markdown-writer">
+ <markdown-toolbar class="gt-df">
+ <div class="markdown-toolbar-group">
+ <md-header class="markdown-toolbar-button">{{svg "octicon-heading"}}</md-header>
+ <md-bold class="markdown-toolbar-button">{{svg "octicon-bold"}}</md-bold>
+ <md-italic class="markdown-toolbar-button">{{svg "octicon-italic"}}</md-italic>
+ </div>
+ <div class="markdown-toolbar-group">
+ <md-quote class="markdown-toolbar-button">{{svg "octicon-quote"}}</md-quote>
+ <md-code class="markdown-toolbar-button">{{svg "octicon-code"}}</md-code>
+ <md-link class="markdown-toolbar-button">{{svg "octicon-link"}}</md-link>
+ </div>
+ <div class="markdown-toolbar-group">
+ <md-unordered-list class="markdown-toolbar-button">{{svg "octicon-list-unordered"}}</md-unordered-list>
+ <md-ordered-list class="markdown-toolbar-button">{{svg "octicon-list-ordered"}}</md-ordered-list>
+ <md-task-list class="markdown-toolbar-button">{{svg "octicon-tasklist"}}</md-task-list>
+ </div>
+ <div class="markdown-toolbar-group">
+ <md-mention class="markdown-toolbar-button">{{svg "octicon-mention"}}</md-mention>
+ <md-ref class="markdown-toolbar-button">{{svg "octicon-cross-reference"}}</md-ref>
+ </div>
+ <div class="markdown-toolbar-group gt-f1"></div>
+ <div class="markdown-toolbar-group">
+ <span class="markdown-toolbar-button markdown-switch-easymde">{{svg "octicon-arrow-switch"}}</span>
+ </div>
+ </markdown-toolbar>
+ <textarea class="markdown-text-editor js-quick-submit" name="{{.TextareaName}}" placeholder="{{.TextareaPlaceholder}}">{{.TextareaContent}}</textarea>
+ </div>
+ <div class="ui tab markup" data-tab-panel="markdown-previewer">
+ {{.locale.Tr "loading"}}
+ </div>
+</div>
diff --git a/web_src/css/editor-markdown.css b/web_src/css/editor-markdown.css
new file mode 100644
index 0000000000..31ffeb06d0
--- /dev/null
+++ b/web_src/css/editor-markdown.css
@@ -0,0 +1,25 @@
+.combo-markdown-editor {
+ width: 100%;
+}
+
+.combo-markdown-editor markdown-toolbar {
+ cursor: default;
+ display: block;
+ padding-bottom: 10px;
+}
+
+.combo-markdown-editor .markdown-toolbar-group {
+ display: inline-block;
+}
+
+.combo-markdown-editor .markdown-toolbar-button {
+ user-select: none;
+ padding: 5px;
+ cursor: pointer;
+}
+
+.combo-markdown-editor .markdown-text-editor {
+ display: block;
+ width: 100%;
+ height: 200px;
+}
diff --git a/web_src/css/editor.css b/web_src/css/editor.css
index d3f9edeb2d..ba35036e4f 100644
--- a/web_src/css/editor.css
+++ b/web_src/css/editor.css
@@ -13,7 +13,6 @@
}
.editor-toolbar {
- max-width: calc(100vw - 80px);
border-color: var(--color-secondary);
}
diff --git a/web_src/css/index.css b/web_src/css/index.css
index dd5f739379..e8d4e290d0 100644
--- a/web_src/css/index.css
+++ b/web_src/css/index.css
@@ -29,6 +29,7 @@
@import "./form.css";
@import "./repository.css";
@import "./editor.css";
+@import "./editor-markdown.css";
@import "./organization.css";
@import "./user.css";
@import "./dashboard.css";
diff --git a/web_src/css/repository.css b/web_src/css/repository.css
index 27d6a51cdd..fb5c75b73c 100644
--- a/web_src/css/repository.css
+++ b/web_src/css/repository.css
@@ -2116,10 +2116,6 @@
height: 48px;
}
-.repository.wiki.new .ui.attached.tabular.menu.previewtabs {
- margin-bottom: 15px;
-}
-
.repository.wiki.view > .markup {
padding: 15px 30px;
}
diff --git a/web_src/css/review.css b/web_src/css/review.css
index 3deb2192fc..913a7e9df2 100644
--- a/web_src/css/review.css
+++ b/web_src/css/review.css
@@ -248,6 +248,11 @@ a.blob-excerpt:hover {
}
}
+.review-box-panel .combo-markdown-editor textarea {
+ width: 730px;
+ max-width: calc(100vw - 70px);
+}
+
#review-box {
position: relative;
}
diff --git a/web_src/js/features/comp/ComboMarkdownEditor.js b/web_src/js/features/comp/ComboMarkdownEditor.js
new file mode 100644
index 0000000000..4905ec2341
--- /dev/null
+++ b/web_src/js/features/comp/ComboMarkdownEditor.js
@@ -0,0 +1,277 @@
+import '@github/markdown-toolbar-element';
+import {attachTribute} from '../tribute.js';
+import {hideElem, showElem} from '../../utils/dom.js';
+import {initEasyMDEImagePaste, initTextareaImagePaste} from './ImagePaste.js';
+import $ from 'jquery';
+import {initMarkupContent} from '../../markup/content.js';
+import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';
+import {attachRefIssueContextPopup} from '../contextpopup.js';
+
+let elementIdCounter = 0;
+
+/**
+ * validate if the given textarea is non-empty.
+ * @param {jQuery} $textarea
+ * @returns {boolean} returns true if validation succeeded.
+ */
+export function validateTextareaNonEmpty($textarea) {
+ // When using EasyMDE, the original edit area HTML element is hidden, breaking HTML5 input validation.
+ // The workaround (https://github.com/sparksuite/simplemde-markdown-editor/issues/324) doesn't work with contenteditable, so we just show an alert.
+ if (!$textarea.val()) {
+ if ($textarea.is(':visible')) {
+ $textarea.prop('required', true);
+ const $form = $textarea.parents('form');
+ $form[0]?.reportValidity();
+ } else {
+ // The alert won't hurt users too much, because we are dropping the EasyMDE and the check only occurs in a few places.
+ alert('Require non-empty content');
+ }
+ return false;
+ }
+ return true;
+}
+
+class ComboMarkdownEditor {
+ constructor(container, options = {}) {
+ container._giteaComboMarkdownEditor = this;
+ this.options = options;
+ this.container = container;
+ }
+
+ async init() {
+ this.textarea = this.container.querySelector('.markdown-text-editor');
+ this.textarea._giteaComboMarkdownEditor = this;
+ this.textarea.id = `_combo_markdown_editor_${String(elementIdCounter)}`;
+ this.textarea.addEventListener('input', (e) => {this.options?.onContentChanged?.(this, e)});
+ this.textareaMarkdownToolbar = this.container.querySelector('markdown-toolbar');
+ this.textareaMarkdownToolbar.setAttribute('for', this.textarea.id);
+
+ elementIdCounter++;
+
+ this.switchToEasyMDEButton = this.container.querySelector('.markdown-switch-easymde');
+ this.switchToEasyMDEButton?.addEventListener('click', async (e) => {
+ e.preventDefault();
+ await this.switchToEasyMDE();
+ });
+
+ await attachTribute(this.textarea, {mentions: true, emoji: true});
+
+ const dropzoneParentContainer = this.container.getAttribute('data-dropzone-parent-container');
+ if (dropzoneParentContainer) {
+ this.dropzone = this.container.closest(this.container.getAttribute('data-dropzone-parent-container'))?.querySelector('.dropzone');
+ initTextareaImagePaste(this.textarea, this.dropzone);
+ }
+
+ this.setupTab();
+ this.prepareEasyMDEToolbarActions();
+ }
+
+ setupTab() {
+ const $container = $(this.container);
+ const $tabMenu = $container.find('.tabular.menu');
+ const $tabs = $tabMenu.find('> .item');
+
+ // Fomantic Tab requires the "data-tab" to be globally unique.
+ // So here it uses our defined "data-tab-for" and "data-tab-panel" to generate the "data-tab" attribute for Fomantic.
+ const $tabEditor = $tabs.filter(`.item[data-tab-for="markdown-writer"]`);
+ const $tabPreviewer = $tabs.filter(`.item[data-tab-for="markdown-previewer"]`);
+ $tabEditor.attr('data-tab', `markdown-writer-${elementIdCounter}`);
+ $tabPreviewer.attr('data-tab', `markdown-previewer-${elementIdCounter}`);
+ const $panelEditor = $container.find('.ui.tab[data-tab-panel="markdown-writer"]');
+ const $panelPreviewer = $container.find('.ui.tab[data-tab-panel="markdown-previewer"]');
+ $panelEditor.attr('data-tab', `markdown-writer-${elementIdCounter}`);
+ $panelPreviewer.attr('data-tab', `markdown-previewer-${elementIdCounter}`);
+ elementIdCounter++;
+
+ $tabs.tab();
+
+ this.previewUrl = $tabPreviewer.attr('data-preview-url');
+ this.previewContext = $tabPreviewer.attr('data-preview-context');
+ this.previewMode = this.options.previewMode ?? 'comment';
+ this.previewWiki = this.options.previewWiki ?? false;
+ $tabPreviewer.on('click', () => {
+ $.post(this.previewUrl, {
+ _csrf: window.config.csrfToken,
+ mode: this.previewMode,
+ context: this.previewContext,
+ text: this.value(),
+ wiki: this.previewWiki,
+ }, (data) => {
+ $panelPreviewer.html(data);
+ initMarkupContent();
+
+ const refIssues = $panelPreviewer.find('p .ref-issue');
+ attachRefIssueContextPopup(refIssues);
+ });
+ });
+ }
+
+ prepareEasyMDEToolbarActions() {
+ this.easyMDEToolbarDefault = [
+ 'bold', 'italic', 'strikethrough', '|', 'heading-1', 'heading-2', 'heading-3', 'heading-bigger', 'heading-smaller', '|',
+ 'code', 'quote', '|', 'gitea-checkbox-empty', 'gitea-checkbox-checked', '|',
+ 'unordered-list', 'ordered-list', '|', 'link', 'image', 'table', 'horizontal-rule', '|', 'clean-block', '|',
+ 'gitea-switch-to-textarea',
+ ];
+
+ this.easyMDEToolbarActions = {
+ 'gitea-checkbox-empty': {
+ action(e) {
+ const cm = e.codemirror;
+ cm.replaceSelection(`\n- [ ] ${cm.getSelection()}`);
+ cm.focus();
+ },
+ className: 'fa fa-square-o',
+ title: 'Add Checkbox (empty)',
+ },
+ 'gitea-checkbox-checked': {
+ action(e) {
+ const cm = e.codemirror;
+ cm.replaceSelection(`\n- [x] ${cm.getSelection()}`);
+ cm.focus();
+ },
+ className: 'fa fa-check-square-o',
+ title: 'Add Checkbox (checked)',
+ },
+ 'gitea-switch-to-textarea': {
+ action: this.switchToTextarea.bind(this),
+ className: 'fa fa-file',
+ title: 'Revert to simple textarea',
+ },
+ 'gitea-code-inline': {
+ action(e) {
+ const cm = e.codemirror;
+ const selection = cm.getSelection();
+ cm.replaceSelection(`\`${selection}\``);
+ if (!selection) {
+ const cursorPos = cm.getCursor();
+ cm.setCursor(cursorPos.line, cursorPos.ch - 1);
+ }
+ cm.focus();
+ },
+ className: 'fa fa-angle-right',
+ title: 'Add Inline Code',
+ }
+ };
+ }
+
+ parseEasyMDEToolbar(actions) {
+ const processed = [];
+ for (const action of actions) {
+ if (action.startsWith('gitea-')) {
+ const giteaAction = this.easyMDEToolbarActions[action];
+ if (!giteaAction) throw new Error(`Unknown EasyMDE toolbar action ${action}`);
+ processed.push(giteaAction);
+ } else {
+ processed.push(action);
+ }
+ }
+ return processed;
+ }
+
+ async switchToTextarea() {
+ showElem(this.textareaMarkdownToolbar);
+ if (this.easyMDE) {
+ this.easyMDE.toTextArea();
+ this.easyMDE = null;
+ }
+ }
+
+ async switchToEasyMDE() {
+ // EasyMDE's CSS should be loaded via webpack config, otherwise our own styles can not overwrite the default styles.
+ const {default: EasyMDE} = await import(/* webpackChunkName: "easymde" */'easymde');
+ const easyMDEOpt = {
+ autoDownloadFontAwesome: false,
+ element: this.textarea,
+ forceSync: true,
+ renderingConfig: {singleLineBreaks: false},
+ indentWithTabs: false,
+ tabSize: 4,
+ spellChecker: false,
+ inputStyle: 'contenteditable', // nativeSpellcheck requires contenteditable
+ nativeSpellcheck: true,
+ ...this.options.easyMDEOptions,
+ };
+ easyMDEOpt.toolbar = this.parseEasyMDEToolbar(easyMDEOpt.toolbar ?? this.easyMDEToolbarDefault);
+
+ this.easyMDE = new EasyMDE(easyMDEOpt);
+ this.easyMDE.codemirror.on('change', (...args) => {this.options?.onContentChanged?.(this, ...args)});
+ this.easyMDE.codemirror.setOption('extraKeys', {
+ 'Cmd-Enter': (cm) => handleGlobalEnterQuickSubmit(cm.getTextArea()),
+ 'Ctrl-Enter': (cm) => handleGlobalEnterQuickSubmit(cm.getTextArea()),
+ Enter: (cm) => {
+ const tributeContainer = document.querySelector('.tribute-container');
+ if (!tributeContainer || tributeContainer.style.display === 'none') {
+ cm.execCommand('newlineAndIndent');
+ }
+ },
+ Up: (cm) => {
+ const tributeContainer = document.querySelector('.tribute-container');
+ if (!tributeContainer || tributeContainer.style.display === 'none') {
+ return cm.execCommand('goLineUp');
+ }
+ },
+ Down: (cm) => {
+ const tributeContainer = document.querySelector('.tribute-container');
+ if (!tributeContainer || tributeContainer.style.display === 'none') {
+ return cm.execCommand('goLineDown');
+ }
+ },
+ });
+ await attachTribute(this.easyMDE.codemirror.getInputField(), {mentions: true, emoji: true});
+ initEasyMDEImagePaste(this.easyMDE, this.dropzone);
+ hideElem(this.textareaMarkdownToolbar);
+ }
+
+ value(v = undefined) {
+ if (v === undefined) {
+ if (this.easyMDE) {
+ return this.easyMDE.value();
+ }
+ return this.textarea.value;
+ }
+
+ if (this.easyMDE) {
+ this.easyMDE.value(v);
+ } else {
+ this.textarea.value = v;
+ }
+ }
+
+ focus() {
+ if (this.easyMDE) {
+ this.easyMDE.codemirror.focus();
+ } else {
+ this.textarea.focus();
+ }
+ }
+
+ moveCursorToEnd() {
+ this.textarea.focus();
+ this.textarea.setSelectionRange(this.textarea.value.length, this.textarea.value.length);
+ if (this.easyMDE) {
+ this.easyMDE.codemirror.focus();
+ this.easyMDE.codemirror.setCursor(this.easyMDE.codemirror.lineCount(), 0);
+ }
+ }
+}
+
+export function getComboMarkdownEditor(el) {
+ if (el instanceof $) el = el[0];
+ return el?._giteaComboMarkdownEditor;
+}
+
+export async function initComboMarkdownEditor(container, options = {}) {
+ if (container instanceof $) {
+ if (container.length !== 1) {
+ throw new Error('initComboMarkdownEditor: container must be a single element');
+ }
+ container = container[0];
+ }
+ if (!container) {
+ throw new Error('initComboMarkdownEditor: container is null');
+ }
+ const editor = new ComboMarkdownEditor(container, options);
+ await editor.init();
+ return editor;
+}
diff --git a/web_src/js/features/comp/EasyMDE.js b/web_src/js/features/comp/EasyMDE.js
deleted file mode 100644
index 2979627b00..0000000000
--- a/web_src/js/features/comp/EasyMDE.js
+++ /dev/null
@@ -1,181 +0,0 @@
-import $ from 'jquery';
-import {attachTribute} from '../tribute.js';
-import {handleGlobalEnterQuickSubmit} from './QuickSubmit.js';
-
-/**
- * @returns {EasyMDE}
- */
-export async function importEasyMDE() {
- // EasyMDE's CSS should be loaded via webpack config, otherwise our own styles can
- // not overwrite the default styles.
- const {default: EasyMDE} = await import(/* webpackChunkName: "easymde" */'easymde');
- return EasyMDE;
-}
-
-/**
- * create an EasyMDE editor for comment
- * @param textarea jQuery or HTMLElement
- * @param easyMDEOptions the options for EasyMDE
- * @returns {null|EasyMDE}
- */
-export async function createCommentEasyMDE(textarea, easyMDEOptions = {}) {
- if (textarea instanceof $) {
- textarea = textarea[0];
- }
- if (!textarea) {
- return null;
- }
-
- const EasyMDE = await importEasyMDE();
-
- const easyMDE = new EasyMDE({
- autoDownloadFontAwesome: false,
- element: textarea,
- forceSync: true,
- renderingConfig: {
- singleLineBreaks: false,
- },
- indentWithTabs: false,
- tabSize: 4,
- spellChecker: false,
- inputStyle: 'contenteditable', // nativeSpellcheck requires contenteditable
- nativeSpellcheck: true,
- toolbar: ['bold', 'italic', 'strikethrough', '|',
- 'heading-1', 'heading-2', 'heading-3', 'heading-bigger', 'heading-smaller', '|',
- 'code', 'quote', '|', {
- name: 'checkbox-empty',
- action(e) {
- const cm = e.codemirror;
- cm.replaceSelection(`\n- [ ] ${cm.getSelection()}`);
- cm.focus();
- },
- className: 'fa fa-square-o',
- title: 'Add Checkbox (empty)',
- },
- {
- name: 'checkbox-checked',
- action(e) {
- const cm = e.codemirror;
- cm.replaceSelection(`\n- [x] ${cm.getSelection()}`);
- cm.focus();
- },
- className: 'fa fa-check-square-o',
- title: 'Add Checkbox (checked)',
- }, '|',
- 'unordered-list', 'ordered-list', '|',
- 'link', 'image', 'table', 'horizontal-rule', '|',
- 'clean-block', '|',
- {
- name: 'revert-to-textarea',
- action(e) {
- e.toTextArea();
- },
- className: 'fa fa-file',
- title: 'Revert to simple textarea',
- },
- ], ...easyMDEOptions});
-
- const inputField = easyMDE.codemirror.getInputField();
-
- easyMDE.codemirror.on('change', (...args) => {
- easyMDEOptions?.onChange?.(...args);
- });
- easyMDE.codemirror.setOption('extraKeys', {
- 'Cmd-Enter': codeMirrorQuickSubmit,
- 'Ctrl-Enter': codeMirrorQuickSubmit,
- Enter: (cm) => {
- const tributeContainer = document.querySelector('.tribute-container');
- if (!tributeContainer || tributeContainer.style.display === 'none') {
- cm.execCommand('newlineAndIndent');
- }
- },
- Backspace: (cm) => {
- if (cm.getInputField().trigger) {
- cm.getInputField().trigger('input');
- }
- cm.execCommand('delCharBefore');
- },
- Up: (cm) => {
- const tributeContainer = document.querySelector('.tribute-container');
- if (!tributeContainer || tributeContainer.style.display === 'none') {
- return cm.execCommand('goLineUp');
- }
- },
- Down: (cm) => {
- const tributeContainer = document.querySelector('.tribute-container');
- if (!tributeContainer || tributeContainer.style.display === 'none') {
- return cm.execCommand('goLineDown');
- }
- },
- });
- await attachTribute(inputField, {mentions: true, emoji: true});
- attachEasyMDEToElements(easyMDE);
- return easyMDE;
-}
-
-/**
- * attach the EasyMDE object to its input elements (InputField, TextArea)
- * @param {EasyMDE} easyMDE
- */
-export function attachEasyMDEToElements(easyMDE) {
- // TODO: that's the only way we can do now to attach the EasyMDE object to a HTMLElement
-
- // InputField is used by CodeMirror to accept user input
- const inputField = easyMDE.codemirror.getInputField();
- inputField._data_easyMDE = easyMDE;
-
- // TextArea is the real textarea element in the form
- const textArea = easyMDE.codemirror.getTextArea();
- textArea._data_easyMDE = easyMDE;
-}
-
-
-/**
- * get the attached EasyMDE editor created by createCommentEasyMDE
- * @param el jQuery or HTMLElement
- * @returns {null|EasyMDE}
- */
-export function getAttachedEasyMDE(el) {
- if (el instanceof $) {
- el = el[0];
- }
- if (!el) {
- return null;
- }
- return el._data_easyMDE;
-}
-
-/**
- * validate if the given EasyMDE textarea is is non-empty.
- * @param {jQuery} $textarea
- * @returns {boolean} returns true if validation succeeded.
- */
-export function validateTextareaNonEmpty($textarea) {
- const $mdeInputField = $(getAttachedEasyMDE($textarea).codemirror.getInputField());
- // The original edit area HTML element is hidden and replaced by the
- // SimpleMDE/EasyMDE editor, breaking HTML5 input validation if the text area is empty.
- // This is a workaround for this upstream bug.
- // See https://github.com/sparksuite/simplemde-markdown-editor/issues/324
- if (!$textarea.val()) {
- $mdeInputField.prop('required', true);
- const $form = $textarea.parents('form');
- if (!$form.length) {
- // this should never happen. we put a alert here in case the textarea would be forgotten to be put in a form
- alert('Require non-empty content');
- } else {
- $form[0].reportValidity();
- }
- return false;
- }
- $mdeInputField.prop('required', false);
- return true;
-}
-
-/**
- * there is no guarantee that the CodeMirror object is inside the same form as the textarea,
- * so can not call handleGlobalEnterQuickSubmit directly.
- * @param {CodeMirror.EditorFromTextArea} codeMirror
- */
-export function codeMirrorQuickSubmit(codeMirror) {
- handleGlobalEnterQuickSubmit(codeMirror.getTextArea());
-}
diff --git a/web_src/js/features/comp/ImagePaste.js b/web_src/js/features/comp/ImagePaste.js
index da41e7611a..9145b24062 100644
--- a/web_src/js/features/comp/ImagePaste.js
+++ b/web_src/js/features/comp/ImagePaste.js
@@ -88,38 +88,43 @@ class CodeMirrorEditor {
}
-export function initEasyMDEImagePaste(easyMDE, $dropzone) {
+const uploadClipboardImage = async (editor, dropzone, e) => {
+ const $dropzone = $(dropzone);
const uploadUrl = $dropzone.attr('data-upload-url');
const $files = $dropzone.find('.files');
if (!uploadUrl || !$files.length) return;
- const uploadClipboardImage = async (editor, e) => {
- const pastedImages = clipboardPastedImages(e);
- if (!pastedImages || pastedImages.length === 0) {
- return;
- }
- e.preventDefault();
- e.stopPropagation();
+ const pastedImages = clipboardPastedImages(e);
+ if (!pastedImages || pastedImages.length === 0) {
+ return;
+ }
+ e.preventDefault();
+ e.stopPropagation();
- for (const img of pastedImages) {
- const name = img.name.slice(0, img.name.lastIndexOf('.'));
+ for (const img of pastedImages) {
+ const name = img.name.slice(0, img.name.lastIndexOf('.'));
- const placeholder = `![${name}](uploading ...)`;
- editor.insertPlaceholder(placeholder);
- const data = await uploadFile(img, uploadUrl);
- editor.replacePlaceholder(placeholder, `![${name}](/attachments/${data.uuid})`);
+ const placeholder = `![${name}](uploading ...)`;
+ editor.insertPlaceholder(placeholder);
+ const data = await uploadFile(img, uploadUrl);
+ editor.replacePlaceholder(placeholder, `![${name}](/attachments/${data.uuid})`);
- const $input = $(`<input name="files" type="hidden">`).attr('id', data.uuid).val(data.uuid);
- $files.append($input);
- }
- };
+ const $input = $(`<input name="files" type="hidden">`).attr('id', data.uuid).val(data.uuid);
+ $files.append($input);
+ }
+};
+export function initEasyMDEImagePaste(easyMDE, dropzone) {
+ if (!dropzone) return;
easyMDE.codemirror.on('paste', async (_, e) => {
- return uploadClipboardImage(new CodeMirrorEditor(easyMDE.codemirror), e);
+ return uploadClipboardImage(new CodeMirrorEditor(easyMDE.codemirror), dropzone, e);
});
+}
- $(easyMDE.element).on('paste', async (e) => {
- return uploadClipboardImage(new TextareaEditor(easyMDE.element), e.originalEvent);
+export function initTextareaImagePaste(textarea, dropzone) {
+ if (!dropzone) return;
+ $(textarea).on('paste', async (e) => {
+ return uploadClipboardImage(new TextareaEditor(textarea), dropzone, e.originalEvent);
});
}
diff --git a/web_src/js/features/comp/MarkupContentPreview.js b/web_src/js/features/comp/MarkupContentPreview.js
deleted file mode 100644
index a32bf30184..0000000000
--- a/web_src/js/features/comp/MarkupContentPreview.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import $ from 'jquery';
-import {initMarkupContent} from '../../markup/content.js';
-import {attachRefIssueContextPopup} from '../contextpopup.js';
-
-const {csrfToken} = window.config;
-
-export function initCompMarkupContentPreviewTab($form) {
- const $tabMenu = $form.find('.tabular.menu');
- $tabMenu.find('.item').tab();
- $tabMenu.find(`.item[data-tab="${$tabMenu.data('preview')}"]`).on('click', function () {
- const $this = $(this);
- $.post($this.data('url'), {
- _csrf: csrfToken,
- mode: 'comment',
- context: $this.data('context'),
- text: $form.find(`.tab[data-tab="${$tabMenu.data('write')}"] textarea`).val()
- }, (data) => {
- const $previewPanel = $form.find(`.tab[data-tab="${$tabMenu.data('preview')}"]`);
- $previewPanel.html(data);
- const refIssues = $previewPanel.find('p .ref-issue');
- attachRefIssueContextPopup(refIssues);
- initMarkupContent();
- });
- });
-}
diff --git a/web_src/js/features/contextpopup.js b/web_src/js/features/contextpopup.js
index 7b37035547..5c5733b35a 100644
--- a/web_src/js/features/contextpopup.js
+++ b/web_src/js/features/contextpopup.js
@@ -10,17 +10,16 @@ export function initContextPopups() {
}
export function attachRefIssueContextPopup(refIssues) {
- if (!refIssues.length) return;
- refIssues.each(function () {
- if ($(this).hasClass('ref-external-issue')) {
+ for (const refIssue of refIssues) {
+ if (refIssue.classList.contains('ref-external-issue')) {
return;
}
- const {owner, repo, index} = parseIssueHref($(this).attr('href'));
+ const {owner, repo, index} = parseIssueHref(refIssue.getAttribute('href'));
if (!owner) return;
const el = document.createElement('div');
- this.parentNode.insertBefore(el, this.nextSibling);
+ refIssue.parentNode.insertBefore(el, refIssue.nextSibling);
const view = createApp(ContextPopup);
@@ -31,7 +30,7 @@ export function attachRefIssueContextPopup(refIssues) {
el.textContent = 'ContextPopup failed to load';
}
- createTippy(this, {
+ createTippy(refIssue, {
content: el,
placement: 'top-start',
interactive: true,
@@ -40,5 +39,5 @@ export function attachRefIssueContextPopup(refIssues) {
el.firstChild.dispatchEvent(new CustomEvent('ce-load-context-popup', {detail: {owner, repo, index}}));
}
});
- });
+ }
}
diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js
index 458f11c6f2..df66db7f6c 100644
--- a/web_src/js/features/repo-diff.js
+++ b/web_src/js/features/repo-diff.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import {initCompReactionSelector} from './comp/ReactionSelector.js';
import {initRepoIssueContentHistory} from './repo-issue-content.js';
-import {validateTextareaNonEmpty} from './comp/EasyMDE.js';
import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles} from './pull-view-file.js';
+import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.js';
const {csrfToken} = window.config;
diff --git a/web_src/js/features/repo-issue.js b/web_src/js/features/repo-issue.js
index 3f1b73d91e..03c9977f49 100644
--- a/web_src/js/features/repo-issue.js
+++ b/web_src/js/features/repo-issue.js
@@ -1,12 +1,9 @@
import $ from 'jquery';
import {htmlEscape} from 'escape-goat';
-import {attachTribute} from './tribute.js';
-import {createCommentEasyMDE, getAttachedEasyMDE} from './comp/EasyMDE.js';
-import {initEasyMDEImagePaste} from './comp/ImagePaste.js';
-import {initCompMarkupContentPreviewTab} from './comp/MarkupContentPreview.js';
import {showTemporaryTooltip, createTippy} from '../modules/tippy.js';
import {hideElem, showElem, toggleElem} from '../utils/dom.js';
import {setFileFolding} from './file-fold.js';
+import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
const {appSubUrl, csrfToken} = window.config;
@@ -223,21 +220,6 @@ export function initRepoIssueCodeCommentCancel() {
});
}
-export function initRepoIssueStatusButton() {
- // Change status
- const $statusButton = $('#status-button');
- $('#comment-form textarea').on('keyup', function () {
- const easyMDE = getAttachedEasyMDE(this);
- const value = easyMDE?.value() || $(this).val();
- $statusButton.text($statusButton.data(value.length === 0 ? 'status' : 'status-and-comment'));
- });
- $statusButton.on('click', (e) => {
- e.preventDefault();
- $('#status').val($statusButton.data('status-val'));
- $('#comment-form').trigger('submit');
- });
-}
-
export function initRepoPullRequestUpdate() {
// Pull Request update button
const $pullUpdateButton = $('.update-button > button');
@@ -402,35 +384,18 @@ export function initRepoIssueComments() {
});
}
-
-function assignMenuAttributes(menu) {
- const id = Math.floor(Math.random() * Math.floor(1000000));
- menu.attr('data-write', menu.attr('data-write') + id);
- menu.attr('data-preview', menu.attr('data-preview') + id);
- menu.find('.item').each(function () {
- const tab = $(this).attr('data-tab') + id;
- $(this).attr('data-tab', tab);
- });
- menu.parent().find("*[data-tab='write']").attr('data-tab', `write${id}`);
- menu.parent().find("*[data-tab='preview']").attr('data-tab', `preview${id}`);
- initCompMarkupContentPreviewTab(menu.parent('.form'));
- return id;
-}
-
export async function handleReply($el) {
hideElem($el);
const form = $el.closest('.comment-code-cloud').find('.comment-form');
form.removeClass('gt-hidden');
+
const $textarea = form.find('textarea');
- let easyMDE = getAttachedEasyMDE($textarea);
- if (!easyMDE) {
- await attachTribute($textarea.get(), {mentions: true, emoji: true});
- easyMDE = await createCommentEasyMDE($textarea);
+ let editor = getComboMarkdownEditor($textarea);
+ if (!editor) {
+ editor = await initComboMarkdownEditor(form.find('.combo-markdown-editor'));
}
- $textarea.focus();
- easyMDE.codemirror.focus();
- assignMenuAttributes(form.find('.menu'));
- return easyMDE;
+ editor.focus();
+ return editor;
}
export function initRepoPullRequestReview() {
@@ -494,14 +459,7 @@ export function initRepoPullRequestReview() {
const $reviewBox = $('.review-box-panel');
if ($reviewBox.length === 1) {
- (async () => {
- // the editor's height is too large in some cases, and the panel cannot be scrolled with page now because there is `.repository .diff-detail-box.sticky { position: sticky; }`
- // the temporary solution is to make the editor's height smaller (about 4 lines). GitHub also only show 4 lines for default. We can improve the UI (including Dropzone area) in future
- // EasyMDE's options can not handle minHeight & maxHeight together correctly, we have to set max-height for .CodeMirror-scroll in CSS.
- const $reviewTextarea = $reviewBox.find('textarea');
- const easyMDE = await createCommentEasyMDE($reviewTextarea, {minHeight: '80px'});
- initEasyMDEImagePaste(easyMDE, $reviewBox.find('.dropzone'));
- })();
+ const _promise = initComboMarkdownEditor($reviewBox.find('.combo-markdown-editor'));
}
// The following part is only for diff views
@@ -565,20 +523,16 @@ export function initRepoPullRequestReview() {
}
const td = ntr.find(`.add-comment-${side}`);
- let commentCloud = td.find('.comment-code-cloud');
+ const commentCloud = td.find('.comment-code-cloud');
if (commentCloud.length === 0 && !ntr.find('button[name="pending_review"]').length) {
- const data = await $.get($(this).closest('[data-new-comment-url]').data('new-comment-url'));
- td.html(data);
- commentCloud = td.find('.comment-code-cloud');
- assignMenuAttributes(commentCloud.find('.menu'));
+ const html = await $.get($(this).closest('[data-new-comment-url]').attr('data-new-comment-url'));
+ td.html(html);
td.find("input[name='line']").val(idx);
td.find("input[name='side']").val(side === 'left' ? 'previous' : 'proposed');
td.find("input[name='path']").val(path);
- const $textarea = commentCloud.find('textarea');
- await attachTribute($textarea.get(), {mentions: true, emoji: true});
- const easyMDE = await createCommentEasyMDE($textarea);
- $textarea.focus();
- easyMDE.codemirror.focus();
+
+ const editor = await initComboMarkdownEditor(td.find('.combo-markdown-editor'));
+ editor.focus();
}
});
}
diff --git a/web_src/js/features/repo-legacy.js b/web_src/js/features/repo-legacy.js
index 3689c34272..2e39d3762f 100644
--- a/web_src/js/features/repo-legacy.js
+++ b/web_src/js/features/repo-legacy.js
@@ -1,11 +1,8 @@
import $ from 'jquery';
-import {createCommentEasyMDE, getAttachedEasyMDE} from './comp/EasyMDE.js';
-import {initCompMarkupContentPreviewTab} from './comp/MarkupContentPreview.js';
-import {initEasyMDEImagePaste} from './comp/ImagePaste.js';
import {
initRepoIssueBranchSelect, initRepoIssueCodeCommentCancel, initRepoIssueCommentDelete,
initRepoIssueComments, initRepoIssueDependencyDelete, initRepoIssueReferenceIssue,
- initRepoIssueStatusButton, initRepoIssueTitleEdit, initRepoIssueWipToggle,
+ initRepoIssueTitleEdit, initRepoIssueWipToggle,
initRepoPullRequestUpdate, updateIssuesMeta, handleReply
} from './repo-issue.js';
import {initUnicodeEscapeButton} from './repo-unicode-escape.js';
@@ -19,27 +16,27 @@ import {
import {initCitationFileCopyContent} from './citation.js';
import {initCompLabelEdit} from './comp/LabelEdit.js';
import {initRepoDiffConversationNav} from './repo-diff.js';
-import {attachTribute} from './tribute.js';
import {createDropzone} from './dropzone.js';
import {initCommentContent, initMarkupContent} from '../markup/content.js';
import {initCompReactionSelector} from './comp/ReactionSelector.js';
import {initRepoSettingBranches} from './repo-settings.js';
import {initRepoPullRequestMergeForm} from './repo-issue-pr-form.js';
import {hideElem, showElem} from '../utils/dom.js';
+import {getComboMarkdownEditor, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
import {attachRefIssueContextPopup} from './contextpopup.js';
const {csrfToken} = window.config;
-// if there are draft comments (more than 20 chars), confirm before reloading, to avoid losing comments
+// if there are draft comments, confirm before reloading, to avoid losing comments
function reloadConfirmDraftComment() {
const commentTextareas = [
document.querySelector('.edit-content-zone:not(.gt-hidden) textarea'),
- document.querySelector('.edit_area'),
+ document.querySelector('#comment-form textarea'),
];
for (const textarea of commentTextareas) {
- // Most users won't feel too sad if they lose a comment with 10 or 20 chars, they can re-type these in seconds.
+ // Most users won't feel too sad if they lose a comment with 10 chars, they can re-type these in seconds.
// But if they have typed more (like 50) chars and the comment is lost, they will be very unhappy.
- if (textarea && textarea.value.trim().length > 20) {
+ if (textarea && textarea.value.trim().length > 10) {
textarea.parentElement.scrollIntoView();
if (!window.confirm('Page will be reloaded, but there are draft comments. Continuing to reload will discard the comments. Continue?')) {
return;
@@ -85,25 +82,20 @@ export function initRepoCommentForm() {
});
}
- (async () => {
- const $statusButton = $('#status-button');
- for (const textarea of $commentForm.find('textarea:not(.review-textarea, .no-easymde)')) {
- // Don't initialize EasyMDE for the dormant #edit-content-form
- if (textarea.closest('#edit-content-form')) {
- continue;
- }
- const easyMDE = await createCommentEasyMDE(textarea, {
- 'onChange': () => {
- const value = easyMDE?.value().trim();
- $statusButton.text($statusButton.attr(value.length === 0 ? 'data-status' : 'data-status-and-comment'));
- },
- });
- initEasyMDEImagePaste(easyMDE, $commentForm.find('.dropzone'));
- }
- })();
+ const $statusButton = $('#status-button');
+ $statusButton.on('click', (e) => {
+ e.preventDefault();
+ $('#status').val($statusButton.data('status-val'));
+ $('#comment-form').trigger('submit');
+ });
+
+ const _promise = initComboMarkdownEditor($commentForm.find('.combo-markdown-editor'), {
+ onContentChanged(editor) {
+ $statusButton.text($statusButton.attr(editor.value().trim() ? 'data-status-and-comment' : 'data-status'));
+ },
+ });
initBranchSelector();
- initCompMarkupContentPreviewTab($commentForm);
// List submits
function initListSubmits(selector, outerSelector) {
@@ -275,7 +267,7 @@ export function initRepoCommentForm() {
} else if (input_id === '#project_id') {
icon = svg('octicon-project', 18, 'gt-mr-3');
} else if (input_id === '#assignee_id') {
- icon = `<img class="ui avatar image gt-mr-3" src=${$(this).data('avatar')}>`;
+ icon = `<img class="ui avatar image gt-mr-3" alt="avatar" src=${$(this).data('avatar')}>`;
}
$list.find('.selected').html(`
@@ -322,162 +314,148 @@ async function onEditContent(event) {
const $editContentZone = $segment.find('.edit-content-zone');
const $renderContent = $segment.find('.render-content');
const $rawContent = $segment.find('.raw-content');
- let $textarea;
- let easyMDE;
- // Setup new form
- if ($editContentZone.html().length === 0) {
- $editContentZone.html($('#edit-content-form').html());
- $textarea = $editContentZone.find('textarea');
- await attachTribute($textarea.get(), {mentions: true, emoji: true});
-
- let dz;
- const $dropzone = $editContentZone.find('.dropzone');
- if ($dropzone.length === 1) {
- $dropzone.data('saved', false);
-
- const fileUuidDict = {};
- dz = await createDropzone($dropzone[0], {
- url: $dropzone.data('upload-url'),
- headers: {'X-Csrf-Token': csrfToken},
- maxFiles: $dropzone.data('max-file'),
- maxFilesize: $dropzone.data('max-size'),
- acceptedFiles: (['*/*', ''].includes($dropzone.data('accepts'))) ? null : $dropzone.data('accepts'),
- addRemoveLinks: true,
- dictDefaultMessage: $dropzone.data('default-message'),
- dictInvalidFileType: $dropzone.data('invalid-input-type'),
- dictFileTooBig: $dropzone.data('file-too-big'),
- dictRemoveFile: $dropzone.data('remove-file'),
- timeout: 0,
- thumbnailMethod: 'contain',
- thumbnailWidth: 480,
- thumbnailHeight: 480,
- init() {
- this.on('success', (file, data) => {
- file.uuid = data.uuid;
- fileUuidDict[file.uuid] = {submitted: false};
- const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
- $dropzone.find('.files').append(input);
- });
- this.on('removedfile', (file) => {
- $(`#${file.uuid}`).remove();
- if ($dropzone.data('remove-url') && !fileUuidDict[file.uuid].submitted) {
- $.post($dropzone.data('remove-url'), {
- file: file.uuid,
- _csrf: csrfToken,
- });
- }
- });
- this.on('submit', () => {
- $.each(fileUuidDict, (fileUuid) => {
- fileUuidDict[fileUuid].submitted = true;
+ let comboMarkdownEditor;
+
+ const setupDropzone = async ($dropzone) => {
+ if ($dropzone.length === 0) return null;
+ $dropzone.data('saved', false);
+
+ const fileUuidDict = {};
+ const dz = await createDropzone($dropzone[0], {
+ url: $dropzone.data('upload-url'),
+ headers: {'X-Csrf-Token': csrfToken},
+ maxFiles: $dropzone.data('max-file'),
+ maxFilesize: $dropzone.data('max-size'),
+ acceptedFiles: (['*/*', ''].includes($dropzone.data('accepts'))) ? null : $dropzone.data('accepts'),
+ addRemoveLinks: true,
+ dictDefaultMessage: $dropzone.data('default-message'),
+ dictInvalidFileType: $dropzone.data('invalid-input-type'),
+ dictFileTooBig: $dropzone.data('file-too-big'),
+ dictRemoveFile: $dropzone.data('remove-file'),
+ timeout: 0,
+ thumbnailMethod: 'contain',
+ thumbnailWidth: 480,
+ thumbnailHeight: 480,
+ init() {
+ this.on('success', (file, data) => {
+ file.uuid = data.uuid;
+ fileUuidDict[file.uuid] = {submitted: false};
+ const input = $(`<input id="${data.uuid}" name="files" type="hidden">`).val(data.uuid);
+ $dropzone.find('.files').append(input);
+ });
+ this.on('removedfile', (file) => {
+ $(`#${file.uuid}`).remove();
+ if ($dropzone.data('remove-url') && !fileUuidDict[file.uuid].submitted) {
+ $.post($dropzone.data('remove-url'), {
+ file: file.uuid,
+ _csrf: csrfToken,
});
+ }
+ });
+ this.on('submit', () => {
+ $.each(fileUuidDict, (fileUuid) => {
+ fileUuidDict[fileUuid].submitted = true;
});
- this.on('reload', () => {
- $.getJSON($editContentZone.data('attachment-url'), (data) => {
- dz.removeAllFiles(true);
- $dropzone.find('.files').empty();
- $.each(data, function () {
- const imgSrc = `${$dropzone.data('link-url')}/${this.uuid}`;
- dz.emit('addedfile', this);
- dz.emit('thumbnail', this, imgSrc);
- dz.emit('complete', this);
- dz.files.push(this);
- fileUuidDict[this.uuid] = {submitted: true};
- $dropzone.find(`img[src='${imgSrc}']`).css('max-width', '100%');
- const input = $(`<input id="${this.uuid}" name="files" type="hidden">`).val(this.uuid);
- $dropzone.find('.files').append(input);
- });
+ });
+ this.on('reload', () => {
+ $.getJSON($editContentZone.data('attachment-url'), (data) => {
+ dz.removeAllFiles(true);
+ $dropzone.find('.files').empty();
+ $.each(data, function () {
+ const imgSrc = `${$dropzone.data('link-url')}/${this.uuid}`;
+ dz.emit('addedfile', this);
+ dz.emit('thumbnail', this, imgSrc);
+ dz.emit('complete', this);
+ dz.files.push(this);
+ fileUuidDict[this.uuid] = {submitted: true};
+ $dropzone.find(`img[src='${imgSrc}']`).css('max-width', '100%');
+ const input = $(`<input id="${this.uuid}" name="files" type="hidden">`).val(this.uuid);
+ $dropzone.find('.files').append(input);
});
});
- },
- });
+ });
+ },
+ });
+ dz.emit('reload');
+ return dz;
+ };
+
+ const cancelAndReset = (dz) => {
+ showElem($renderContent);
+ hideElem($editContentZone);
+ if (dz) {
dz.emit('reload');
}
- // Give new write/preview data-tab name to distinguish from others
- const $editContentForm = $editContentZone.find('.ui.comment.form');
- const $tabMenu = $editContentForm.find('.tabular.menu');
- $tabMenu.attr('data-write', $editContentZone.data('write'));
- $tabMenu.attr('data-preview', $editContentZone.data('preview'));
- $tabMenu.find('.write.item').attr('data-tab', $editContentZone.data('write'));
- $tabMenu.find('.preview.item').attr('data-tab', $editContentZone.data('preview'));
- $editContentForm.find('.write').attr('data-tab', $editContentZone.data('write'));
- $editContentForm.find('.preview').attr('data-tab', $editContentZone.data('preview'));
- easyMDE = await createCommentEasyMDE($textarea);
-
- initCompMarkupContentPreviewTab($editContentForm);
- initEasyMDEImagePaste(easyMDE, $dropzone);
-
- const $saveButton = $editContentZone.find('.save.button');
- $textarea.on('ce-quick-submit', () => {
- $saveButton.trigger('click');
- });
+ };
+
+ const saveAndRefresh = (dz, $dropzone) => {
+ showElem($renderContent);
+ hideElem($editContentZone);
+ const $attachments = $dropzone.find('.files').find('[name=files]').map(function () {
+ return $(this).val();
+ }).get();
+ $.post($editContentZone.data('update-url'), {
+ _csrf: csrfToken,
+ content: comboMarkdownEditor.value(),
+ context: $editContentZone.data('context'),
+ files: $attachments,
+ }, (data) => {
+ if (!data.content) {
+ $renderContent.html($('#no-content').html());
+ $rawContent.text('');
+ } else {
+ $renderContent.html(data.content);
+ $rawContent.text(comboMarkdownEditor.value());
- $editContentZone.find('.cancel.button').on('click', (e) => {
- e.preventDefault();
- showElem($renderContent);
- hideElem($editContentZone);
+ const refIssues = $renderContent.find('p .ref-issue');
+ attachRefIssueContextPopup(refIssues);
+ }
+ const $content = $segment;
+ if (!$content.find('.dropzone-attachments').length) {
+ if (data.attachments !== '') {
+ $content.append(`<div class="dropzone-attachments"></div>`);
+ $content.find('.dropzone-attachments').replaceWith(data.attachments);
+ }
+ } else if (data.attachments === '') {
+ $content.find('.dropzone-attachments').remove();
+ } else {
+ $content.find('.dropzone-attachments').replaceWith(data.attachments);
+ }
if (dz) {
+ dz.emit('submit');
dz.emit('reload');
}
+ initMarkupContent();
+ initCommentContent();
});
+ };
- $saveButton.on('click', () => {
- showElem($renderContent);
- hideElem($editContentZone);
- const $attachments = $dropzone.find('.files').find('[name=files]').map(function () {
- return $(this).val();
- }).get();
- $.post($editContentZone.data('update-url'), {
- _csrf: csrfToken,
- content: $textarea.val(),
- context: $editContentZone.data('context'),
- files: $attachments,
- }, (data) => {
- if (data.length === 0 || data.content.length === 0) {
- $renderContent.html($('#no-content').html());
- $rawContent.text('');
- } else {
- $renderContent.html(data.content);
- $rawContent.text($textarea.val());
- const refIssues = $renderContent.find('p .ref-issue');
- attachRefIssueContextPopup(refIssues);
- }
- const $content = $segment;
- if (!$content.find('.dropzone-attachments').length) {
- if (data.attachments !== '') {
- $content.append(`<div class="dropzone-attachments"></div>`);
- $content.find('.dropzone-attachments').replaceWith(data.attachments);
- }
- } else if (data.attachments === '') {
- $content.find('.dropzone-attachments').remove();
- } else {
- $content.find('.dropzone-attachments').replaceWith(data.attachments);
- }
- if (dz) {
- dz.emit('submit');
- dz.emit('reload');
- }
- initMarkupContent();
- initCommentContent();
- });
+ if (!$editContentZone.html()) {
+ $editContentZone.html($('#issue-comment-editor-template').html());
+ comboMarkdownEditor = await initComboMarkdownEditor($editContentZone.find('.combo-markdown-editor'));
+
+ const $dropzone = $editContentZone.find('.dropzone');
+ const dz = await setupDropzone($dropzone);
+ $editContentZone.find('.cancel.button').on('click', (e) => {
+ e.preventDefault();
+ cancelAndReset(dz);
+ });
+ $editContentZone.find('.save.button').on('click', (e) => {
+ e.preventDefault();
+ saveAndRefresh(dz, $dropzone);
});
- } else { // use existing form
- $textarea = $segment.find('textarea');
- easyMDE = getAttachedEasyMDE($textarea);
+ } else {
+ comboMarkdownEditor = getComboMarkdownEditor($editContentZone.find('.combo-markdown-editor'));
}
// Show write/preview tab and copy raw content as needed
showElem($editContentZone);
hideElem($renderContent);
- if ($textarea.val().length === 0) {
- $textarea.val($rawContent.text());
- easyMDE.value($rawContent.text());
+ if (!comboMarkdownEditor.value()) {
+ comboMarkdownEditor.value($rawContent.text());
}
- requestAnimationFrame(() => {
- $textarea.focus();
- easyMDE.codemirror.focus();
- });
+ comboMarkdownEditor.focus();
}
export function initRepository() {
@@ -575,7 +553,6 @@ export function initRepository() {
initRepoIssueCommentDelete();
initRepoIssueDependencyDelete();
initRepoIssueCodeCommentCancel();
- initRepoIssueStatusButton();
initRepoPullRequestUpdate();
initCompReactionSelector();
@@ -592,12 +569,6 @@ export function initRepository() {
const $form = $repoComparePull.find('.pullrequest-form');
showElem($form);
- $form.find('textarea.edit_area').each(function() {
- const easyMDE = getAttachedEasyMDE($(this));
- if (easyMDE) {
- easyMDE.codemirror.refresh();
- }
- });
});
}
@@ -614,24 +585,22 @@ function initRepoIssueCommentEdit() {
const target = $(this).data('target');
const quote = $(`#${target}`).text().replace(/\n/g, '\n> ');
const content = `> ${quote}\n\n`;
- let easyMDE;
+ let editor;
if ($(this).hasClass('quote-reply-diff')) {
const $replyBtn = $(this).closest('.comment-code-cloud').find('button.comment-form-reply');
- easyMDE = await handleReply($replyBtn);
+ editor = await handleReply($replyBtn);
} else {
// for normal issue/comment page
- easyMDE = getAttachedEasyMDE($('#comment-form .edit_area'));
+ editor = getComboMarkdownEditor($('#comment-form .combo-markdown-editor'));
}
- if (easyMDE) {
- if (easyMDE.value() !== '') {
- easyMDE.value(`${easyMDE.value()}\n\n${content}`);
+ if (editor) {
+ if (editor.value()) {
+ editor.value(`${editor.value()}\n\n${content}`);
} else {
- easyMDE.value(`${content}`);
+ editor.value(content);
}
- requestAnimationFrame(() => {
- easyMDE.codemirror.focus();
- easyMDE.codemirror.setCursor(easyMDE.codemirror.lineCount(), 0);
- });
+ editor.focus();
+ editor.moveCursorToEnd();
}
});
}
diff --git a/web_src/js/features/repo-release.js b/web_src/js/features/repo-release.js
index a230d7765e..5cc6f1e3cd 100644
--- a/web_src/js/features/repo-release.js
+++ b/web_src/js/features/repo-release.js
@@ -1,9 +1,6 @@
import $ from 'jquery';
-import {attachTribute} from './tribute.js';
-import {initCompMarkupContentPreviewTab} from './comp/MarkupContentPreview.js';
-import {initEasyMDEImagePaste} from './comp/ImagePaste.js';
-import {createCommentEasyMDE} from './comp/EasyMDE.js';
import {hideElem, showElem} from '../utils/dom.js';
+import {initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
export function initRepoRelease() {
$(document).on('click', '.remove-rel-attach', function() {
@@ -51,17 +48,9 @@ function initTagNameEditor() {
}
function initRepoReleaseEditor() {
- const $editor = $('.repository.new.release .content-editor');
+ const $editor = $('.repository.new.release .combo-markdown-editor');
if ($editor.length === 0) {
return;
}
-
- (async () => {
- const $textarea = $editor.find('textarea');
- await attachTribute($textarea.get(), {mentions: true, emoji: true});
- const easyMDE = await createCommentEasyMDE($textarea);
- initCompMarkupContentPreviewTab($editor);
- const $dropzone = $editor.parent().find('.dropzone');
- initEasyMDEImagePaste(easyMDE, $dropzone);
- })();
+ const _promise = initComboMarkdownEditor($editor);
}
diff --git a/web_src/js/features/repo-wiki.js b/web_src/js/features/repo-wiki.js
index 4555b32e5f..a48f63dcb1 100644
--- a/web_src/js/features/repo-wiki.js
+++ b/web_src/js/features/repo-wiki.js
@@ -1,194 +1,68 @@
import $ from 'jquery';
import {initMarkupContent} from '../markup/content.js';
-import {attachEasyMDEToElements, codeMirrorQuickSubmit, importEasyMDE, validateTextareaNonEmpty} from './comp/EasyMDE.js';
-import {initCompMarkupContentPreviewTab} from './comp/MarkupContentPreview.js';
+import {validateTextareaNonEmpty, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.js';
const {csrfToken} = window.config;
async function initRepoWikiFormEditor() {
- const $editArea = $('.repository.wiki textarea#edit_area');
+ const $editArea = $('.repository.wiki .combo-markdown-editor textarea');
if (!$editArea.length) return;
- let sideBySideChanges = 0;
- let sideBySideTimeout = null;
- let hasEasyMDE = true;
-
const $form = $('.repository.wiki.new .ui.form');
- const EasyMDE = await importEasyMDE();
- const easyMDE = new EasyMDE({
- autoDownloadFontAwesome: false,
- element: $editArea[0],
- forceSync: true,
- previewRender(plainText, preview) { // Async method
- // FIXME: still send render request when return back to edit mode
- const render = function () {
- sideBySideChanges = 0;
- if (sideBySideTimeout !== null) {
- clearTimeout(sideBySideTimeout);
- sideBySideTimeout = null;
- }
- $.post($editArea.data('url'), {
- _csrf: csrfToken,
- mode: 'gfm',
- context: $editArea.data('context'),
- text: plainText,
- wiki: true
- }, (data) => {
- preview.innerHTML = `<div class="markup ui segment">${data}</div>`;
- initMarkupContent();
- });
- };
+ const $editorContainer = $form.find('.combo-markdown-editor');
+ let editor;
- setTimeout(() => {
- if (!easyMDE.isSideBySideActive()) {
- render();
- } else {
- // delay preview by keystroke counting
- sideBySideChanges++;
- if (sideBySideChanges > 10) {
- render();
- }
- // or delay preview by timeout
- if (sideBySideTimeout !== null) {
- clearTimeout(sideBySideTimeout);
- sideBySideTimeout = null;
- }
- sideBySideTimeout = setTimeout(render, 600);
- }
- }, 0);
- if (!easyMDE.isSideBySideActive()) {
- return 'Loading...';
- }
- return preview.innerHTML;
- },
- renderingConfig: {
- singleLineBreaks: false
- },
- indentWithTabs: false,
- tabSize: 4,
- spellChecker: false,
- inputStyle: 'contenteditable', // nativeSpellcheck requires contenteditable
- nativeSpellcheck: true,
- toolbar: ['bold', 'italic', 'strikethrough', '|',
- 'heading-1', 'heading-2', 'heading-3', 'heading-bigger', 'heading-smaller', '|',
- {
- name: 'code-inline',
- action(e) {
- const cm = e.codemirror;
- const selection = cm.getSelection();
- cm.replaceSelection(`\`${selection}\``);
- if (!selection) {
- const cursorPos = cm.getCursor();
- cm.setCursor(cursorPos.line, cursorPos.ch - 1);
- }
- cm.focus();
- },
- className: 'fa fa-angle-right',
- title: 'Add Inline Code',
- }, 'code', 'quote', '|', {
- name: 'checkbox-empty',
- action(e) {
- const cm = e.codemirror;
- cm.replaceSelection(`\n- [ ] ${cm.getSelection()}`);
- cm.focus();
- },
- className: 'fa fa-square-o',
- title: 'Add Checkbox (empty)',
- },
- {
- name: 'checkbox-checked',
- action(e) {
- const cm = e.codemirror;
- cm.replaceSelection(`\n- [x] ${cm.getSelection()}`);
- cm.focus();
- },
- className: 'fa fa-check-square-o',
- title: 'Add Checkbox (checked)',
- }, '|',
- 'unordered-list', 'ordered-list', '|',
- 'link', 'image', 'table', 'horizontal-rule', '|',
- 'clean-block', 'preview', 'fullscreen', 'side-by-side', '|',
- {
- name: 'revert-to-textarea',
- action(e) {
- e.toTextArea();
- hasEasyMDE = false;
- const $root = $form.find('.field.content');
- const loading = $root.data('loading');
- $root.append(`<div class="ui bottom tab markup" data-tab="preview">${loading}</div>`);
- initCompMarkupContentPreviewTab($form);
- },
- className: 'fa fa-file',
- title: 'Revert to simple textarea',
- },
- ]
- });
+ let renderRequesting = false;
+ let lastContent;
+ const renderEasyMDEPreview = function () {
+ if (renderRequesting) return;
- easyMDE.codemirror.setOption('extraKeys', {
- 'Cmd-Enter': codeMirrorQuickSubmit,
- 'Ctrl-Enter': codeMirrorQuickSubmit,
- });
+ const $previewFull = $editorContainer.find('.EasyMDEContainer .editor-preview-active');
+ const $previewSide = $editorContainer.find('.EasyMDEContainer .editor-preview-active-side');
+ const $previewTarget = $previewSide.length ? $previewSide : $previewFull;
+ const newContent = $editArea.val();
+ if (editor && $previewTarget.length && lastContent !== newContent) {
+ renderRequesting = true;
+ $.post(editor.previewUrl, {
+ _csrf: csrfToken,
+ mode: editor.previewMode,
+ context: editor.previewContext,
+ text: newContent,
+ wiki: editor.previewWiki,
+ }).done((data) => {
+ lastContent = newContent;
+ $previewTarget.html(`<div class="markup ui segment">${data}</div>`);
+ initMarkupContent();
+ }).always(() => {
+ renderRequesting = false;
+ setTimeout(renderEasyMDEPreview, 1000);
+ });
+ } else {
+ setTimeout(renderEasyMDEPreview, 1000);
+ }
+ };
+ renderEasyMDEPreview();
- attachEasyMDEToElements(easyMDE);
+ editor = await initComboMarkdownEditor($editorContainer, {
+ previewMode: 'gfm',
+ previewWiki: true,
+ easyMDEOptions: {
+ previewRender: (_content, previewTarget) => previewTarget.innerHTML, // disable builtin preview render
+ toolbar: ['bold', 'italic', 'strikethrough', '|',
+ 'heading-1', 'heading-2', 'heading-3', 'heading-bigger', 'heading-smaller', '|',
+ 'gitea-code-inline', 'code', 'quote', '|', 'gitea-checkbox-empty', 'gitea-checkbox-checked', '|',
+ 'unordered-list', 'ordered-list', '|',
+ 'link', 'image', 'table', 'horizontal-rule', '|',
+ 'clean-block', 'preview', 'fullscreen', 'side-by-side', '|', 'gitea-switch-to-textarea'
+ ],
+ },
+ });
$form.on('submit', () => {
if (!validateTextareaNonEmpty($editArea)) {
return false;
}
});
-
- setTimeout(() => {
- const $bEdit = $('.repository.wiki.new .previewtabs a[data-tab="write"]');
- const $bPrev = $('.repository.wiki.new .previewtabs a[data-tab="preview"]');
- const $toolbar = $('.editor-toolbar');
- const $bPreview = $('.editor-toolbar button.preview');
- const $bSideBySide = $('.editor-toolbar a.fa-columns');
- $bEdit.on('click', (e) => {
- if (!hasEasyMDE) {
- return false;
- }
- e.stopImmediatePropagation();
- if ($toolbar.hasClass('disabled-for-preview')) {
- $bPreview.trigger('click');
- }
-
- return false;
- });
- $bPrev.on('click', (e) => {
- if (!hasEasyMDE) {
- return false;
- }
- e.stopImmediatePropagation();
- if (!$toolbar.hasClass('disabled-for-preview')) {
- $bPreview.trigger('click');
- }
- return false;
- });
- $bPreview.on('click', () => {
- setTimeout(() => {
- if ($toolbar.hasClass('disabled-for-preview')) {
- if ($bEdit.hasClass('active')) {
- $bEdit.removeClass('active');
- }
- if (!$bPrev.hasClass('active')) {
- $bPrev.addClass('active');
- }
- } else {
- if (!$bEdit.hasClass('active')) {
- $bEdit.addClass('active');
- }
- if ($bPrev.hasClass('active')) {
- $bPrev.removeClass('active');
- }
- }
- }, 0);
-
- return false;
- });
- $bSideBySide.on('click', () => {
- sideBySideChanges = 10;
- });
- }, 0);
}
export function initRepoWikiForm() {
diff --git a/web_src/js/features/tribute.js b/web_src/js/features/tribute.js
index 94f3512a2e..e77ba29950 100644
--- a/web_src/js/features/tribute.js
+++ b/web_src/js/features/tribute.js
@@ -1,11 +1,10 @@
import {emojiKeys, emojiHTML, emojiString} from './emoji.js';
-import {uniq} from '../utils.js';
import {htmlEscape} from 'escape-goat';
function makeCollections({mentions, emoji}) {
const collections = [];
- if (mentions) {
+ if (emoji) {
collections.push({
trigger: ':',
requireLeadingSpace: true,
@@ -30,14 +29,14 @@ function makeCollections({mentions, emoji}) {
});
}
- if (emoji) {
+ if (mentions) {
collections.push({
values: window.config.tributeValues,
requireLeadingSpace: true,
menuItemTemplate: (item) => {
return `
<div class="tribute-item">
- <img src="${htmlEscape(item.original.avatar)}"/>
+ <img src="${htmlEscape(item.original.avatar)}" class="gt-mr-3"/>
<span class="name">${htmlEscape(item.original.name)}</span>
${item.original.fullname && item.original.fullname !== '' ? `<span class="fullname">${htmlEscape(item.original.fullname)}</span>` : ''}
</div>
@@ -49,30 +48,10 @@ function makeCollections({mentions, emoji}) {
return collections;
}
-export async function attachTribute(elementOrNodeList, {mentions, emoji} = {}) {
- if (!window.config.requireTribute || !elementOrNodeList) return;
- const nodes = Array.from('length' in elementOrNodeList ? elementOrNodeList : [elementOrNodeList]);
- if (!nodes.length) return;
-
- const mentionNodes = nodes.filter((node) => {
- return mentions || node.id === 'content';
- });
- const emojiNodes = nodes.filter((node) => {
- return emoji || node.id === 'content' || node.classList.contains('emoji-input');
- });
- const uniqueNodes = uniq([...mentionNodes, ...emojiNodes]);
- if (!uniqueNodes.length) return;
-
+export async function attachTribute(element, {mentions, emoji} = {}) {
const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs');
-
- const collections = makeCollections({
- mentions: mentions || mentionNodes.length > 0,
- emoji: emoji || emojiNodes.length > 0,
- });
-
+ const collections = makeCollections({mentions, emoji});
const tribute = new Tribute({collection: collections, noMatchTemplate: ''});
- for (const node of uniqueNodes) {
- tribute.attach(node);
- }
+ tribute.attach(element);
return tribute;
}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 839289e9d2..e727acfa06 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -4,7 +4,6 @@ import './bootstrap.js';
import {initRepoActivityTopAuthorsChart} from './components/RepoActivityTopAuthors.vue';
import {initDashboardRepoList} from './components/DashboardRepoList.vue';
-import {attachTribute} from './features/tribute.js';
import {initGlobalCopyToClipboardListener} from './features/clipboard.js';
import {initContextPopups} from './features/contextpopup.js';
import {initRepoGraphGit} from './features/repo-graph.js';
@@ -110,8 +109,6 @@ onDomReady(() => {
initGlobalFormDirtyLeaveConfirm();
initGlobalLinkActions();
- attachTribute(document.querySelectorAll('#content, .emoji-input'));
-
initCommonIssue();
initCommonOrganization();
diff --git a/web_src/js/utils.js b/web_src/js/utils.js
index b3ffbf2988..e72e55dc65 100644
--- a/web_src/js/utils.js
+++ b/web_src/js/utils.js
@@ -30,11 +30,6 @@ export function isDarkTheme() {
return style.getPropertyValue('--is-dark-theme').trim().toLowerCase() === 'true';
}
-// removes duplicate elements in an array
-export function uniq(arr) {
- return Array.from(new Set(arr));
-}
-
// strip <tags> from a string
export function stripTags(text) {
return text.replace(/<[^>]*>?/gm, '');
diff --git a/web_src/js/utils.test.js b/web_src/js/utils.test.js
index 306acd34af..46fbb28de4 100644
--- a/web_src/js/utils.test.js
+++ b/web_src/js/utils.test.js
@@ -1,6 +1,6 @@
import {expect, test} from 'vitest';
import {
- basename, extname, isObject, uniq, stripTags, joinPaths, parseIssueHref,
+ basename, extname, isObject, stripTags, joinPaths, parseIssueHref,
prettyNumber, parseUrl, translateMonth, translateDay, blobToDataURI,
toAbsoluteUrl,
} from './utils.js';
@@ -62,10 +62,6 @@ test('isObject', () => {
expect(isObject([])).toBeFalsy();
});
-test('uniq', () => {
- expect(uniq([1, 1, 1, 2])).toEqual([1, 2]);
-});
-
test('stripTags', () => {
expect(stripTags('<a>test</a>')).toEqual('test');
});