feat: repli des articles en mode lecture avec bouton Lire plus

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Vincent BourdonandClaude Opus 5.5 committed 2026-11-06 18:58:51 +01:00
1 parent d8b334767d
commit dcacf1acb1
5 files changed
+872

No files matched your search

+24
View File
@@ -0,0 +1,24 @@
/* Read More — collapsed articles in reader view.
* max-height only: the article must keep a non-zero height so that
* FreshRSS still marks it as read when scrolled past. */
.flux .text.read-more-collapsed {
max-height: var(--read-more-height, 250px);
overflow: hidden;
position: relative;
}
.flux .text.read-more-collapsed::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 4em;
background: linear-gradient(to bottom, transparent, var(--frss-background-color, #fff));
pointer-events: none;
}
.flux .read-more-btn {
display: block;
margin: 0.5em auto 1em;
}
+101
View File
@@ -0,0 +1,101 @@
/* Read More — FreshRSS extension.
* Collapses long articles in reader view behind a "Read more" button.
* Collapsing uses max-height (never display:none) so FreshRSS's
* mark-as-read-on-scroll keeps measuring a non-zero article height. */
(function (root) {
'use strict';
const THRESHOLD_RATIO = 1.5;
const STATE = { collapsed: 'collapsed', short: 'short', expanded: 'expanded' };
const defaultMeasure = (el) => el.scrollHeight;
function shouldCollapse(contentHeight, collapsedHeight) {
return contentHeight >= collapsedHeight * THRESHOLD_RATIO;
}
function expand(flux, text, button) {
text.classList.remove('read-more-collapsed');
text.style.removeProperty('--read-more-height');
button.remove();
flux.dataset.readMore = STATE.expanded;
}
function collapse(flux, text, opts) {
const doc = flux.ownerDocument;
text.classList.add('read-more-collapsed');
text.style.setProperty('--read-more-height', opts.collapsedHeight + 'px');
const button = doc.createElement('button');
button.type = 'button';
button.className = 'btn read-more-btn';
button.textContent = opts.label;
button.addEventListener('click', function (ev) {
ev.preventDefault();
ev.stopPropagation();
expand(flux, text, button);
});
text.after(button);
flux.dataset.readMore = STATE.collapsed;
}
function processEntry(flux, opts) {
const state = flux.dataset.readMore;
if (state !== undefined && state !== STATE.short) {
return;
}
const text = flux.querySelector('.text');
if (!text) {
return;
}
const measure = opts.measure || defaultMeasure;
if (shouldCollapse(measure(text), opts.collapsedHeight)) {
collapse(flux, text, opts);
} else {
flux.dataset.readMore = STATE.short;
}
}
function processEntries(rootNode, opts) {
rootNode.querySelectorAll('.flux').forEach((flux) => processEntry(flux, opts));
}
function init(win, measure) {
const context = win.context;
if (!context || context.current_view !== 'reader') {
return;
}
const conf = (context.extensions && context.extensions.read_more) || {};
const opts = {
collapsedHeight: Number(conf.collapsed_height) || 250,
label: conf.label || 'Read more',
measure: measure,
};
const doc = win.document;
processEntries(doc, opts);
doc.body.addEventListener('freshrss:load-more', () => processEntries(doc, opts));
// Late-loading images can make a "short" article long enough to collapse.
doc.addEventListener('load', function (ev) {
const target = ev.target;
if (!target || target.tagName !== 'IMG' || !target.closest) {
return;
}
const flux = target.closest('.flux[data-read-more="short"]');
if (flux) {
processEntry(flux, opts);
}
}, true);
}
const api = { THRESHOLD_RATIO, shouldCollapse, processEntries, init };
root.ReadMore = api;
if (typeof module !== 'undefined' && module.exports) {
module.exports = api;
}
if (root.document && root.document.addEventListener) {
if (typeof root.context !== 'undefined' && typeof root.context.extensions !== 'undefined') {
init(root);
} else {
root.document.addEventListener('freshrss:globalContextLoaded', () => init(root), false);
}
}
}(typeof window !== 'undefined' ? window : globalThis));