source: Klonkt/src/views/pages/news.ejs@ 3597763

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

Web-timeline: custom-emoji + embedded quote-kaart (afgekeken van Shaer)

De Shaer-apps renderen FEP-9098 custom-emoji's en de FEP-044f quote-kaart
inline; de Klonkt web-timeline toonde nog letterlijke :shortcodes: en geen
quote. Nu rendert de web-nieuwsfeed dezelfde dingen server-side: emoji's in
content, auteur-naam en booster-naam, plus de geneste quote-kaart (avatar +
naam + content + thumbnail). De byline (avatar/naam/handle) en de boost-icon
(SVG, themebaar) had de web-UI al.

De data was al aanwezig op de timeline-rijen (emoji_json, author_emoji_json,
reblog_emoji_json, quote_json); dit is puur de render-kant.

New file:
src/services/NoteRender.js

  • pure helpers: emojiMap (beide vormen), emojiHtml (tag-bewust, skip code/pre), emojiName (escape + emoji), parseQuote

src/views/partials/quote-card.ejs

  • geneste quote-kaart (mirror van de Shaer QuoteCard)

test/note-render.test.js

  • 7 tests voor de render-helpers

Changed files:
src/middleware/render.js

  • emojiHtml/emojiName/noteQuote als template-helpers in de render-locals

src/views/partials/tl-item.ejs

  • content, auteur-naam en booster-naam via de emoji-helpers; quote-kaart erna

src/views/pages/news.ejs

  • CSS: img.emoji (inline, geen media-styling) + .tl-quote* (themebaar)

remarks: 187 tests groen (was 180). Visueel geverifieerd via een standalone
render van tl-item met echte emoji-urls. Volgende surfaces (messages, profiel,
cirkel) kunnen dezelfde helpers hergebruiken.

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

  • Property mode set to 100644
