Introduce GitHub markdown editor, keep EasyMDE as fallback (#23876)
The first step of the plan * #23290 Thanks to @silverwind for the first try in #15394 . Close #10729 and a lot of related issues. The EasyMDE is not removed, now it works as a fallback, users can switch between these two editors. Editor list: * Issue / PR comment * Issue / PR comment edit * Issue / PR comment quote reply * PR diff view, inline comment * PR diff view, inline comment edit * PR diff view, inline comment quote reply * Release editor * Wiki editor Some editors have attached dropzone Screenshots: <details> ![image](https://user-images.githubusercontent.com/2114189/229363558-7e44dcd4-fb6d-48a0-92f8-bd12f57bb0a0.png) ![image](https://user-images.githubusercontent.com/2114189/229363566-781489c8-5306-4347-9714-d71af5d5b0b1.png) ![image](https://user-images.githubusercontent.com/2114189/229363771-1717bf5c-0f2a-4fc2-ba84-4f5b2a343a11.png) ![image](https://user-images.githubusercontent.com/2114189/229363793-ad362d0f-a045-47bd-8f9d-05a9a842bb39.png) </details> --------- Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
parent
d67e40684f
commit
5cc0801de9
6
package-lock.json
generated
6
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
35
routers/web/devtest/devtest.go
Normal file
35
routers/web/devtest/devtest.go
Normal file
|
@ -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"))))
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}},
|
||||
|
|
12
templates/devtest/gitea-ui.tmpl
Normal file
12
templates/devtest/gitea-ui.tmpl
Normal file
|
@ -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" .}}
|
5
templates/devtest/list.tmpl
Normal file
5
templates/devtest/list.tmpl
Normal file
|
@ -0,0 +1,5 @@
|
|||
<ul>
|
||||
{{range .SubNames}}
|
||||
<li><a href="{{AppSubUrl}}/devtest/{{.}}">{{.}}</a></li>
|
||||
{{end}}
|
||||
</ul>
|
|
@ -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" .}}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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" .}}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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" .}}
|
||||
|
||||
|
|
|
@ -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}}">
|
||||
|
|
|
@ -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>
|
||||
|
|
47
templates/shared/combomarkdowneditor.tmpl
Normal file
47
templates/shared/combomarkdowneditor.tmpl
Normal file
|
@ -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>
|
25
web_src/css/editor-markdown.css
Normal file
25
web_src/css/editor-markdown.css
Normal file
|
@ -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;
|
||||
}
|
|
@ -13,7 +13,6 @@
|
|||
}
|
||||
|
||||
.editor-toolbar {
|
||||
max-width: calc(100vw - 80px);
|
||||
border-color: var(--color-secondary);
|
||||
}
|
||||
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -2116,10 +2116,6 @@
|
|||
height: 48px;
|
||||
}
|
||||
|
||||
.repository.wiki.new .ui.attached.tabular.menu.previewtabs {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.repository.wiki.view > .markup {
|
||||
padding: 15px 30px;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
277
web_src/js/features/comp/ComboMarkdownEditor.js
Normal file
277
web_src/js/features/comp/ComboMarkdownEditor.js
Normal file
|
@ -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;
|
||||
}
|
|
@ -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());
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
|
||||
$(easyMDE.element).on('paste', async (e) => {
|
||||
return uploadClipboardImage(new TextareaEditor(easyMDE.element), e.originalEvent);
|
||||
return uploadClipboardImage(new CodeMirrorEditor(easyMDE.codemirror), dropzone, e);
|
||||
});
|
||||
}
|
||||
|
||||
export function initTextareaImagePaste(textarea, dropzone) {
|
||||
if (!dropzone) return;
|
||||
$(textarea).on('paste', async (e) => {
|
||||
return uploadClipboardImage(new TextareaEditor(textarea), dropzone, e.originalEvent);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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}}));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 comboMarkdownEditor;
|
||||
|
||||
let dz;
|
||||
const $dropzone = $editContentZone.find('.dropzone');
|
||||
if ($dropzone.length === 1) {
|
||||
$dropzone.data('saved', false);
|
||||
const setupDropzone = async ($dropzone) => {
|
||||
if ($dropzone.length === 0) return null;
|
||||
$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);
|
||||
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('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 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());
|
||||
|
||||
const $saveButton = $editContentZone.find('.save.button');
|
||||
$textarea.on('ce-quick-submit', () => {
|
||||
$saveButton.trigger('click');
|
||||
});
|
||||
|
||||
$editContentZone.find('.cancel.button').on('click', (e) => {
|
||||
e.preventDefault();
|
||||
showElem($renderContent);
|
||||
hideElem($editContentZone);
|
||||
if (dz) {
|
||||
dz.emit('reload');
|
||||
const refIssues = $renderContent.find('p .ref-issue');
|
||||
attachRefIssueContextPopup(refIssues);
|
||||
}
|
||||
});
|
||||
|
||||
$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 {
|
||||
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);
|
||||
}
|
||||
if (dz) {
|
||||
dz.emit('submit');
|
||||
dz.emit('reload');
|
||||
}
|
||||
initMarkupContent();
|
||||
initCommentContent();
|
||||
});
|
||||
} 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();
|
||||
});
|
||||
} else { // use existing form
|
||||
$textarea = $segment.find('textarea');
|
||||
easyMDE = getAttachedEasyMDE($textarea);
|
||||
};
|
||||
|
||||
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 {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
let renderRequesting = false;
|
||||
let lastContent;
|
||||
const renderEasyMDEPreview = function () {
|
||||
if (renderRequesting) return;
|
||||
|
||||
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();
|
||||
|
||||
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'
|
||||
],
|
||||
},
|
||||
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',
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
easyMDE.codemirror.setOption('extraKeys', {
|
||||
'Cmd-Enter': codeMirrorQuickSubmit,
|
||||
'Ctrl-Enter': codeMirrorQuickSubmit,
|
||||
});
|
||||
|
||||
attachEasyMDEToElements(easyMDE);
|
||||
|
||||
$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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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, '');
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user