source: Klonkt/src/views/partials/reply-editor.ejs@ ff3b8ce

main
Last change on this file since ff3b8ce was ff3b8ce, checked in by Robin <roboburr@…>, 7 weeks ago

Polish: reply editor styled as a sibling of the post editor (klonkt-demo-3dp)

The rich reply editor worked but looked ad-hoc. This is a visual pass, no
behavior change:

  • Toolbar: text glyphs (B/I/link/list/quote/paperclip) replaced with the post editor's Feather SVG icons; 32px buttons, transparent border, 6px radius, hover only on hover-capable devices, and an is-active accent-filled state reflecting the caret's formatting (bold/italic/list) via queryCommandState.
  • Editor box: framed with a rule border and an accent focus ring, tidy paragraph/list/blockquote/link spacing.
  • Footer, language select, mentions chips and attachment chips restyled to the same rule/paper/accent tokens; the mobile full-screen top bar gets a divider.

Reuses the theme's --rule/--paper/--paper-2/--accent tokens so it tracks light
and dark. 97 tests green; browser-verified: 32px SVG buttons + separator, framed
editor (10px radius), bold shows is-active on selection, mobile full-screen top
bar order and z-index intact.

Co-Authored-By: Claude Opus 4.8 <noreply@…>

  • Property mode set to 100644