File size: 20.3 KB
Line 
1<div class="tl-wrap">
2 <%- include('../partials/fedi-tabs', { active: 'feed' }) %>
3 <div class="tl-titlebar">
4 <h1 class="tl-title"><%= t('tl.title') %></h1>
5 <button type="button" id="tl-paste-btn" class="tl-paste-btn" title="<%= t('fedi.remote_interact') %>" aria-label="<%= t('fedi.remote_interact') %>">
6 <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
7 </button>
8 </div>
9 <p class="tl-lead"><%= t('tl.lead') %></p>
10
11 <div class="tl-paste" id="tl-paste">
12 <div class="tl-paste-card" role="dialog" aria-label="<%= t('fedi.remote_interact') %>">
13 <button type="button" class="tl-paste-x" aria-label="<%= t('fedi.cancel') %>">&times;</button>
14 <h3 class="tl-paste-title"><%= t('fedi.remote_interact') %></h3>
15 <input type="url" class="tl-paste-input" inputmode="url" autocomplete="off" autocapitalize="none" spellcheck="false" placeholder="<%= t('tl.paste_ph') %>">
16 <div class="tl-paste-actions">
17 <button type="button" class="tl-paste-cancel"><%= t('fedi.cancel') %></button>
18 <button type="button" class="tl-paste-go"><%= t('tl.paste_go') %></button>
19 </div>
20 </div>
21 </div>
22 <style>
23 .tl-titlebar { display: flex; align-items: center; justify-content: space-between; gap: .5rem; }
24 .tl-boost-by { display: flex; align-items: center; gap: .35rem; font-size: .8rem; color: var(--ink-soft); margin: 0 0 .45rem .2rem; }
25 .tl-boost-by svg { color: #16a34a; flex: 0 0 auto; }
26 .tl-boost-by strong { color: var(--ink); font-weight: 600; }
27 .tl-paste-btn { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center; width: 38px; height: 38px;
28 border: 1px solid var(--rule); border-radius: 999px; background: var(--paper-2); color: var(--ink-soft); cursor: pointer; transition: color .15s, border-color .15s, background .15s; }
29 .tl-paste-btn:hover { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 50%, transparent); background: color-mix(in srgb, var(--accent) 10%, transparent); }
30 .tl-paste { position: fixed; inset: 0; z-index: 2147483601; display: none; align-items: center; justify-content: center; padding: 1rem; background: rgba(0,0,0,.55); }
31 .tl-paste.is-open { display: flex; }
32 .tl-paste-card { position: relative; width: 100%; max-width: 460px; background: var(--paper); color: var(--ink); border: 1px solid var(--rule);
33 border-radius: 16px; padding: 1.4rem 1.4rem 1.2rem; box-shadow: 0 18px 50px rgba(0,0,0,.4); }
34 .tl-paste-x { position: absolute; top: .5rem; right: .6rem; width: 2rem; height: 2rem; border: 0; border-radius: 999px; background: transparent; color: var(--ink-soft); font-size: 1.4rem; line-height: 1; cursor: pointer; }
35 .tl-paste-title { margin: 0 0 .8rem; font-size: 1.1rem; }
36 .tl-paste-input { width: 100%; box-sizing: border-box; padding: .6rem .8rem; border: 1.5px solid var(--rule); border-radius: 10px; background: var(--paper-2); color: var(--ink); font: inherit; font-size: .95rem; }
37 .tl-paste-input:focus { outline: none; border-color: var(--accent); }
38 .tl-paste-actions { display: flex; justify-content: flex-end; gap: .6rem; margin-top: 1rem; }
39 .tl-paste-actions button { padding: .5rem 1.1rem; border-radius: 999px; font: inherit; font-weight: 600; font-size: .88rem; cursor: pointer; border: 0; }
40 .tl-paste-cancel { background: transparent; border: 1.5px solid var(--rule); color: var(--ink); }
41 .tl-paste-go { background: var(--accent); color: var(--paper); }
42 </style>
43 <script>
44 (function () {
45 if (window.__tlPasteWired) return; window.__tlPasteWired = true;
46 function modal() { return document.getElementById('tl-paste'); }
47 function openM() {
48 var m = modal(); if (!m) return;
49 var inp = m.querySelector('.tl-paste-input');
50 m.classList.add('is-open');
51 if (inp) {
52 inp.value = ''; inp.focus();
53 if (navigator.clipboard && navigator.clipboard.readText) {
54 navigator.clipboard.readText().then(function (t) {
55 t = (t || '').trim();
56 if (/^https?:\/\//i.test(t) && !inp.value) { inp.value = t; inp.select(); }
57 }).catch(function () {});
58 }
59 }
60 }
61 function closeM() { var m = modal(); if (m) m.classList.remove('is-open'); }
62 function go() {
63 var m = modal(); if (!m) return;
64 var inp = m.querySelector('.tl-paste-input'), v = (inp ? inp.value : '').trim();
65 if (!/^https?:\/\//i.test(v)) { if (inp) inp.focus(); return; }
66 location.href = '/authorize_interaction?uri=' + encodeURIComponent(v);
67 }
68 document.addEventListener('click', function (e) {
69 if (e.target.closest && e.target.closest('#tl-paste-btn')) { e.preventDefault(); openM(); return; }
70 if (e.target.closest && e.target.closest('.tl-paste-go')) { e.preventDefault(); go(); return; }
71 if (e.target.closest && (e.target.closest('.tl-paste-cancel') || e.target.closest('.tl-paste-x'))) { e.preventDefault(); closeM(); return; }
72 var op = document.querySelector('.tl-paste.is-open'); if (op && e.target === op) closeM();
73 });
74 document.addEventListener('keydown', function (e) {
75 var m = document.querySelector('.tl-paste.is-open'); if (!m) return;
76 if (e.key === 'Escape') closeM();
77 else if (e.key === 'Enter' && e.target.closest && e.target.closest('.tl-paste')) { e.preventDefault(); go(); }
78 });
79 })();
80 </script>
81 <% if (typeof success !== 'undefined' && success) { %><div class="alert alert-success"><%= success %></div><% } %>
82 <% if (typeof error !== 'undefined' && error) { %><div class="alert alert-error"><%= error %></div><% } %>
83
84 <% if (!timeline || !timeline.length) { %>
85 <div class="tl-empty">
86 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>
87 <p><%= t('tl.empty') %></p>
88 </div>
89 <% } else { %>
90 <ol class="tl-feed" id="tl-feed">
91 <% timeline.forEach(function(p){ %>
92 <%- include('../partials/tl-item', { p: p }) %>
93 <% }); %>
94 </ol>
95 <%- include('../partials/load-more', { hasMore: hasMore, nextOffset: nextOffset, moreBase: moreBase, moreTarget: '#tl-feed', morePath: '/news' }) %>
96 <% } %>
97</div>
98
99<style>
100 .tl-wrap { max-width: 640px; margin: 1rem auto 3rem; padding: 0 1rem; }
101 .tl-title { margin: 0 0 .15rem; font-size: clamp(1.5rem, 4vw, 1.8rem); }
102 .tl-lead { color: var(--ink-soft, #888); margin: 0 0 1.5rem; }
103 .tl-empty { text-align: center; color: var(--ink-soft, #888); margin: 3rem auto; }
104 .tl-empty svg { width: 44px; height: 44px; color: color-mix(in srgb, var(--accent, #888) 60%, transparent); margin: 0 auto .75rem; display: block; }
105
106 .tl-feed { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 1rem; }
107 .tl-item { padding: 1.1rem 1.15rem; border-radius: 16px;
108 border: 1px solid color-mix(in srgb, var(--ink, #000) 8%, transparent);
109 background: color-mix(in srgb, var(--ink, #000) 2.5%, transparent);
110 transition: border-color .15s ease, box-shadow .15s ease; }
111 .tl-item:hover { border-color: color-mix(in srgb, var(--accent, #888) 32%, transparent);
112 box-shadow: 0 2px 14px color-mix(in srgb, var(--ink, #000) 6%, transparent); }
113
114 .tl-head { display: flex; align-items: center; gap: .65rem; margin: 0 0 .65rem; }
115 .tl-avatar { flex: 0 0 44px; width: 44px; height: 44px; border-radius: 50%; overflow: hidden;
116 display: inline-flex; align-items: center; justify-content: center; font-weight: 700; font-size: 1.1rem;
117 background: color-mix(in srgb, var(--accent, #888) 22%, transparent); color: var(--accent, #555);
118 box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent, #888) 18%, transparent); }
119 .tl-avatar img { width: 100%; height: 100%; object-fit: cover; background: #fff; }
120 .tl-id { flex: 1; min-width: 0; display: flex; flex-direction: column; line-height: 1.25; }
121 .tl-author { font-weight: 700; color: var(--ink, inherit); text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
122 .tl-author:hover { text-decoration: underline; }
123 .tl-handle { color: var(--ink-soft, #888); font-size: .82rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
124 .tl-time { color: var(--ink-soft, #999); font-size: .78rem; flex: 0 0 auto; align-self: flex-start; white-space: nowrap; }
125
126 .tl-content { line-height: 1.55; overflow-wrap: anywhere; }
127 .tl-content p { margin: .4rem 0; } .tl-content p:first-child { margin-top: 0; } .tl-content p:last-child { margin-bottom: 0; }
128 .tl-content a { color: var(--accent, #06c); }
129 .tl-content img { max-width: 100%; height: auto; border-radius: 10px; background: #fff; }
130 /* Long posts collapse to a max height with a fade + "read more" (added by JS only when it overflows). */
131 .tl-content.tl-clamp { max-height: 20em; overflow: hidden;
132 -webkit-mask-image: linear-gradient(180deg, #000 72%, transparent);
133 mask-image: linear-gradient(180deg, #000 72%, transparent); }
134 .tl-readmore { display: inline-block; margin: .15rem 0 .1rem; padding: .15rem 0;
135 background: none; border: 0; color: var(--accent, #06c); font: inherit; font-weight: 600;
136 font-size: .85rem; cursor: pointer; }
137 .tl-readmore:hover { text-decoration: underline; }
138
139 /* FEP-9098 custom emojis: small inline images, never treated as media. */
140 img.emoji { height: 1.35em; width: auto; margin: 0 .04em; vertical-align: -0.22em;
141 display: inline-block; border-radius: 0; background: none; box-shadow: none; }
142 .tl-content img.emoji, .tl-quote-body img.emoji { max-width: none; }
143
144 /* FEP-044f embedded quote card (mirror of the Shaer QuoteCard). */
145 .tl-quote { margin: .75rem 0 0; padding: .6rem .7rem; border-radius: 12px;
146 border: 1px solid color-mix(in srgb, var(--ink, #000) 12%, transparent);
147 background: color-mix(in srgb, var(--ink, #000) 3.5%, transparent); }
148 .tl-quote-head { display: flex; align-items: center; gap: .4rem; margin: 0 0 .35rem; min-width: 0; }
149 .tl-quote-avatar { flex: 0 0 auto; width: 22px; height: 22px; border-radius: 50%; object-fit: cover; background: #fff; }
150 .tl-quote-name { font-weight: 700; font-size: .85rem; color: var(--ink, inherit); white-space: nowrap; }
151 .tl-quote-handle { color: var(--ink-soft, #888); font-size: .78rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
152 .tl-quote-body { line-height: 1.5; font-size: .92rem; color: var(--ink-soft, #555);
153 overflow-wrap: anywhere; max-height: 16em; overflow: hidden; }
154 .tl-quote-body p { margin: .3rem 0; } .tl-quote-body p:first-child { margin-top: 0; } .tl-quote-body p:last-child { margin-bottom: 0; }
155 .tl-quote-body a { color: var(--accent, #06c); }
156 .tl-quote-media { display: block; margin: .45rem 0 0; }
157 .tl-quote-media img { max-width: 100%; height: auto; border-radius: 8px; }
158
159 /* Always show the FULL image at its natural ratio — never cropped, no letterbox. */
160 .tl-media { display: flex; flex-direction: column; gap: .4rem; margin: .75rem 0 0; }
161 .tl-media-img { display: block; border-radius: 12px; overflow: hidden;
162 background-image: linear-gradient(135deg,
163 color-mix(in srgb, var(--accent, #888) 22%, var(--paper-2, var(--paper))) 0%,
164 color-mix(in srgb, var(--accent, #888) 6%, var(--paper-2, var(--paper))) 100%); }
165 .tl-media-img img { width: 100%; height: auto; display: block; background: #fff; transition: opacity .4s ease; }
166 .tl-media-img img.is-loading { opacity: 0; }
167 /* Reserve placeholder space while the (natural-aspect) feed image loads. */
168 .tl-media-img:has(img.is-loading) { min-height: 220px; }
169
170 .tl-media-video, .tl-media-audio { width: 100%; margin: .75rem 0 0; border-radius: 12px; display: block; }
171 .tl-media-video { max-height: 480px; background: #000; }
172 .tl-poll { margin: .75rem 0 0; display: flex; flex-direction: column; gap: 8px; }
173 .tl-poll-form { display: flex; flex-direction: column; gap: 8px; }
174 .tl-poll-choice { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border: 1px solid var(--line, rgba(128,128,128,.3)); border-radius: 10px; cursor: pointer; }
175 .tl-poll-choice:hover { border-color: var(--accent); }
176 .tl-poll-choice input { accent-color: var(--accent); }
177 .tl-poll-btn { align-self: flex-start; margin-top: 2px; }
178 .tl-poll-res { position: relative; padding: 9px 12px; border-radius: 10px; overflow: hidden; background: var(--paper-2, rgba(128,128,128,.08)); display: flex; align-items: center; gap: 8px; }
179 .tl-poll-fill { position: absolute; inset: 0 auto 0 0; background: color-mix(in srgb, var(--accent) 22%, transparent); z-index: 0; }
180 .tl-poll-res.is-mine .tl-poll-fill { background: color-mix(in srgb, var(--accent) 40%, transparent); }
181 .tl-poll-name { position: relative; z-index: 1; flex: 1; font-size: .95rem; }
182 .tl-poll-pct { position: relative; z-index: 1; font-variant-numeric: tabular-nums; font-weight: 500; }
183 .tl-poll-foot { font-size: .82rem; color: var(--ink-soft, #888); }
184
185 .tl-embed { margin: .75rem 0 0; border-radius: 12px; overflow: hidden; background: var(--paper-2, #111); }
186 .tl-embed-frame { width: 100%; border: 0; display: block; aspect-ratio: 16 / 9; }
187 .tl-embed-spotify { aspect-ratio: auto; height: 152px; }
188 .tl-embed-sc { aspect-ratio: auto; height: 166px; }
189 .tl-embed-klonkt { aspect-ratio: auto; height: 180px; background: var(--paper, #fff); }
190 .tl-embed-bandcamp { aspect-ratio: auto; height: 470px; }
191 .tl-embed-apple { aspect-ratio: auto; height: 175px; }
192 .tl-embed-open { display: inline-block; margin: .45rem .9rem 0 0; font-size: .82rem; font-weight: 600; color: var(--accent); text-decoration: none; }
193 .tl-embed-open:hover { text-decoration: underline; }
194
195 .tl-orig { display: inline-block; margin: .7rem 0 0; font-size: .82rem; color: var(--accent, #06c); text-decoration: none; }
196 .tl-orig:hover { text-decoration: underline; }
197
198 /* Uniform circular action buttons with coloured icons (no emoji). */
199 .tl-actions { display: flex; gap: .5rem; margin-top: .9rem; align-items: center; }
200 .tl-act-form { margin: 0; display: inline-flex; }
201 .tl-act { width: 36px; height: 36px; flex: 0 0 36px; border-radius: 50%; padding: 0; box-sizing: border-box;
202 display: inline-flex; align-items: center; justify-content: center;
203 border: 1px solid color-mix(in srgb, var(--ink, #000) 12%, transparent);
204 background: color-mix(in srgb, var(--ink, #000) 3%, transparent);
205 color: var(--ink-soft, #888); cursor: pointer; -webkit-tap-highlight-color: transparent;
206 transition: background .12s, border-color .12s, transform .1s; }
207 .tl-act svg { width: 18px; height: 18px; display: block; }
208 .tl-act:active { transform: scale(.9); }
209 .tl-act-like svg { color: #e8b04b; }
210 .tl-act-like:hover { background: color-mix(in srgb, #e8b04b 15%, transparent); border-color: color-mix(in srgb, #e8b04b 50%, transparent); }
211 .tl-act-like.is-on { background: color-mix(in srgb, #e8b04b 18%, transparent); border-color: color-mix(in srgb, #e8b04b 60%, transparent); }
212 .tl-act-boost svg { color: #2fa85a; }
213 .tl-act-boost:hover { background: color-mix(in srgb, #2fa85a 15%, transparent); border-color: color-mix(in srgb, #2fa85a 50%, transparent); }
214 .tl-act-boost.is-on { background: color-mix(in srgb, #2fa85a 18%, transparent); border-color: color-mix(in srgb, #2fa85a 60%, transparent); }
215 .tl-act-reply svg { color: var(--accent, #06c); }
216 .tl-act-reply:hover { background: color-mix(in srgb, var(--accent, #888) 14%, transparent); border-color: color-mix(in srgb, var(--accent, #888) 50%, transparent); }
217 .tl-act-block svg { color: #e0524d; }
218 .tl-act-block:hover { background: color-mix(in srgb, #d9534f 14%, transparent); border-color: color-mix(in srgb, #d9534f 50%, transparent); }
219</style>
220
221<script>
222(function(){
223 if (window.__fediRemoteWired) return; window.__fediRemoteWired = true;
224 var current = null, currentBtn = null;
225 function close(){ if (current) { current.remove(); current = null; currentBtn = null; } }
226 function go(raw, uri){ var d=(raw||'').trim().replace(/^@?[^@\s]*@/,'').replace(/^https?:\/\//i,'').replace(/\/.*$/,'').trim(); if(d){ try{ localStorage.setItem('pcmsFediServer', d); }catch(e){} location.href='https://'+d+'/authorize_interaction?uri='+encodeURIComponent(uri||''); } }
227 function place(f, b){ var r=b.getBoundingClientRect(); f.style.top=(r.bottom+window.scrollY+6)+'px'; f.style.left=Math.max(8, Math.min(r.left+window.scrollX, window.scrollX+window.innerWidth-340))+'px'; }
228 document.addEventListener('click', function(e){
229 if (e.target.closest && e.target.closest('.fedi-remote-cancel')) { close(); return; }
230 if (current && e.target.closest && e.target.closest('.fedi-remote-form')) return;
231 var b = e.target.closest && e.target.closest('.fedi-remote-reply-btn');
232 if (b) {
233 e.preventDefault();
234 if (currentBtn === b) { close(); return; }
235 close();
236 var f = document.createElement('form'); f.className='fedi-remote-form'; f.dataset.uri = b.getAttribute('data-fedi-uri')||'';
237 f.innerHTML = '<input type="text" autocomplete="off" spellcheck="false"><button type="submit" class="btn btn-primary fedi-remote-go" aria-label="ok">&rarr;</button><button type="button" class="fedi-remote-cancel" aria-label="x">&times;</button>';
238 var _inp = f.querySelector('input'); _inp.placeholder = b.getAttribute('data-fedi-ph') || 'mastodon.social';
239 try{ var _sv = localStorage.getItem('pcmsFediServer'); if(_sv) _inp.value = _sv; }catch(e){}
240 document.body.appendChild(f); place(f, b); current=f; currentBtn=b; _inp.focus(); _inp.select();
241 return;
242 }
243 if (current) close();
244 });
245 document.addEventListener('submit', function(e){ var f=e.target.closest && e.target.closest('.fedi-remote-form'); if(!f) return; e.preventDefault(); go(f.querySelector('input').value, f.dataset.uri); });
246 document.addEventListener('keydown', function(e){ if (e.key === 'Escape') close(); });
247 window.addEventListener('scroll', close, true);
248})();
249
250/* Short feed videos (gif/cover loops like an animated cover) autoplay + loop muted like a GIF;
251 longer real videos keep their controls. Decided on the actual duration once metadata loads. */
252(function(){
253 document.querySelectorAll('.tl-media-video').forEach(function(v){
254 if (v.dataset.gifWired) return; v.dataset.gifWired = '1';
255 var decide = function(){
256 if (v.duration && v.duration <= 30) {
257 v.removeAttribute('controls'); v.loop = true; v.muted = true; v.play().catch(function(){});
258 }
259 };
260 if (v.readyState >= 1) decide(); else v.addEventListener('loadedmetadata', decide, { once: true });
261 });
262})();
263
264/* Like/Boost toggle in place — POST via fetch, flip the button, stay on the page (no reload, no banner). */
265(function(){
266 if (window.__newsReactWired) return; window.__newsReactWired = true;
267 document.addEventListener('submit', function(e){
268 var f = e.target.closest && e.target.closest('.tl-react-form');
269 if (!f) return;
270 e.preventDefault();
271 var btn = f.querySelector('button'); if (!btn || btn.disabled) return;
272 btn.disabled = true;
273 var body = new URLSearchParams();
274 new FormData(f).forEach(function(v, k){ body.append(k, v); });
275 fetch(f.action, { method: 'POST', body: body, headers: { 'X-Requested-With': 'fetch' }, credentials: 'same-origin' })
276 .then(function(r){ return r.ok ? r.json() : null; })
277 .then(function(j){ if (j) btn.classList.toggle('is-on', !!j.on); })
278 .catch(function(){})
279 .then(function(){ btn.disabled = false; });
280 });
281})();
282</script>
283
284<script>
285// Collapse long post bodies to a max height with a "read more" toggle — only when the content
286// actually overflows. Runs on every /news render (full load + htmx swap); a per-element flag
287// prevents double-wiring.
288(function(){
289 var RM = '<%= t("tl.read_more") %>', SL = '<%= t("tl.show_less") %>';
290 document.querySelectorAll('.tl-content:not(.nsfw-media):not([data-rm])').forEach(function(c){
291 c.setAttribute('data-rm', '1');
292 if (c.scrollHeight > 360) {
293 c.classList.add('tl-clamp');
294 var b = document.createElement('button');
295 b.type = 'button'; b.className = 'tl-readmore'; b.textContent = RM;
296 b.addEventListener('click', function(){
297 b.textContent = c.classList.toggle('tl-clamp') ? RM : SL;
298 });
299 c.insertAdjacentElement('afterend', b);
300 }
301 });
302})();
303</script>
Note: See TracBrowser for help on using the repository browser.