This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

appview: use markdownEditor fragment for all markdown body inputs

Signed-off-by: Seongmin Lee <git@boltless.me>

author
Seongmin Lee
committer
Tangled
date (Jul 22, 2026, 4:28 PM +0300) commit 23aa5efc parent 86b99d9c change-id upxxpvvr
+128 -133
+6 -5
appview/pages/templates/fragments/comment/edit.html
··· 8 8 hx-disabled-elt="find button[type='submit']" 9 9 > 10 10 <input name="aturi" type="hidden" value="{{ .Comment.AtUri }}"> 11 - <textarea 12 - name="body" 13 - class="w-full p-2 rounded border border-gray-200 dark:border-gray-700" 14 - rows="5" 15 - autofocus>{{ .Comment.EditableBody }}</textarea> 11 + {{ template "fragments/markdownEditor" 12 + (dict "Name" "body" 13 + "Value" .Comment.EditableBody 14 + "Placeholder" "Describe this pull request. Markdown is supported.") }} 16 15 <div id="comment-error" class="error"></div> 17 16 {{ template "editActions" $ }} 18 17 </form> ··· 42 41 hx-get="/comment?aturi={{ .Comment.AtUri }}" 43 42 hx-target="closest form" 44 43 hx-swap="outerHTML" 44 + hx-indicator="this" 45 + hx-disabled-elt="this" 45 46 > 46 47 {{ i "x" "size-4" }} 47 48 Cancel
+6 -6
appview/pages/templates/fragments/comment/reply.html
··· 8 8 hx-disabled-elt="find button[type='submit']" 9 9 > 10 10 {{ template "user/fragments/picHandleLink" .LoggedInUser.Did }} 11 - <textarea 12 - name="body" 13 - class="w-full p-2" 14 - placeholder="Leave a reply..." 15 - autofocus 16 - rows="3"></textarea> 11 + {{ template "fragments/markdownEditor" 12 + (dict "Name" "body" 13 + "BlobName" "blob" 14 + "Rows" 3 15 + "AutoFocus" true 16 + "Placeholder" "Leave a reply...") }} 17 17 {{ template "replyActions" . }} 18 18 </form> 19 19 {{ end }}
+1 -1
appview/pages/templates/fragments/line-quote-button.html
··· 18 18 const btnEnd = document.getElementById('line-quote-btn-end'); 19 19 20 20 const textarea = () => 21 - document.getElementById('comment-textarea'); 21 + document.querySelector('form[hx-post="/comment"] textarea'); 22 22 23 23 const lineOf = (el) => 24 24 el?.closest?.('span[id*="-O"]')
+69
appview/pages/templates/fragments/markdownEditor.html
··· 1 + {{ define "fragments/markdownEditor" }} 2 + {{ $name := .Name }} 3 + {{ $value := .Value }} 4 + {{ $blobName := .BlobName }} 5 + {{ $blobValues := .BlobValues }} 6 + {{ $rows := (or .Rows 5) }} 7 + {{ $required := .Required }} 8 + {{ $autofocus := .AutoFocus }} 9 + {{ $placeholder := .Placeholder }} 10 + <div 11 + class="flex flex-col gap-2" 12 + data-md-editor 13 + hx-disinherit="*" 14 + hx-include="this" 15 + > 16 + <div class="btn-group self-start text-gray-600 dark:text-gray-300"> 17 + <button type="button" data-md-mode="write" 18 + class="btn-group-item active group"> 19 + {{ i "pencil" "w-3.5 h-3.5" }} 20 + Write 21 + </button> 22 + <button type="button" data-md-mode="preview" 23 + hx-post="/markup/preview" 24 + hx-target="next [data-md-preview]" 25 + hx-swap="innerHTML" 26 + hx-indicator="this" 27 + hx-disabled-elt="this" 28 + class="btn-group-item group"> 29 + {{ i "eye" "w-3.5 h-3.5 inline group-[.htmx-request]:hidden" }} 30 + {{ i "loader-circle" "w-3.5 h-3.5 animate-spin hidden group-[.htmx-request]:inline" }} 31 + Preview 32 + </button> 33 + </div> 34 + <div data-md-panel="write"> 35 + <textarea 36 + name="{{ $name }}" 37 + rows="{{ $rows }}" 38 + class="w-full resize-y dark:bg-gray-800 dark:text-white dark:border-gray-700" 39 + {{if $required}}required{{end}} 40 + {{if $autofocus}}autofocus{{end}} 41 + placeholder="{{ $placeholder }}" 42 + >{{with $value}}{{.}}{{end}}</textarea> 43 + </div> 44 + <div data-md-panel="preview" class="hidden"> 45 + <div data-md-preview class="min-h-[6rem] p-3 border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-900/30"> 46 + <span class="text-gray-400 dark:text-gray-500 italic">Loading preview...</span> 47 + </div> 48 + </div> 49 + </div> 50 + <script> 51 + (() => { 52 + if (window.__mdEditorWired) return; 53 + window.__mdEditorWired = true; 54 + document.body.addEventListener('click', (e) => { 55 + const btn = e.target.closest('[data-md-mode]'); 56 + if (!btn) return; 57 + const editor = btn.closest('[data-md-editor]'); 58 + if (!editor) return; 59 + const mode = btn.dataset.mdMode; 60 + editor.querySelectorAll('[data-md-panel]').forEach(p => { 61 + p.classList.toggle('hidden', p.dataset.mdPanel !== mode); 62 + }); 63 + editor.querySelectorAll('[data-md-mode]').forEach(b => { 64 + b.classList.toggle('active', b === btn); 65 + }); 66 + }); 67 + })(); 68 + </script> 69 + {{ end }}
+7 -12
appview/pages/templates/repo/issues/fragments/newComment.html
··· 3 3 <form 4 4 hx-post="/comment" 5 5 hx-swap="none" 6 - hx-trigger="submit, click from:#close-button, keydown[commentButtonEnabled() && (ctrlKey || metaKey) && key=='Enter'] from:#comment-textarea" 6 + hx-trigger="submit, click from:#close-button, keydown[commentButtonEnabled() && (ctrlKey || metaKey) && key=='Enter'] from:(find textarea)" 7 7 hx-disabled-elt="find button[type='submit']" 8 8 hx-on::before-request="document.getElementById('comment-error').innerHTML = ''" 9 9 hx-swap="none" 10 10 class="group/form " 11 11 > 12 12 <input name="subject-uri" type="hidden" value="{{ .Issue.AtUri }}"> 13 - <div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-full border border-gray-200 dark:border-gray-700"> 13 + <div class="bg-white dark:bg-gray-800 rounded drop-shadow-sm py-4 px-4 relative w-full border border-gray-200 dark:border-gray-700" hx-on:keyup="updateCommentForm()"> 14 14 <div class="text-sm pb-2 text-gray-500 dark:text-gray-400"> 15 15 {{ template "user/fragments/picHandleLink" .LoggedInUser.Did }} 16 16 </div> 17 - <textarea 18 - id="comment-textarea" 19 - name="body" 20 - class="w-full p-2 rounded" 21 - placeholder="Add to the discussion. Markdown is supported." 22 - onkeyup="updateCommentForm()" 23 - rows="5" 24 - required 25 - ></textarea> 17 + {{ template "fragments/markdownEditor" 18 + (dict "Name" "body" 19 + "BlobName" "blob" 20 + "Placeholder" "Add to the discussion. Markdown is supported.") }} 26 21 </div> 27 22 28 23 <div class="flex gap-2 mt-2 items-center"> ··· 85 80 } 86 81 87 82 function updateCommentForm() { 88 - const textarea = document.getElementById('comment-textarea'); 83 + const textarea = document.querySelector('form[hx-post="/comment"] textarea'); 89 84 const commentButton = document.getElementById('comment-button'); 90 85 const closeButtonText = document.getElementById('close-button-text'); 91 86
+6 -7
appview/pages/templates/repo/pulls/fragments/pullNewComment.html
··· 12 12 > 13 13 <input name="subject-uri" type="hidden" value="{{ .Pull.AtUri }}"> 14 14 <input name="pull-round-idx" type="hidden" value="{{ .RoundNumber }}"> 15 - <textarea 16 - id="comment-textarea" 17 - name="body" 18 - class="w-full p-2 rounded border" 19 - rows=8 20 - placeholder="Add to the discussion..."></textarea 21 - > 15 + {{ template "fragments/markdownEditor" 16 + (dict "Name" "body" 17 + "BlobName" "blob" 18 + "Rows" 8 19 + "Required" true 20 + "Placeholder" "Add to the discussion...") }} 22 21 {{ template "replyActions" . }} 23 22 <div id="comment-error-{{ .RoundNumber }}" class="error"></div> 24 23 </form>
+15 -83
appview/pages/templates/repo/pulls/fragments/pullStepDetails.html
··· 1 1 {{ define "repo/pulls/fragments/pullStepDetails" }} 2 2 {{ $hasSidePanel := and .LabelDefs .RepoInfo.Roles.IsPushAllowed }} 3 - {{ $previewUrl := printf "/%s/pulls/new/preview" .RepoInfo.FullName }} 4 3 {{ $labelCtx := dict "Defs" .LabelDefs "State" .LabelState "RepoInfo" .RepoInfo "Subject" "" "LoggedInUser" .LoggedInUser }} 5 4 6 5 <section class="flex flex-col md:flex-row gap-6"> 7 6 <div class="flex-1 min-w-0 flex flex-col gap-4"> 8 - {{ template "pullStepDetailsSingle" (dict "Root" . "PreviewUrl" $previewUrl) }} 7 + {{ template "pullStepDetailsSingle" . }} 9 8 {{ template "pullSubmitRow" . }} 10 9 </div> 11 10 ··· 17 16 {{ end }} 18 17 </section> 19 18 20 - {{ template "markdownEditorScript" }} 19 + <script> 20 + document.querySelectorAll('input[name=body]').forEach(el => { 21 + el.addEventListener('oninput', () => { 22 + document.querySelector('#bodyDirty').value = 1; 23 + }); 24 + }); 25 + </script> 21 26 {{ end }} 22 27 23 28 {{ define "pullStepDetailsSingle" }} 24 - {{ $root := .Root }} 25 - {{ $previewUrl := .PreviewUrl }} 26 - <input type="hidden" name="titleDirty" id="titleDirty" value="{{ if $root.TitleDirty }}1{{ end }}" /> 27 - <input type="hidden" name="bodyDirty" id="bodyDirty" value="{{ if $root.BodyDirty }}1{{ end }}" /> 29 + <input type="hidden" name="titleDirty" id="titleDirty" value="{{ if .TitleDirty }}1{{ end }}" /> 30 + <input type="hidden" name="bodyDirty" id="bodyDirty" value="{{ if .BodyDirty }}1{{ end }}" /> 28 31 <div class="flex flex-col gap-1"> 29 32 <label for="title" class="text-xs tracking-wide text-gray-800 dark:text-gray-200">Title</label> 30 33 <input 31 34 type="text" 32 35 name="title" 33 36 id="title" 34 - value="{{ $root.Title }}" 37 + value="{{ .Title }}" 35 38 oninput="document.getElementById('titleDirty').value='1'" 36 39 class="w-full dark:bg-gray-800 dark:text-white dark:border-gray-700" 37 40 placeholder="One-line summary of your change." 38 41 /> 39 42 </div> 40 43 41 - {{ template "markdownEditor" (dict 42 - "Id" "pull-body" 43 - "Name" "body" 44 - "Value" $root.Body 45 - "Rows" 6 44 + {{ template "fragments/markdownEditor" (dict 45 + "Name" "body" 46 + "Value" .Body 47 + "Rows" 6 46 48 "Placeholder" "Describe your change. Markdown is supported." 47 - "PreviewUrl" $previewUrl 48 - "DirtyFlag" "bodyDirty" 49 49 ) }} 50 50 {{ end }} 51 51 52 - {{ define "markdownEditor" }} 53 - {{ $id := .Id }} 54 - {{ $name := .Name }} 55 - {{ $value := .Value }} 56 - {{ $rows := .Rows }} 57 - {{ $placeholder := .Placeholder }} 58 - {{ $previewUrl := .PreviewUrl }} 59 - {{ $dirtyFlag := .DirtyFlag }} 60 - <div class="flex flex-col gap-2" data-md-editor="{{ $id }}"> 61 - <div class="btn-group self-start text-gray-600 dark:text-gray-300"> 62 - <button type="button" data-md-mode="write" 63 - class="btn-group-item active group"> 64 - {{ i "pencil" "w-3.5 h-3.5" }} 65 - Write 66 - </button> 67 - <button type="button" data-md-mode="preview" 68 - hx-post="{{ $previewUrl }}" 69 - hx-vals='js:{body: document.querySelector("[data-md-editor=\"{{ $id }}\"] textarea").value}' 70 - hx-params="body" 71 - hx-target="[data-md-editor='{{ $id }}'] [data-md-preview]" 72 - hx-swap="innerHTML" 73 - hx-indicator="this" 74 - class="btn-group-item group"> 75 - {{ i "eye" "w-3.5 h-3.5 inline group-[.htmx-request]:hidden" }} 76 - {{ i "loader-circle" "w-3.5 h-3.5 animate-spin hidden group-[.htmx-request]:inline" }} 77 - Preview 78 - </button> 79 - </div> 80 - <div data-md-panel="write"> 81 - <textarea 82 - id="{{ $id }}" 83 - name="{{ $name }}" 84 - rows="{{ $rows }}" 85 - class="w-full resize-y dark:bg-gray-800 dark:text-white dark:border-gray-700" 86 - placeholder="{{ $placeholder }}" 87 - {{ if $dirtyFlag }}oninput="document.getElementById('{{ $dirtyFlag }}').value='1'"{{ end }} 88 - >{{ $value }}</textarea> 89 - </div> 90 - <div data-md-panel="preview" class="hidden"> 91 - <div data-md-preview class="min-h-[6rem] p-3 border border-gray-200 dark:border-gray-700 rounded bg-gray-50 dark:bg-gray-900/30"> 92 - <span class="text-gray-400 dark:text-gray-500 italic">Loading preview...</span> 93 - </div> 94 - </div> 95 - </div> 96 - {{ end }} 97 - 98 52 {{ define "pullSubmitRow" }} 99 53 <div class="flex items-center justify-end gap-4 mt-auto"> 100 54 {{ if and .MergeCheck .MergeCheck.IsConflicted }} ··· 125 79 </button> 126 80 </div> 127 81 {{ end }} 128 - 129 - {{ define "markdownEditorScript" }} 130 - <script> 131 - (() => { 132 - if (window.__mdEditorWired) return; 133 - window.__mdEditorWired = true; 134 - document.body.addEventListener('click', (e) => { 135 - const btn = e.target.closest('[data-md-mode]'); 136 - if (!btn) return; 137 - const editor = btn.closest('[data-md-editor]'); 138 - if (!editor) return; 139 - const mode = btn.dataset.mdMode; 140 - editor.querySelectorAll('[data-md-panel]').forEach(p => { 141 - p.classList.toggle('hidden', p.dataset.mdPanel !== mode); 142 - }); 143 - editor.querySelectorAll('[data-md-mode]').forEach(b => { 144 - b.classList.toggle('active', b === btn); 145 - }); 146 - }); 147 - })(); 148 - </script> 149 - {{ end }}
+1 -5
appview/pages/templates/repo/pulls/fragments/pullStepReview.html
··· 98 98 {{ define "pullReviewStackedCommits" }} 99 99 {{ $root := . }} 100 100 {{ $commits := .Comparison.FormatPatch }} 101 - {{ $previewUrl := printf "/%s/pulls/new/preview" .RepoInfo.FullName }} 102 101 {{ $hasSidePanel := and $root.LabelDefs $root.RepoInfo.Roles.IsPushAllowed }} 103 102 <ul class="flex flex-col gap-2"> 104 103 {{ range $idx, $p := $commits }} ··· 148 147 placeholder="{{ $p.Title }}" 149 148 /> 150 149 </div> 151 - {{ template "markdownEditor" (dict 152 - "Id" (printf "stack-body-%s" $cid) 150 + {{ template "fragments/markdownEditor" (dict 153 151 "Name" $bodyName 154 152 "Value" $bodyValue 155 153 "Rows" 4 156 154 "Placeholder" "Describe this pull request. Markdown is supported." 157 - "LabelText" "description" 158 - "PreviewUrl" $previewUrl 159 155 ) }} 160 156 </div> 161 157 {{ if $hasSidePanel }}
+5 -7
appview/pages/templates/strings/string.html
··· 125 125 <div class="text-sm pb-2 text-gray-500 dark:text-gray-400"> 126 126 {{ template "user/fragments/picHandleLink" .LoggedInUser.Did }} 127 127 </div> 128 - <textarea 129 - name="body" 130 - class="w-full p-2 rounded" 131 - placeholder="Add to the discussion. Markdown is supported." 132 - rows="5" 133 - required 134 - ></textarea> 128 + {{ template "fragments/markdownEditor" 129 + (dict "Name" "body" 130 + "BlobName" "blob" 131 + "Required" true 132 + "Placeholder" "Add to the discussion. Markdown is supported.") }} 135 133 <div id="comment-error" class="error"></div> 136 134 </div> 137 135 <div class="flex gap-2 mt-2">
-5
appview/pulls/compose.go
··· 155 155 } 156 156 } 157 157 158 - func (s *Pulls) MarkdownPreview(w http.ResponseWriter, r *http.Request) { 159 - body := r.FormValue("body") 160 - s.pages.MarkdownPreviewFragment(w, body) 161 - } 162 - 163 158 func (s *Pulls) RefreshCompose(w http.ResponseWriter, r *http.Request) { 164 159 l := s.logger.With("handler", "RefreshCompose") 165 160
-1
appview/pulls/router.go
··· 14 14 r.Get("/", s.NewPull) 15 15 r.Get("/refresh", s.RefreshCompose) 16 16 r.Post("/refresh", s.RefreshCompose) 17 - r.Post("/preview", s.MarkdownPreview) 18 17 r.Post("/", s.NewPull) 19 18 }) 20 19
+1 -1
appview/state/comment.go
··· 71 71 Comment: comment, 72 72 }) 73 73 if err != nil { 74 - l.Error("failed to render") 74 + l.Error("failed to render", "err", err) 75 75 } 76 76 } 77 77
+8
appview/state/markup.go
··· 1 + package state 2 + 3 + import "net/http" 4 + 5 + func (s *State) MarkdownPreview(w http.ResponseWriter, r *http.Request) { 6 + body := r.FormValue("body") 7 + s.pages.MarkdownPreviewFragment(w, body) 8 + }
+3
appview/state/router.go
··· 257 257 r.Delete("/", s.DeleteComment) 258 258 }) 259 259 260 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/markup", func(r chi.Router) { 261 + r.Post("/preview", s.MarkdownPreview) 262 + }) 260 263 r.Get("/profile/popover", s.ProfilePopover) 261 264 262 265 r.Route("/profile", func(r chi.Router) {