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

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

Feature: rich replies phase 1 — shared editor, mobile full-screen, language (klonkt-demo-c7f)

Replying to fediverse comments used bare textareas in four places. This adds
ONE shared, progressively-enhanced editor component and mounts it on the two
new-reply spots (inline thread reply in fedi-node, and authorize_interaction);
the edit forms and prutter follow with the media phase.

  • partials/reply-editor.ejs + assets/js/reply-editor.js + css: renders a plain textarea that works without JS; the JS upgrades it to a contenteditable with a small toolbar (bold/italic/link/list/quote) and a language select. Assets load once per render even when the partial repeats per comment.
  • Mobile (max-width 700px): focusing the editor opens a FULL-SCREEN compose overlay (top bar with cancel and send, scroll lock), the right pattern on phones. Two real-world fixes came out of browser verification: site CSS gives thread forms display:contents, which collapses the form box and breaks both flex and position:fixed (now overridden with !important); and the overlay sits at z-index 1200, above the bottom tab bar (1050) and sheets (1100).
  • Server: fedi-reply and authorize_interaction accept content (editor HTML) + language next to text. deliverReply sanitizes the HTML (HtmlSanitizerService), runs the same mention/hashtag/URL enrichment as the plain path, and places the parent mention inline in the first paragraph (own paragraph before block content, merged paragraph around bare inline text). Plain-text path unchanged (no-JS fallback).
  • ap_outbox.language (additive) -> contentMap on the outgoing Note.

5 new tests (sanitize, mention placement, plain path unchanged, empty-html
reject, bogus language dropped); 88 green. Live-verified in the browser:
desktop upgrade, mobile full-screen (enter/cancel/scroll-lock), and a real
submit landing in ap_outbox with markup + language intact.

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

  • Property mode set to 100644
File size: 3.5 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%>
19<% var _reLang = (typeof defaultLang !== 'undefined' && defaultLang) || (typeof lang !== 'undefined' ? lang : 'en'); %>
20<form method="post" action="<%= action %>" class="re-form" data-re>
21 <% (typeof hiddenFields !== 'undefined' ? hiddenFields : []).forEach(function (h) { %>
22 <input type="hidden" name="<%= h.name %>" value="<%= h.value %>">
23 <% }); %>
24 <input type="hidden" name="content" value="">
25 <%# Full-screen top bar (mobile compose). Hidden until JS + fullscreen. %>
26 <div class="re-head" hidden>
27 <button type="button" class="re-cancel" aria-label="<%= t('comments.cancel') %>">×</button>
28 <strong class="re-head-title"><%= t('re.title') %></strong>
29 <button type="submit" class="btn re-send-top"><%= submitLabel %></button>
30 </div>
31 <div class="re-toolbar" role="toolbar" aria-label="<%= t('re.title') %>" hidden>
32 <button type="button" data-cmd="bold" title="<%= t('re.bold') %>" aria-label="<%= t('re.bold') %>"><b>B</b></button>
33 <button type="button" data-cmd="italic" title="<%= t('re.italic') %>" aria-label="<%= t('re.italic') %>"><i>I</i></button>
34 <button type="button" data-cmd="link" title="<%= t('re.link') %>" aria-label="<%= t('re.link') %>">🔗</button>
35 <button type="button" data-cmd="list" title="<%= t('re.list') %>" aria-label="<%= t('re.list') %>">•≡</button>
36 <button type="button" data-cmd="quote" title="<%= t('re.quote') %>" aria-label="<%= t('re.quote') %>">❝</button>
37 </div>
38 <textarea name="text" rows="<%= typeof rows !== 'undefined' ? rows : 3 %>" required placeholder="<%= placeholder %>"></textarea>
39 <div class="re-editor" contenteditable="true" role="textbox" aria-multiline="true"
40 data-ph="<%= placeholder %>" aria-label="<%= placeholder %>" hidden></div>
41 <div class="re-foot">
42 <select name="language" class="re-lang" aria-label="<%= t('re.lang') %>" hidden>
43 <% [['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) { %>
44 <option value="<%= l[0] %>" <%= l[0] === _reLang ? 'selected' : '' %>><%= l[1] %></option>
45 <% }); %>
46 </select>
47 <button type="submit" class="btn re-send"><%= submitLabel %></button>
48 </div>
49</form>
50<%# Assets once per render, even when this partial repeats per comment. %>
51<% if (!locals.__reAssets) { locals.__reAssets = 1; %>
52<link rel="stylesheet" href="/assets/css/reply-editor.css">
53<script src="/assets/js/reply-editor.js" defer></script>
54<% } %>
Note: See TracBrowser for help on using the repository browser.