Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
102 lines
3.2 KiB
JavaScript
102 lines
3.2 KiB
JavaScript
/* 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));
|