fix: FeedLand widget click-to-expand and dark mode for expanded items
Add row selection and expand/collapse behavior matching Dave Winer's blogroll.js: first click selects, second click (or caret click) expands to show up to 5 recent items fetched from blogroll API. Items cached after first fetch. Added dark mode styles for expanded items.
This commit is contained in:
@@ -86,6 +86,7 @@
|
|||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
width: 12px;
|
width: 12px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.trBlogrollFeed .tdBlogrollFeedTitle {
|
.trBlogrollFeed .tdBlogrollFeedTitle {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
@@ -117,12 +118,46 @@
|
|||||||
.trBlogrollFeed:hover {
|
.trBlogrollFeed:hover {
|
||||||
background-color: whitesmoke;
|
background-color: whitesmoke;
|
||||||
}
|
}
|
||||||
|
.trBlogrollFeed.selectedFeed {
|
||||||
|
background-color: #e8f0fe;
|
||||||
|
}
|
||||||
.darkCaretColor {
|
.darkCaretColor {
|
||||||
opacity: .9;
|
opacity: .9;
|
||||||
}
|
}
|
||||||
.lightCaretColor {
|
.lightCaretColor {
|
||||||
opacity: .2;
|
opacity: .2;
|
||||||
}
|
}
|
||||||
|
/* Expanded items (newspod) */
|
||||||
|
.divNewsPod {
|
||||||
|
padding: 2px 0 4px 16px;
|
||||||
|
}
|
||||||
|
.divNewsPod ul {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.divNewsPod li {
|
||||||
|
padding: 2px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.divNewsPod li a {
|
||||||
|
color: #1a73e8;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.divNewsPod li a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.divNewsPod li .spItemWhen {
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
.divNewsPod .feedLoading {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.5;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
.divBlogrollFooter {
|
.divBlogrollFooter {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -163,21 +198,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Dark mode adjustments */
|
/* Dark mode adjustments */
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.dark .divBlogrollContainer {
|
|
||||||
border-color: #444;
|
|
||||||
color: #e0e0e0;
|
|
||||||
}
|
|
||||||
.dark .divBlogrollContainer:focus {
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
}
|
|
||||||
.dark .trBlogrollFeed:hover {
|
|
||||||
background-color: #2a2a2a;
|
|
||||||
}
|
|
||||||
.dark .divBlogrollFooter {
|
|
||||||
border-top-color: #444;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.dark .divBlogrollContainer {
|
.dark .divBlogrollContainer {
|
||||||
border-color: #444;
|
border-color: #444;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
@@ -188,6 +208,12 @@
|
|||||||
.dark .trBlogrollFeed:hover {
|
.dark .trBlogrollFeed:hover {
|
||||||
background-color: #2a2a2a;
|
background-color: #2a2a2a;
|
||||||
}
|
}
|
||||||
|
.dark .trBlogrollFeed.selectedFeed {
|
||||||
|
background-color: #1e3a5f;
|
||||||
|
}
|
||||||
|
.dark .divNewsPod li a {
|
||||||
|
color: #8ab4f8;
|
||||||
|
}
|
||||||
.dark .divBlogrollFooter {
|
.dark .divBlogrollFooter {
|
||||||
border-top-color: #444;
|
border-top-color: #444;
|
||||||
}
|
}
|
||||||
@@ -219,15 +245,46 @@
|
|||||||
<table class="divBlogrollTable">
|
<table class="divBlogrollTable">
|
||||||
<tbody>
|
<tbody>
|
||||||
<template x-for="blog in sortedBlogs" :key="blog.id">
|
<template x-for="blog in sortedBlogs" :key="blog.id">
|
||||||
<tr class="trBlogrollFeed">
|
<tr>
|
||||||
<td class="tdBlogrollWedge">
|
{# Feed row #}
|
||||||
<span class="darkCaretColor">▶</span>
|
<td colspan="2" style="padding:0; border:0;">
|
||||||
</td>
|
<div class="trBlogrollFeed"
|
||||||
<td class="tdBlogrollFeedTitle">
|
:class="selectedId === blog.id ? 'selectedFeed' : ''"
|
||||||
<span class="spWhenUpdated" x-text="relativeTime(blog.lastFetchAt)"></span>
|
style="display:flex; align-items:flex-start;">
|
||||||
<span class="spTitleString">
|
<div class="tdBlogrollWedge"
|
||||||
<a :href="blog.siteUrl || blog.feedUrl" target="_blank" rel="noopener" x-text="blog.title"></a>
|
@click="toggleExpand(blog)">
|
||||||
</span>
|
<span :class="expandedId === blog.id ? 'darkCaretColor' : (selectedId === blog.id ? 'darkCaretColor' : 'lightCaretColor')"
|
||||||
|
x-text="expandedId === blog.id ? '\u25BC' : '\u25B6'"></span>
|
||||||
|
</div>
|
||||||
|
<div class="tdBlogrollFeedTitle" style="flex:1; min-width:0;"
|
||||||
|
@click="handleRowClick(blog)">
|
||||||
|
<span class="spWhenUpdated" x-text="relativeTime(blog.lastFetchAt)"></span>
|
||||||
|
<span class="spTitleString">
|
||||||
|
<a :href="blog.siteUrl || blog.feedUrl" target="_blank" rel="noopener"
|
||||||
|
x-text="blog.title" @click.stop></a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{# Expanded items #}
|
||||||
|
<div class="divNewsPod" x-show="expandedId === blog.id" x-collapse>
|
||||||
|
<template x-if="blog._loadingItems">
|
||||||
|
<div class="feedLoading">Loading…</div>
|
||||||
|
</template>
|
||||||
|
<template x-if="!blog._loadingItems && blog._items && blog._items.length > 0">
|
||||||
|
<ul>
|
||||||
|
<template x-for="item in blog._items" :key="item.id">
|
||||||
|
<li>
|
||||||
|
<a :href="item.url" target="_blank" rel="noopener"
|
||||||
|
x-text="truncate(item.title || item.summary || item.url, 100)"></a>
|
||||||
|
<span class="spItemWhen" x-text="relativeTime(item.published)"></span>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
<template x-if="!blog._loadingItems && (!blog._items || blog._items.length === 0)">
|
||||||
|
<div class="feedLoading">No recent items</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
@@ -250,6 +307,8 @@ function feedlandWidget() {
|
|||||||
title: 'FeedLand',
|
title: 'FeedLand',
|
||||||
riverUrl: 'https://feedland.com',
|
riverUrl: 'https://feedland.com',
|
||||||
loading: true,
|
loading: true,
|
||||||
|
selectedId: null,
|
||||||
|
expandedId: null,
|
||||||
|
|
||||||
get sortedBlogs() {
|
get sortedBlogs() {
|
||||||
const sorted = [...this.blogs];
|
const sorted = [...this.blogs];
|
||||||
@@ -265,6 +324,45 @@ function feedlandWidget() {
|
|||||||
return sorted;
|
return sorted;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleRowClick(blog) {
|
||||||
|
if (this.selectedId !== blog.id) {
|
||||||
|
// First click: select
|
||||||
|
this.selectedId = blog.id;
|
||||||
|
} else {
|
||||||
|
// Second click: toggle expand
|
||||||
|
this.toggleExpand(blog);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async toggleExpand(blog) {
|
||||||
|
this.selectedId = blog.id;
|
||||||
|
if (this.expandedId === blog.id) {
|
||||||
|
// Collapse
|
||||||
|
this.expandedId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Expand — fetch items if not cached
|
||||||
|
this.expandedId = blog.id;
|
||||||
|
if (!blog._items) {
|
||||||
|
blog._loadingItems = true;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/blogrollapi/api/blogs/' + blog.id);
|
||||||
|
const data = await res.json();
|
||||||
|
blog._items = (data.items || []).slice(0, 5);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('FeedLand: failed to load items for', blog.title, err);
|
||||||
|
blog._items = [];
|
||||||
|
} finally {
|
||||||
|
blog._loadingItems = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
truncate(str, max) {
|
||||||
|
if (!str) return '';
|
||||||
|
return str.length > max ? str.slice(0, max) + '…' : str;
|
||||||
|
},
|
||||||
|
|
||||||
relativeTime(iso) {
|
relativeTime(iso) {
|
||||||
if (!iso) return '';
|
if (!iso) return '';
|
||||||
const diff = Date.now() - new Date(iso).getTime();
|
const diff = Date.now() - new Date(iso).getTime();
|
||||||
@@ -280,7 +378,11 @@ function feedlandWidget() {
|
|||||||
try {
|
try {
|
||||||
const res = await fetch('/blogrollapi/api/blogs?source=feedland&sort=recent&limit=100');
|
const res = await fetch('/blogrollapi/api/blogs?source=feedland&sort=recent&limit=100');
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
this.blogs = data.items || [];
|
this.blogs = (data.items || []).map(b => ({
|
||||||
|
...b,
|
||||||
|
_items: null,
|
||||||
|
_loadingItems: false,
|
||||||
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('FeedLand widget error:', err);
|
console.error('FeedLand widget error:', err);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user