Refactor markdown editor and use it for milestone description editor (#32688)

Refactor markdown editor to clarify its "preview" behavior and remove
jQuery code.

Close #15045

---------

Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
wxiaoguang 2024-12-04 10:11:34 +08:00 committed by GitHub
parent 2f43536c3e
commit c9e582c6b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 147 additions and 116 deletions

View File

@ -9,6 +9,7 @@ import (
"reflect" "reflect"
"strings" "strings"
"code.gitea.io/gitea/modules/htmlutil"
"code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
@ -214,7 +215,9 @@ func (r *Router) normalizeRequestPath(resp http.ResponseWriter, req *http.Reques
normalizedPath = "/" normalizedPath = "/"
} else if !strings.HasPrefix(normalizedPath+"/", "/v2/") { } else if !strings.HasPrefix(normalizedPath+"/", "/v2/") {
// do not respond to other requests, to simulate a real sub-path environment // do not respond to other requests, to simulate a real sub-path environment
http.Error(resp, "404 page not found, sub-path is: "+setting.AppSubURL, http.StatusNotFound) resp.Header().Add("Content-Type", "text/html; charset=utf-8")
resp.WriteHeader(http.StatusNotFound)
_, _ = resp.Write([]byte(htmlutil.HTMLFormat(`404 page not found, sub-path is: <a href="%s">%s</a>`, setting.AppSubURL, setting.AppSubURL)))
return return
} }
normalized = true normalized = true

View File

@ -2590,7 +2590,6 @@ diff.generated = generated
diff.vendored = vendored diff.vendored = vendored
diff.comment.add_line_comment = Add line comment diff.comment.add_line_comment = Add line comment
diff.comment.placeholder = Leave a comment diff.comment.placeholder = Leave a comment
diff.comment.markdown_info = Styling with markdown is supported.
diff.comment.add_single_comment = Add single comment diff.comment.add_single_comment = Add single comment
diff.comment.add_review_comment = Add comment diff.comment.add_review_comment = Add comment
diff.comment.start_review = Start review diff.comment.start_review = Start review

View File

@ -485,6 +485,8 @@ func registerRoutes(m *web.Router) {
m.Methods("GET, HEAD", "/*", public.FileHandlerFunc()) m.Methods("GET, HEAD", "/*", public.FileHandlerFunc())
}, optionsCorsHandler()) }, optionsCorsHandler())
m.Post("/-/markup", reqSignIn, web.Bind(structs.MarkupOption{}), misc.Markup)
m.Group("/explore", func() { m.Group("/explore", func() {
m.Get("", func(ctx *context.Context) { m.Get("", func(ctx *context.Context) {
ctx.Redirect(setting.AppSubURL + "/explore/repos") ctx.Redirect(setting.AppSubURL + "/explore/repos")

View File

@ -1,3 +1,3 @@
{{/* TODO: the devtest.js is isolated from index.js, so no module is shared and many index.js functions do not work in devtest.ts */}} {{/* TODO: the devtest.js is isolated from index.js, so no module is shared and many index.js functions do not work in devtest.ts */}}
<script src="{{AssetUrlPrefix}}/js/devtest.js?v={{AssetVersion}}"></script> <script src="{{AssetUrlPrefix}}/js/devtest.js?v={{AssetVersion}}"></script>
{{template "base/footer" dict}} {{template "base/footer" ctx.RootData}}

View File

@ -1,2 +1,2 @@
{{template "base/head" dict}} {{template "base/head" ctx.RootData}}
<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/devtest.css?v={{AssetVersion}}"> <link rel="stylesheet" href="{{AssetUrlPrefix}}/css/devtest.css?v={{AssetVersion}}">

View File

@ -183,8 +183,7 @@
<div> <div>
<h1>ComboMarkdownEditor</h1> <h1>ComboMarkdownEditor</h1>
<div>ps: no JS code attached, so just a layout</div> {{template "shared/combomarkdowneditor" dict "MarkdownPreviewContext" "/owner/path"}}
{{template "shared/combomarkdowneditor" .}}
</div> </div>
<h1>Tailwind CSS Demo</h1> <h1>Tailwind CSS Demo</h1>

View File

@ -23,6 +23,7 @@
<input id="email" name="email" type="email" value="{{.Org.Email}}" maxlength="255"> <input id="email" name="email" type="email" value="{{.Org.Email}}" maxlength="255">
</div> </div>
<div class="field {{if .Err_Description}}error{{end}}"> <div class="field {{if .Err_Description}}error{{end}}">
{{/* it is rendered as markdown, but the length is limited, so at the moment we do not use the markdown editor here */}}
<label for="description">{{ctx.Locale.Tr "org.org_desc"}}</label> <label for="description">{{ctx.Locale.Tr "org.org_desc"}}</label>
<textarea id="description" name="description" rows="2" maxlength="255">{{.Org.Description}}</textarea> <textarea id="description" name="description" rows="2" maxlength="255">{{.Org.Description}}</textarea>
</div> </div>

View File

@ -18,7 +18,16 @@
</div> </div>
<div class="field"> <div class="field">
<label>{{ctx.Locale.Tr "repo.projects.description"}}</label> <label>{{ctx.Locale.Tr "repo.projects.description"}}</label>
<textarea name="content" placeholder="{{ctx.Locale.Tr "repo.projects.description_placeholder"}}">{{.content}}</textarea> {{/* TODO: repo-level project and org-level project have different behaviros to render */}}
{{/* the "Repository" is nil when the project is org-level */}}
{{template "shared/combomarkdowneditor" (dict
"MarkdownPreviewInRepo" $.Repository
"MarkdownPreviewContext" (Iif $.Repository "" .HomeLink)
"MarkdownPreviewMode" (Iif $.Repository "comment")
"TextareaName" "content"
"TextareaContent" .content
"TextareaPlaceholder" (ctx.Locale.Tr "repo.projects.description_placeholder")
)}}
</div> </div>
{{if not .PageIsEditProjects}} {{if not .PageIsEditProjects}}

View File

@ -240,8 +240,9 @@
<template id="issue-comment-editor-template"> <template id="issue-comment-editor-template">
<div class="ui form comment"> <div class="ui form comment">
{{template "shared/combomarkdowneditor" (dict {{template "shared/combomarkdowneditor" (dict
"MarkdownPreviewUrl" (print $.Repository.Link "/markup") "CustomInit" true
"MarkdownPreviewContext" $.RepoLink "MarkdownPreviewInRepo" $.Repository
"MarkdownPreviewMode" "comment"
"TextareaName" "content" "TextareaName" "content"
"DropzoneParentContainer" ".ui.form" "DropzoneParentContainer" ".ui.form"
)}} )}}

View File

@ -9,24 +9,24 @@
<input type="hidden" name="diff_start_cid"> <input type="hidden" name="diff_start_cid">
<input type="hidden" name="diff_end_cid"> <input type="hidden" name="diff_end_cid">
<input type="hidden" name="diff_base_cid"> <input type="hidden" name="diff_base_cid">
<div class="field">
{{template "shared/combomarkdowneditor" (dict {{template "shared/combomarkdowneditor" (dict
"MarkdownPreviewUrl" (print $.root.Repository.Link "/markup") "CustomInit" true
"MarkdownPreviewContext" $.root.RepoLink "MarkdownPreviewInRepo" $.root.Repository
"MarkdownPreviewMode" "comment"
"TextareaName" "content" "TextareaName" "content"
"TextareaPlaceholder" (ctx.Locale.Tr "repo.diff.comment.placeholder") "TextareaPlaceholder" (ctx.Locale.Tr "repo.diff.comment.placeholder")
"DropzoneParentContainer" "form" "DropzoneParentContainer" "form"
"DisableAutosize" "true" "DisableAutosize" "true"
)}} )}}
</div>
{{if $.root.IsAttachmentEnabled}} {{if $.root.IsAttachmentEnabled}}
<div class="field"> <div class="field">
{{template "repo/upload" $.root}} {{template "repo/upload" $.root}}
</div> </div>
{{end}} {{end}}
<div class="field footer tw-mx-2"> <div class="field footer">
<span class="markup-info">{{svg "octicon-markdown"}} {{ctx.Locale.Tr "repo.diff.comment.markdown_info"}}</span>
<div class="tw-text-right"> <div class="tw-text-right">
{{if $.reply}} {{if $.reply}}
<button class="ui submit primary tiny button btn-reply" type="submit">{{ctx.Locale.Tr "repo.diff.comment.reply"}}</button> <button class="ui submit primary tiny button btn-reply" type="submit">{{ctx.Locale.Tr "repo.diff.comment.reply"}}</button>

View File

@ -16,8 +16,8 @@
</div> </div>
<div class="field"> <div class="field">
{{template "shared/combomarkdowneditor" (dict {{template "shared/combomarkdowneditor" (dict
"MarkdownPreviewUrl" (print .Repository.Link "/markup") "MarkdownPreviewInRepo" $.Repository
"MarkdownPreviewContext" .RepoLink "MarkdownPreviewMode" "comment"
"TextareaName" "content" "TextareaName" "content"
"TextareaPlaceholder" (ctx.Locale.Tr "repo.diff.review.placeholder") "TextareaPlaceholder" (ctx.Locale.Tr "repo.diff.review.placeholder")
"DropzoneParentContainer" "form" "DropzoneParentContainer" "form"

View File

@ -5,8 +5,9 @@
<div class="field"> <div class="field">
{{template "shared/combomarkdowneditor" (dict {{template "shared/combomarkdowneditor" (dict
"MarkdownPreviewUrl" (print .Repository.Link "/markup") "CustomInit" true
"MarkdownPreviewContext" .RepoLink "MarkdownPreviewInRepo" $.Repository
"MarkdownPreviewMode" "comment"
"TextareaName" "content" "TextareaName" "content"
"TextareaContent" $textareaContent "TextareaContent" $textareaContent
"TextareaPlaceholder" (ctx.Locale.Tr "repo.diff.comment.placeholder") "TextareaPlaceholder" (ctx.Locale.Tr "repo.diff.comment.placeholder")

View File

@ -7,9 +7,10 @@
{{if $useMarkdownEditor}} {{if $useMarkdownEditor}}
{{template "shared/combomarkdowneditor" (dict {{template "shared/combomarkdowneditor" (dict
"CustomInit" true
"ContainerClasses" "tw-hidden" "ContainerClasses" "tw-hidden"
"MarkdownPreviewUrl" (print .root.RepoLink "/markup") "MarkdownPreviewInRepo" $.root.Repository
"MarkdownPreviewContext" .root.RepoLink "MarkdownPreviewMode" "comment"
"TextareaContent" .item.Attributes.value "TextareaContent" .item.Attributes.value
"TextareaPlaceholder" .item.Attributes.placeholder "TextareaPlaceholder" .item.Attributes.placeholder
"DropzoneParentContainer" ".combo-editor-dropzone" "DropzoneParentContainer" ".combo-editor-dropzone"

View File

@ -36,9 +36,14 @@
</div> </div>
<div class="field"> <div class="field">
<label>{{ctx.Locale.Tr "repo.milestones.desc"}}</label> <label>{{ctx.Locale.Tr "repo.milestones.desc"}}</label>
<textarea name="content">{{.content}}</textarea> {{template "shared/combomarkdowneditor" (dict
"MarkdownPreviewInRepo" $.Repository
"MarkdownPreviewMode" "comment"
"TextareaName" "content"
"TextareaContent" .content
"TextareaPlaceholder" (ctx.Locale.Tr "repo.milestones.desc")
)}}
</div> </div>
<div class="divider"></div>
<div class="tw-text-right"> <div class="tw-text-right">
{{if .PageIsEditMilestone}} {{if .PageIsEditMilestone}}
<a class="ui primary basic button" href="{{.RepoLink}}/milestones"> <a class="ui primary basic button" href="{{.RepoLink}}/milestones">

View File

@ -142,8 +142,9 @@
<div class="ui form comment"> <div class="ui form comment">
<div class="field"> <div class="field">
{{template "shared/combomarkdowneditor" (dict {{template "shared/combomarkdowneditor" (dict
"MarkdownPreviewUrl" (print .Repository.Link "/markup") "CustomInit" true
"MarkdownPreviewContext" .RepoLink "MarkdownPreviewInRepo" $.Repository
"MarkdownPreviewMode" "comment"
"TextareaName" "content" "TextareaName" "content"
"DropzoneParentContainer" ".ui.form" "DropzoneParentContainer" ".ui.form"
)}} )}}

View File

@ -50,12 +50,11 @@
</div> </div>
<div class="field"> <div class="field">
{{template "shared/combomarkdowneditor" (dict {{template "shared/combomarkdowneditor" (dict
"MarkdownPreviewUrl" (print .Repository.Link "/markup") "MarkdownPreviewInRepo" $.Repository
"MarkdownPreviewContext" .RepoLink "MarkdownPreviewMode" "comment"
"TextareaName" "content" "TextareaName" "content"
"TextareaContent" .content "TextareaContent" .content
"TextareaPlaceholder" (ctx.Locale.Tr "repo.release.message") "TextareaPlaceholder" (ctx.Locale.Tr "repo.release.message")
"TextareaAriaLabel" (ctx.Locale.Tr "repo.release.message")
"DropzoneParentContainer" "form" "DropzoneParentContainer" "form"
)}} )}}
</div> </div>

View File

@ -23,12 +23,12 @@
{{$content = ctx.Locale.Tr "repo.wiki.welcome"}} {{$content = ctx.Locale.Tr "repo.wiki.welcome"}}
{{end}} {{end}}
{{template "shared/combomarkdowneditor" (dict {{template "shared/combomarkdowneditor" (dict
"MarkdownPreviewUrl" (print .Repository.Link "/markup") "CustomInit" true
"MarkdownPreviewContext" .RepoLink "MarkdownPreviewInRepo" $.Repository
"MarkdownPreviewMode" "wiki"
"TextareaName" "content" "TextareaName" "content"
"TextareaPlaceholder" (ctx.Locale.Tr "repo.wiki.page_content")
"TextareaAriaLabel" (ctx.Locale.Tr "repo.wiki.page_content")
"TextareaContent" $content "TextareaContent" $content
"TextareaPlaceholder" (ctx.Locale.Tr "repo.wiki.page_content")
)}} )}}
<div class="field tw-mt-4"> <div class="field tw-mt-4">

View File

@ -1,23 +1,39 @@
{{/* {{/*
Template Attributes: Template Attributes:
* CustomInit: do not initialize the editor automatically
* ContainerId: id attribute for the container element * ContainerId: id attribute for the container element
* ContainerClasses: additional classes for the container element * ContainerClasses: additional classes for the container element
* MarkdownPreviewUrl: preview url for the preview tab * MarkdownPreviewInRepo: the repo to preview markdown
* MarkdownPreviewContext: preview context for the preview tab * MarkdownPreviewContext: preview context (the related url path when rendering) for the preview tab, eg: repo link or user home link
* MarkdownPreviewMode: content mode for the editor, eg: wiki, comment or default
* TextareaName: name attribute for the textarea * TextareaName: name attribute for the textarea
* TextareaContent: content for the textarea * TextareaContent: content for the textarea
* TextareaMaxLength: maxlength attribute for the textarea
* TextareaPlaceholder: placeholder attribute for the textarea * TextareaPlaceholder: placeholder attribute for the textarea
* TextareaAriaLabel: aria-label attribute for the textarea * TextareaAriaLabel: aria-label attribute for the textarea
* DropzoneParentContainer: container for file upload (leave it empty if no upload) * DropzoneParentContainer: container for file upload (leave it empty if no upload)
* DisableAutosize: whether to disable automatic height resizing * DisableAutosize: whether to disable automatic height resizing
*/}} */}}
<div {{if .ContainerId}}id="{{.ContainerId}}"{{end}} class="combo-markdown-editor {{.ContainerClasses}}" data-dropzone-parent-container="{{.DropzoneParentContainer}}"> {{$ariaLabel := or .TextareaAriaLabel .TextareaPlaceholder}}
{{if .MarkdownPreviewUrl}} {{$repo := .MarkdownPreviewInRepo}}
{{$previewContext := .MarkdownPreviewContext}}
{{$previewMode := .MarkdownPreviewMode}}
{{$previewUrl := print AppSubUrl "/-/markup"}}
{{if $repo}}
{{$previewUrl = print $repo.Link "/markup"}}
{{end}}
{{$supportEasyMDE := or (eq $previewMode "comment") (eq $previewMode "wiki")}}
<div {{if .ContainerId}}id="{{.ContainerId}}"{{end}} class="combo-markdown-editor {{if .CustomInit}}custom-init{{end}} {{.ContainerClasses}}"
data-dropzone-parent-container="{{.DropzoneParentContainer}}"
data-content-mode="{{$previewMode}}"
data-support-easy-mde="{{$supportEasyMDE}}"
data-preview-url="{{$previewUrl}}"
data-preview-context="{{$previewContext}}"
>
<div class="ui top tabular menu"> <div class="ui top tabular menu">
<a class="active item" data-tab-for="markdown-writer">{{template "shared/misc/tabtitle" (ctx.Locale.Tr "write")}}</a> <a class="active item" data-tab-for="markdown-writer">{{template "shared/misc/tabtitle" (ctx.Locale.Tr "write")}}</a>
<a class="item" data-tab-for="markdown-previewer" data-preview-url="{{.MarkdownPreviewUrl}}" data-preview-context="{{.MarkdownPreviewContext}}">{{template "shared/misc/tabtitle" (ctx.Locale.Tr "preview")}}</a> <a class="item" data-tab-for="markdown-previewer">{{template "shared/misc/tabtitle" (ctx.Locale.Tr "preview")}}</a>
</div> </div>
{{end}}
<div class="ui tab active" data-tab-panel="markdown-writer"> <div class="ui tab active" data-tab-panel="markdown-writer">
<markdown-toolbar> <markdown-toolbar>
<div class="markdown-toolbar-group"> <div class="markdown-toolbar-group">
@ -40,17 +56,25 @@ Template Attributes:
<md-task-list class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.list.task.tooltip"}}">{{svg "octicon-tasklist"}}</md-task-list> <md-task-list class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.list.task.tooltip"}}">{{svg "octicon-tasklist"}}</md-task-list>
<button class="markdown-toolbar-button markdown-button-table-add" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.table.add.tooltip"}}">{{svg "octicon-table"}}</button> <button class="markdown-toolbar-button markdown-button-table-add" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.table.add.tooltip"}}">{{svg "octicon-table"}}</button>
</div> </div>
{{if eq $previewMode "comment"}}
<div class="markdown-toolbar-group"> <div class="markdown-toolbar-group">
<md-mention class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.mention.tooltip"}}">{{svg "octicon-mention"}}</md-mention> <md-mention class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.mention.tooltip"}}">{{svg "octicon-mention"}}</md-mention>
<md-ref class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.ref.tooltip"}}">{{svg "octicon-cross-reference"}}</md-ref> <md-ref class="markdown-toolbar-button" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.ref.tooltip"}}">{{svg "octicon-cross-reference"}}</md-ref>
</div> </div>
{{end}}
<div class="markdown-toolbar-group"> <div class="markdown-toolbar-group">
<button class="markdown-toolbar-button markdown-switch-monospace" role="switch" data-enable-text="{{ctx.Locale.Tr "editor.buttons.enable_monospace_font"}}" data-disable-text="{{ctx.Locale.Tr "editor.buttons.disable_monospace_font"}}">{{svg "octicon-typography"}}</button> <button class="markdown-toolbar-button markdown-switch-monospace" role="switch" data-enable-text="{{ctx.Locale.Tr "editor.buttons.enable_monospace_font"}}" data-disable-text="{{ctx.Locale.Tr "editor.buttons.disable_monospace_font"}}">{{svg "octicon-typography"}}</button>
{{if $supportEasyMDE}}
<button class="markdown-toolbar-button markdown-switch-easymde" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.switch_to_legacy.tooltip"}}">{{svg "octicon-arrow-switch"}}</button> <button class="markdown-toolbar-button markdown-switch-easymde" data-tooltip-content="{{ctx.Locale.Tr "editor.buttons.switch_to_legacy.tooltip"}}">{{svg "octicon-arrow-switch"}}</button>
{{end}}
</div> </div>
</markdown-toolbar> </markdown-toolbar>
<text-expander keys=": @ #" multiword="#" suffix=""> <text-expander keys=": @ #" multiword="#" suffix="">
<textarea class="markdown-text-editor"{{if .TextareaName}} name="{{.TextareaName}}"{{end}}{{if .TextareaPlaceholder}} placeholder="{{.TextareaPlaceholder}}"{{end}}{{if .TextareaAriaLabel}} aria-label="{{.TextareaAriaLabel}}"{{end}}{{if .DisableAutosize}} data-disable-autosize="{{.DisableAutosize}}"{{end}}>{{.TextareaContent}}</textarea> <textarea class="markdown-text-editor"
{{if .TextareaName}}name="{{.TextareaName}}"{{end}} {{if .TextareaMaxLength}}maxlength="{{.TextareaMaxLength}}"{{end}}
{{if .TextareaPlaceholder}}placeholder="{{.TextareaPlaceholder}}"{{end}} {{if $ariaLabel}}aria-label="{{$ariaLabel}}"{{end}}
{{if .DisableAutosize}}data-disable-autosize="{{.DisableAutosize}}"{{end}}
>{{.TextareaContent}}</textarea>
</text-expander> </text-expander>
<script> <script>
if (localStorage?.getItem('markdown-editor-monospace') === 'true') { if (localStorage?.getItem('markdown-editor-monospace') === 'true') {

View File

@ -29,6 +29,7 @@
<p id="signed-user-email">{{.SignedUser.Email}}</p> <p id="signed-user-email">{{.SignedUser.Email}}</p>
</div> </div>
<div class="field {{if .Err_Description}}error{{end}}"> <div class="field {{if .Err_Description}}error{{end}}">
{{/* it is rendered as markdown, but the length is limited, so at the moment we do not use the markdown editor here */}}
<label for="description">{{ctx.Locale.Tr "user.user_bio"}}</label> <label for="description">{{ctx.Locale.Tr "user.user_bio"}}</label>
<textarea id="description" name="description" rows="2" placeholder="{{ctx.Locale.Tr "settings.biography_placeholder"}}" maxlength="255">{{.SignedUser.Description}}</textarea> <textarea id="description" name="description" rows="2" placeholder="{{ctx.Locale.Tr "settings.biography_placeholder"}}" maxlength="255">{{.SignedUser.Description}}</textarea>
</div> </div>

View File

@ -96,6 +96,11 @@
font-size: 0.85em; font-size: 0.85em;
} }
.combo-markdown-editor .ui.tab.markup[data-tab-panel="markdown-previewer"] {
border-bottom: 1px solid var(--color-secondary);
padding-bottom: 1rem;
}
text-expander { text-expander {
display: block; display: block;
position: relative; position: relative;

View File

@ -1005,7 +1005,7 @@ td .commit-summary {
} }
.repository.view.issue .comment-list .code-comment .comment-content { .repository.view.issue .comment-list .code-comment .comment-content {
margin-left: 36px; margin-left: 24px;
} }
.repository.view.issue .comment-list .comment > .avatar { .repository.view.issue .comment-list .comment > .avatar {

View File

@ -102,19 +102,11 @@
cursor: pointer; cursor: pointer;
} }
.comment-code-cloud .ui.active.tab {
padding: 0.5em;
}
.comment-code-cloud .ui.active.tab.markup { .comment-code-cloud .ui.active.tab.markup {
padding: 1em; padding: 1em;
min-height: 168px; min-height: 168px;
} }
.comment-code-cloud .ui.tabular.menu {
margin: 0.5em;
}
.comment-code-cloud .editor-statusbar { .comment-code-cloud .editor-statusbar {
display: none; display: none;
} }
@ -123,23 +115,6 @@
padding: 10px 0; padding: 10px 0;
} }
.comment-code-cloud .footer .markup-info {
display: inline-block;
margin: 5px 0;
font-size: 12px;
color: var(--color-text-light);
}
.comment-code-cloud .footer .ui.right.floated {
padding-top: 6px;
}
.comment-code-cloud .footer::after {
clear: both;
content: "";
display: block;
}
.diff-file-body .comment-form { .diff-file-body .comment-form {
margin: 0 0 0 3em; margin: 0 0 0 3em;
} }

View File

@ -1,5 +1,7 @@
import {applyAreYouSure, initAreYouSure} from '../vendor/jquery.are-you-sure.ts'; import {applyAreYouSure, initAreYouSure} from '../vendor/jquery.are-you-sure.ts';
import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.ts'; import {handleGlobalEnterQuickSubmit} from './comp/QuickSubmit.ts';
import {queryElems} from '../utils/dom.ts';
import {initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
export function initGlobalFormDirtyLeaveConfirm() { export function initGlobalFormDirtyLeaveConfirm() {
initAreYouSure(window.jQuery); initAreYouSure(window.jQuery);
@ -11,7 +13,7 @@ export function initGlobalFormDirtyLeaveConfirm() {
} }
export function initGlobalEnterQuickSubmit() { export function initGlobalEnterQuickSubmit() {
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e: KeyboardEvent & {target: HTMLElement}) => {
if (e.key !== 'Enter') return; if (e.key !== 'Enter') return;
const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey); const hasCtrlOrMeta = ((e.ctrlKey || e.metaKey) && !e.altKey);
if (hasCtrlOrMeta && e.target.matches('textarea')) { if (hasCtrlOrMeta && e.target.matches('textarea')) {
@ -27,3 +29,7 @@ export function initGlobalEnterQuickSubmit() {
} }
}); });
} }
export function initGlobalComboMarkdownEditor() {
queryElems<HTMLElement>(document, '.combo-markdown-editor:not(.custom-init)', (el) => initComboMarkdownEditor(el));
}

View File

@ -1,6 +1,5 @@
import '@github/markdown-toolbar-element'; import '@github/markdown-toolbar-element';
import '@github/text-expander-element'; import '@github/text-expander-element';
import $ from 'jquery';
import {attachTribute} from '../tribute.ts'; import {attachTribute} from '../tribute.ts';
import {hideElem, showElem, autosize, isElemVisible} from '../../utils/dom.ts'; import {hideElem, showElem, autosize, isElemVisible} from '../../utils/dom.ts';
import { import {
@ -23,6 +22,8 @@ import {
} from './EditorMarkdown.ts'; } from './EditorMarkdown.ts';
import {DropzoneCustomEventReloadFiles, initDropzone} from '../dropzone.ts'; import {DropzoneCustomEventReloadFiles, initDropzone} from '../dropzone.ts';
import {createTippy} from '../../modules/tippy.ts'; import {createTippy} from '../../modules/tippy.ts';
import {fomanticQuery} from '../../modules/fomantic/base.ts';
import type EasyMDE from 'easymde';
let elementIdCounter = 0; let elementIdCounter = 0;
@ -48,18 +49,23 @@ export function validateTextareaNonEmpty(textarea) {
return true; return true;
} }
type ComboMarkdownEditorOptions = {
editorHeights?: {minHeight?: string, height?: string, maxHeight?: string},
easyMDEOptions?: EasyMDE.Options,
};
export class ComboMarkdownEditor { export class ComboMarkdownEditor {
static EventEditorContentChanged = EventEditorContentChanged; static EventEditorContentChanged = EventEditorContentChanged;
static EventUploadStateChanged = EventUploadStateChanged; static EventUploadStateChanged = EventUploadStateChanged;
public container : HTMLElement; public container : HTMLElement;
// TODO: use correct types to replace these "any" types options: ComboMarkdownEditorOptions;
options: any;
tabEditor: HTMLElement; tabEditor: HTMLElement;
tabPreviewer: HTMLElement; tabPreviewer: HTMLElement;
supportEasyMDE: boolean;
easyMDE: any; easyMDE: any;
easyMDEToolbarActions: any; easyMDEToolbarActions: any;
easyMDEToolbarDefault: any; easyMDEToolbarDefault: any;
@ -71,11 +77,12 @@ export class ComboMarkdownEditor {
dropzone: HTMLElement; dropzone: HTMLElement;
attachedDropzoneInst: any; attachedDropzoneInst: any;
previewMode: string;
previewUrl: string; previewUrl: string;
previewContext: string; previewContext: string;
previewMode: string;
constructor(container, options = {}) { constructor(container, options:ComboMarkdownEditorOptions = {}) {
if (container._giteaComboMarkdownEditor) throw new Error('ComboMarkdownEditor already initialized');
container._giteaComboMarkdownEditor = this; container._giteaComboMarkdownEditor = this;
this.options = options; this.options = options;
this.container = container; this.container = container;
@ -99,6 +106,10 @@ export class ComboMarkdownEditor {
} }
setupContainer() { setupContainer() {
this.supportEasyMDE = this.container.getAttribute('data-support-easy-mde') === 'true';
this.previewMode = this.container.getAttribute('data-content-mode');
this.previewUrl = this.container.getAttribute('data-preview-url');
this.previewContext = this.container.getAttribute('data-preview-context');
initTextExpander(this.container.querySelector('text-expander')); initTextExpander(this.container.querySelector('text-expander'));
} }
@ -137,12 +148,14 @@ export class ComboMarkdownEditor {
monospaceButton.setAttribute('aria-checked', String(enabled)); monospaceButton.setAttribute('aria-checked', String(enabled));
}); });
if (this.supportEasyMDE) {
const easymdeButton = this.container.querySelector('.markdown-switch-easymde'); const easymdeButton = this.container.querySelector('.markdown-switch-easymde');
easymdeButton.addEventListener('click', async (e) => { easymdeButton.addEventListener('click', async (e) => {
e.preventDefault(); e.preventDefault();
this.userPreferredEditor = 'easymde'; this.userPreferredEditor = 'easymde';
await this.switchToEasyMDE(); await this.switchToEasyMDE();
}); });
}
this.initMarkdownButtonTableAdd(); this.initMarkdownButtonTableAdd();
@ -187,6 +200,7 @@ export class ComboMarkdownEditor {
setupTab() { setupTab() {
const tabs = this.container.querySelectorAll<HTMLElement>('.tabular.menu > .item'); const tabs = this.container.querySelectorAll<HTMLElement>('.tabular.menu > .item');
if (!tabs.length) return;
// Fomantic Tab requires the "data-tab" to be globally unique. // 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. // So here it uses our defined "data-tab-for" and "data-tab-panel" to generate the "data-tab" attribute for Fomantic.
@ -207,11 +221,8 @@ export class ComboMarkdownEditor {
}); });
}); });
$(tabs).tab(); fomanticQuery(tabs).tab();
this.previewUrl = this.tabPreviewer.getAttribute('data-preview-url');
this.previewContext = this.tabPreviewer.getAttribute('data-preview-context');
this.previewMode = this.options.previewMode ?? 'comment';
this.tabPreviewer.addEventListener('click', async () => { this.tabPreviewer.addEventListener('click', async () => {
const formData = new FormData(); const formData = new FormData();
formData.append('mode', this.previewMode); formData.append('mode', this.previewMode);
@ -219,7 +230,7 @@ export class ComboMarkdownEditor {
formData.append('text', this.value()); formData.append('text', this.value());
const response = await POST(this.previewUrl, {data: formData}); const response = await POST(this.previewUrl, {data: formData});
const data = await response.text(); const data = await response.text();
renderPreviewPanelContent($(panelPreviewer), data); renderPreviewPanelContent(panelPreviewer, data);
}); });
} }
@ -284,7 +295,7 @@ export class ComboMarkdownEditor {
} }
async switchToUserPreference() { async switchToUserPreference() {
if (this.userPreferredEditor === 'easymde') { if (this.userPreferredEditor === 'easymde' && this.supportEasyMDE) {
await this.switchToEasyMDE(); await this.switchToEasyMDE();
} else { } else {
this.switchToTextarea(); this.switchToTextarea();
@ -304,7 +315,7 @@ export class ComboMarkdownEditor {
if (this.easyMDE) return; if (this.easyMDE) return;
// EasyMDE's CSS should be loaded via webpack config, otherwise our own styles can not overwrite the default styles. // 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 {default: EasyMDE} = await import(/* webpackChunkName: "easymde" */'easymde');
const easyMDEOpt = { const easyMDEOpt: EasyMDE.Options = {
autoDownloadFontAwesome: false, autoDownloadFontAwesome: false,
element: this.textarea, element: this.textarea,
forceSync: true, forceSync: true,
@ -384,19 +395,20 @@ export class ComboMarkdownEditor {
} }
get userPreferredEditor() { get userPreferredEditor() {
return window.localStorage.getItem(`markdown-editor-${this.options.useScene ?? 'default'}`); return window.localStorage.getItem(`markdown-editor-${this.previewMode ?? 'default'}`);
} }
set userPreferredEditor(s) { set userPreferredEditor(s) {
window.localStorage.setItem(`markdown-editor-${this.options.useScene ?? 'default'}`, s); window.localStorage.setItem(`markdown-editor-${this.previewMode ?? 'default'}`, s);
} }
} }
export function getComboMarkdownEditor(el) { export function getComboMarkdownEditor(el) {
if (el instanceof $) el = el[0]; if (!el) return null;
return el?._giteaComboMarkdownEditor; if (el.length) el = el[0];
return el._giteaComboMarkdownEditor;
} }
export async function initComboMarkdownEditor(container: HTMLElement, options = {}) { export async function initComboMarkdownEditor(container: HTMLElement, options:ComboMarkdownEditorOptions = {}) {
if (!container) { if (!container) {
throw new Error('initComboMarkdownEditor: container is null'); throw new Error('initComboMarkdownEditor: container is null');
} }

View File

@ -201,10 +201,8 @@ export function initRepoEditor() {
})(); })();
} }
export function renderPreviewPanelContent($previewPanel, data) { export function renderPreviewPanelContent(previewPanel: Element, content: string) {
$previewPanel.html(data); previewPanel.innerHTML = content;
initMarkupContent(); initMarkupContent();
attachRefIssueContextPopup(previewPanel.querySelectorAll('p .ref-issue'));
const $refIssues = $previewPanel.find('p .ref-issue');
attachRefIssueContextPopup($refIssues);
} }

View File

@ -414,11 +414,6 @@ export function initRepoPullRequestReview() {
await handleReply(this); await handleReply(this);
}); });
const elReviewBox = document.querySelector('.review-box-panel');
if (elReviewBox) {
initComboMarkdownEditor(elReviewBox.querySelector('.combo-markdown-editor'));
}
// The following part is only for diff views // The following part is only for diff views
if (!$('.repository.pull.diff').length) return; if (!$('.repository.pull.diff').length) return;

View File

@ -1,5 +1,4 @@
import {hideElem, showElem} from '../utils/dom.ts'; import {hideElem, showElem} from '../utils/dom.ts';
import {initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
export function initRepoRelease() { export function initRepoRelease() {
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
@ -16,7 +15,6 @@ export function initRepoReleaseNew() {
if (!document.querySelector('.repository.new.release')) return; if (!document.querySelector('.repository.new.release')) return;
initTagNameEditor(); initTagNameEditor();
initRepoReleaseEditor();
} }
function initTagNameEditor() { function initTagNameEditor() {
@ -48,11 +46,3 @@ function initTagNameEditor() {
hideTargetInput(e.target); hideTargetInput(e.target);
}); });
} }
function initRepoReleaseEditor() {
const editor = document.querySelector<HTMLElement>('.repository.new.release .combo-markdown-editor');
if (!editor) {
return;
}
initComboMarkdownEditor(editor);
}

View File

@ -2,6 +2,7 @@ import {initMarkupContent} from '../markup/content.ts';
import {validateTextareaNonEmpty, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts'; import {validateTextareaNonEmpty, initComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
import {fomanticMobileScreen} from '../modules/fomantic.ts'; import {fomanticMobileScreen} from '../modules/fomantic.ts';
import {POST} from '../modules/fetch.ts'; import {POST} from '../modules/fetch.ts';
import type {ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
async function initRepoWikiFormEditor() { async function initRepoWikiFormEditor() {
const editArea = document.querySelector<HTMLTextAreaElement>('.repository.wiki .combo-markdown-editor textarea'); const editArea = document.querySelector<HTMLTextAreaElement>('.repository.wiki .combo-markdown-editor textarea');
@ -9,7 +10,7 @@ async function initRepoWikiFormEditor() {
const form = document.querySelector('.repository.wiki.new .ui.form'); const form = document.querySelector('.repository.wiki.new .ui.form');
const editorContainer = form.querySelector<HTMLElement>('.combo-markdown-editor'); const editorContainer = form.querySelector<HTMLElement>('.combo-markdown-editor');
let editor; let editor: ComboMarkdownEditor;
let renderRequesting = false; let renderRequesting = false;
let lastContent; let lastContent;
@ -45,12 +46,10 @@ async function initRepoWikiFormEditor() {
renderEasyMDEPreview(); renderEasyMDEPreview();
editor = await initComboMarkdownEditor(editorContainer, { editor = await initComboMarkdownEditor(editorContainer, {
useScene: 'wiki',
// EasyMDE has some problems of height definition, it has inline style height 300px by default, so we also use inline styles to override it. // EasyMDE has some problems of height definition, it has inline style height 300px by default, so we also use inline styles to override it.
// And another benefit is that we only need to write the style once for both editors. // And another benefit is that we only need to write the style once for both editors.
// TODO: Move height style to CSS after EasyMDE removal. // TODO: Move height style to CSS after EasyMDE removal.
editorHeights: {minHeight: '300px', height: 'calc(100vh - 600px)'}, editorHeights: {minHeight: '300px', height: 'calc(100vh - 600px)'},
previewMode: 'wiki',
easyMDEOptions: { easyMDEOptions: {
previewRender: (_content, previewTarget) => previewTarget.innerHTML, // disable builtin preview render previewRender: (_content, previewTarget) => previewTarget.innerHTML, // disable builtin preview render
toolbar: ['bold', 'italic', 'strikethrough', '|', toolbar: ['bold', 'italic', 'strikethrough', '|',
@ -59,7 +58,7 @@ async function initRepoWikiFormEditor() {
'unordered-list', 'ordered-list', '|', 'unordered-list', 'ordered-list', '|',
'link', 'image', 'table', 'horizontal-rule', '|', 'link', 'image', 'table', 'horizontal-rule', '|',
'preview', 'fullscreen', 'side-by-side', '|', 'gitea-switch-to-textarea', 'preview', 'fullscreen', 'side-by-side', '|', 'gitea-switch-to-textarea',
], ] as any, // to use custom toolbar buttons
}, },
}); });

View File

@ -83,7 +83,11 @@ import {
initGlobalButtons, initGlobalButtons,
initGlobalDeleteButton, initGlobalDeleteButton,
} from './features/common-button.ts'; } from './features/common-button.ts';
import {initGlobalEnterQuickSubmit, initGlobalFormDirtyLeaveConfirm} from './features/common-form.ts'; import {
initGlobalComboMarkdownEditor,
initGlobalEnterQuickSubmit,
initGlobalFormDirtyLeaveConfirm,
} from './features/common-form.ts';
initGiteaFomantic(); initGiteaFomantic();
initDirAuto(); initDirAuto();
@ -127,6 +131,7 @@ onDomReady(() => {
initGlobalCopyToClipboardListener, initGlobalCopyToClipboardListener,
initGlobalEnterQuickSubmit, initGlobalEnterQuickSubmit,
initGlobalFormDirtyLeaveConfirm, initGlobalFormDirtyLeaveConfirm,
initGlobalComboMarkdownEditor,
initGlobalDeleteButton, initGlobalDeleteButton,
initCommonOrganization, initCommonOrganization,