File size: 7.3 KB
Line 
1<%#
2 Rich reply editor (shared component) — progressive enhancement.
3
4 Renders a plain <textarea name="text"> form that works without JS; the shared
5 /assets/js/reply-editor.js upgrades it to a rich contenteditable with a small
6 toolbar, a language select and (on ≤700px) a full-screen compose overlay,
7 which is the right pattern on mobile. On submit the JS fills the hidden
8 `content` field with the editor HTML and `text` with a plain-text fallback;
9 the server prefers `content` and sanitizes it (fedi-reply / authorize_interaction).
10
11 Locals:
12 action POST target (required)
13 hiddenFields [{ name, value }] extra hidden inputs (optional)
14 placeholder textarea/editor placeholder (required)
15 submitLabel submit button label (required)
16 rows textarea rows for the no-JS fallback (default 3)
17 defaultLang preselected reply language (default: UI lang)
18 initialHtml prefill for EDIT mode (our own sanitized HTML) (optional)
19 initialText plain-text prefill for the no-JS textarea (optional)
20 noAttach true hides the media button (edits do not touch attachments)
21 participants [{uri, url, handle}] conversation partners → the mentions bar
22 (u02): implicit mentions live here as removable chips, not in
23 the text. Absent = no bar, legacy addressing.
24%>
25<% var _reInitHtml = (typeof initialHtml !== 'undefined' && initialHtml) || ''; %>
26<% var _reInitText = (typeof initialText !== 'undefined' && initialText) || ''; %>
27<% var _reNoAttach = (typeof noAttach !== 'undefined' && noAttach); %>
28<% var _reParts = (typeof participants !== 'undefined' && participants && participants.length) ? participants : null; %>
29<% var _reLang = (typeof defaultLang !== 'undefined' && defaultLang) || (typeof lang !== 'undefined' ? lang : 'en'); %>
30<form method="post" action="<%= action %>" class="re-form" data-re
31 data-upload="/posts/upload-reply-media" data-upload-err="<%= t('re.attach_err') %>">
32 <% (typeof hiddenFields !== 'undefined' ? hiddenFields : []).forEach(function (h) { %>
33 <input type="hidden" name="<%= h.name %>" value="<%= h.value %>">
34 <% }); %>
35 <input type="hidden" name="content" value="">
36 <%# Mentions bar (u02): the conversation partners as removable chips. The
37 hidden field always carries the CURRENT kept list (server-rendered full,
38 so the no-JS path addresses everyone). %>
39 <% if (_reParts) { %>
40 <input type="hidden" name="mentions" value="<%= JSON.stringify(_reParts) %>">
41 <div class="re-mentions">
42 <span class="re-mentions-label"><%= t('re.to') %></span>
43 <% _reParts.forEach(function (p) { %>
44 <span class="re-mention" data-uri="<%= p.uri %>"><%= p.handle && p.handle[0] === '@' ? p.handle : '@' + p.handle %><button type="button" class="re-mention-del" title="<%= t('re.mention_del') %>" aria-label="<%= t('re.mention_del') %>">×</button></span>
45 <% }); %>
46 </div>
47 <% } %>
48 <%# Full-screen top bar (mobile compose). Hidden until JS + fullscreen. %>
49 <div class="re-head" hidden>
50 <button type="button" class="re-cancel" aria-label="<%= t('comments.cancel') %>">×</button>
51 <strong class="re-head-title"><%= t('re.title') %></strong>
52 <button type="submit" class="btn re-send-top"><%= submitLabel %></button>
53 </div>
54 <div class="re-toolbar" role="toolbar" aria-label="<%= t('re.title') %>" hidden>
55 <button type="button" data-cmd="bold" title="<%= t('re.bold') %>" aria-label="<%= t('re.bold') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 4h8a4 4 0 0 1 0 8H6z"/><path d="M6 12h9a4 4 0 0 1 0 8H6z"/></svg></button>
56 <button type="button" data-cmd="italic" title="<%= t('re.italic') %>" aria-label="<%= t('re.italic') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/></svg></button>
57 <button type="button" data-cmd="link" title="<%= t('re.link') %>" aria-label="<%= t('re.link') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></button>
58 <button type="button" data-cmd="list" title="<%= t('re.list') %>" aria-label="<%= t('re.list') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="9" y1="6" x2="20" y2="6"/><line x1="9" y1="12" x2="20" y2="12"/><line x1="9" y1="18" x2="20" y2="18"/><circle cx="4.5" cy="6" r="1.2"/><circle cx="4.5" cy="12" r="1.2"/><circle cx="4.5" cy="18" r="1.2"/></svg></button>
59 <button type="button" data-cmd="quote" title="<%= t('re.quote') %>" aria-label="<%= t('re.quote') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.75-2-2-2H4c-1.25 0-2 .75-2 2v6c0 1.25.75 2 2 2h2.5C6 17.5 4 19 3 19v2z"/><path d="M14 21c3 0 7-1 7-8V5c0-1.25-.75-2-2-2h-4c-1.25 0-2 .75-2 2v6c0 1.25.75 2 2 2h2.5c-.5 4.5-2.5 6-3.5 6v2z"/></svg></button>
60 <% if (!_reNoAttach) { %>
61 <span class="re-toolbar-sep" aria-hidden="true"></span>
62 <button type="button" data-cmd="attach" title="<%= t('re.attach') %>" aria-label="<%= t('re.attach') %>"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg></button>
63 <input type="file" class="re-file" accept="image/*,audio/*,video/*" multiple hidden>
64 <% } %>
65 </div>
66 <% if (!_reNoAttach) { %>
67 <input type="hidden" name="attachments" value="">
68 <div class="re-attachments" hidden></div>
69 <% } %>
70 <textarea name="text" rows="<%= typeof rows !== 'undefined' ? rows : 3 %>" required placeholder="<%= placeholder %>"><%= _reInitText %></textarea>
71 <div class="re-editor" contenteditable="true" role="textbox" aria-multiline="true"
72 data-ph="<%= placeholder %>" aria-label="<%= placeholder %>" hidden><%- _reInitHtml %></div>
73 <div class="re-foot">
74 <select name="language" class="re-lang" aria-label="<%= t('re.lang') %>" hidden>
75 <% [['en','English'],['nl','Nederlands'],['de','Deutsch'],['fr','Français'],['es','Español'],['it','Italiano'],['pt','Português'],['pl','Polski'],['ru','Русский'],['uk','Українська'],['sv','Svenska'],['da','Dansk'],['nb','Norsk'],['fi','Suomi'],['tr','Türkçe'],['ja','日本語'],['zh','中文'],['ar','العربية']].forEach(function (l) { %>
76 <option value="<%= l[0] %>" <%= l[0] === _reLang ? 'selected' : '' %>><%= l[1] %></option>
77 <% }); %>
78 </select>
79 <button type="submit" class="btn re-send"><%= submitLabel %></button>
80 </div>
81</form>
82<%# Assets once per render, even when this partial repeats per comment. %>
83<% if (!locals.__reAssets) { locals.__reAssets = 1; %>
84<link rel="stylesheet" href="/assets/css/reply-editor.css">
85<script src="/assets/js/reply-editor.js" defer></script>
86<% } %>
Note: See TracBrowser for help on using the repository browser.