实现收藏文本长文本折叠/展开/复制、收藏搜索关键词高亮
需求7.4两处此前完全没接:文本收藏是不限高度的纯<p>,长文本不折叠、 没有展开/收起,也没有复制按钮;收藏搜索的query从未传到文本收藏这一 层,History那边已有的Highlight组件在Favorites里从未被引用(实测: 搜索一个真实命中的关键词,结果列表0个<mark>)。 新增FavoriteText组件:用scrollHeight/clientHeight判断是否真的溢出 (避免给短文本也显示无用的"展开"按钮),复制走和context-menu.jsx 的copyMessageText一致的navigator.clipboard.writeText。 -webkit-line-clamp在当前环境对这个<p>不生效(getComputedStyle能读到 设置值但视觉不裁切),改用line-height反推的max-height+overflow:hidden; 外层容器不能用display:flex,会被UA blockify成flow-root导致同样失效。 第十二轮最小粒度测试 Ch7 发现,已用真实浏览器验证展开/收起切换正常、 搜索关键词高亮正确命中。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
cccd21aff1
commit
6882103461
@@ -10986,6 +10986,52 @@ div.toast.show {
|
|||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 需求7.4"长文本折叠、展开和复制":clamped 限定默认显示高度,展开态(FavoriteText 的
|
||||||
|
expanded=true) 去掉这个 class 即可显示全文;library-text-actions 放"展开/收起"(仅溢出
|
||||||
|
时才渲染)和"复制"两个按钮,颜色沿用应用里其它文字按钮的强调色。
|
||||||
|
没有用 -webkit-line-clamp:实测这个环境里它对这个 <p> 不生效——getComputedStyle 能读到
|
||||||
|
display/-webkit-line-clamp/overflow 都是设置的值,但视觉上完全不裁切,scrollHeight 也跟
|
||||||
|
clientHeight 相等,是那套盒模型本身在当前渲染环境下没有实际生效,不是 CSS 写错了。改用
|
||||||
|
.library-text-content 已有的 line-height:1.65 反推的 max-height(5 行 = 5*1.65em)+
|
||||||
|
overflow:hidden,是普通块级盒子,不依赖任何厂商前缀的特殊渲染路径,行为可预期。 */
|
||||||
|
.library-text-content.clamped {
|
||||||
|
max-height: 8.25em;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* library-text-wrap 故意不用 display:flex——曾经这样写过,结果 .clamped 的 <p> 变成了 flex
|
||||||
|
item:flex/grid item 的 specified display 会被 UA 按 CSS Display 规范"blockify",实测这里的
|
||||||
|
-webkit-box 被换成了 flow-root(getComputedStyle 验证过),-webkit-line-clamp 整个失效,折叠
|
||||||
|
彻底不生效。改回普通块级 + margin 做间距,让 .clamped 保持自己声明的 -webkit-box 不被更外层
|
||||||
|
的 flex 容器改写。 */
|
||||||
|
.library-text-wrap {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-text-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.1rem;
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-text-actions button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.2rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #3b82c4;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-text-actions button .material-icons {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.library-file-card {
|
.library-file-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 3rem minmax(0, 1fr) auto;
|
grid-template-columns: 3rem minmax(0, 1fr) auto;
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -187,6 +187,33 @@ function LinkText({url, onOpen, removed}) {
|
|||||||
return <button type="button" className="library-link-text" onClick={() => onOpen && onOpen(url)}><Icon>link</Icon><span>{url}</span></button>;
|
return <button type="button" className="library-link-text" onClick={() => onOpen && onOpen(url)}><Icon>link</Icon><span>{url}</span></button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 需求7.4「支持长文本折叠、展开和复制」外加"搜索结果应对匹配关键词进行高亮显示"——原实现整段
|
||||||
|
// 需求在这里都没接:文本收藏是一段不限高度的纯 <p>,长文本不折叠也没有展开/收起,没有复制按钮,
|
||||||
|
// 收藏搜索的 query 也从没传到这一层,History 那边已经在用的 Highlight 组件在 Favorites 里从未
|
||||||
|
// 被引用过(实测:在"我的收藏-文本"搜索一个真实命中的关键词,结果列表 0 个 <mark>)。
|
||||||
|
// overflowing 用 scrollHeight/clientHeight 判断 line-clamp 是否真的裁到了内容(标准判定手法),
|
||||||
|
// 避免给短文本也显示一个没用的"展开"按钮;复制走跟 context-menu.jsx 的 copyMessageText 完全一样
|
||||||
|
// 的 navigator.clipboard.writeText,保持与聊天消息长按菜单里"复制"一致的行为。
|
||||||
|
function FavoriteText({text, query, onToast}) {
|
||||||
|
const [expanded, setExpanded] = React.useState(false);
|
||||||
|
const [overflowing, setOverflowing] = React.useState(false);
|
||||||
|
const ref = React.useRef(null);
|
||||||
|
React.useEffect(() => {
|
||||||
|
const el = ref.current;
|
||||||
|
if (el) setOverflowing(el.scrollHeight - el.clientHeight > 1);
|
||||||
|
}, [text]);
|
||||||
|
const copy = () => {
|
||||||
|
navigator.clipboard.writeText(text || '').then(() => onToast('已复制')).catch(() => onToast('复制失败'));
|
||||||
|
};
|
||||||
|
return <div className="library-text-wrap">
|
||||||
|
<p ref={ref} className={'library-text-content' + (expanded ? '' : ' clamped')}><Highlight text={text} query={query} /></p>
|
||||||
|
<div className="library-text-actions">
|
||||||
|
{overflowing ? <button type="button" onClick={() => setExpanded(v => !v)}>{expanded ? '收起' : '展开'}</button> : null}
|
||||||
|
<button type="button" onClick={copy}><Icon>content_copy</Icon>复制</button>
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
function MediaThumb({url, poster, video, duration, onOpen, removed}) {
|
function MediaThumb({url, poster, video, duration, onOpen, removed}) {
|
||||||
if (removed) {
|
if (removed) {
|
||||||
return <div className="library-media-thumb removed"><Icon>{video ? 'videocam_off' : 'broken_image'}</Icon><em>{video ? '视频已失效' : '图片已失效'}</em></div>;
|
return <div className="library-media-thumb removed"><Icon>{video ? 'videocam_off' : 'broken_image'}</Icon><em>{video ? '视频已失效' : '图片已失效'}</em></div>;
|
||||||
@@ -202,7 +229,7 @@ function MediaThumb({url, poster, video, duration, onOpen, removed}) {
|
|||||||
</button>;
|
</button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FavoriteItem({item, onOpen, selecting, selected, onSelect}) {
|
function FavoriteItem({item, onOpen, selecting, selected, onSelect, query, onToast}) {
|
||||||
const att = firstAttachment(item.snapshot && item.snapshot.content);
|
const att = firstAttachment(item.snapshot && item.snapshot.content);
|
||||||
const source = item.source_display || (item.source && item.source.topic) || '';
|
const source = item.source_display || (item.source && item.source.topic) || '';
|
||||||
return <article className={'library-favorite-item' + (selecting ? ' selecting' : '')}>
|
return <article className={'library-favorite-item' + (selecting ? ' selecting' : '')}>
|
||||||
@@ -216,7 +243,7 @@ function FavoriteItem({item, onOpen, selecting, selected, onSelect}) {
|
|||||||
统一都传。后端 messageAvailability(im_http.go)在源消息被删/被撤回时给 'removed'。 */}
|
统一都传。后端 messageAvailability(im_http.go)在源消息被删/被撤回时给 'removed'。 */}
|
||||||
{item.type === 'text' ? (item.availability === 'removed' ?
|
{item.type === 'text' ? (item.availability === 'removed' ?
|
||||||
<p className="library-text-content removed"><Icon>block</Icon><em>文本已失效</em></p> :
|
<p className="library-text-content removed"><Icon>block</Icon><em>文本已失效</em></p> :
|
||||||
<p className="library-text-content">{plainTextOf(item.snapshot && item.snapshot.content, item.snapshot && item.snapshot.text)}</p>) : null}
|
<FavoriteText text={plainTextOf(item.snapshot && item.snapshot.content, item.snapshot && item.snapshot.text)} query={query} onToast={onToast} />) : null}
|
||||||
{item.type === 'file' ? <FileCard name={att && att.name} size={att && Drafty.getEntitySize(att)} mime={att && att.mime}
|
{item.type === 'file' ? <FileCard name={att && att.name} size={att && Drafty.getEntitySize(att)} mime={att && att.mime}
|
||||||
removed={item.availability === 'removed'} onOpen={onOpen} /> : null}
|
removed={item.availability === 'removed'} onOpen={onOpen} /> : null}
|
||||||
{item.type === 'voice' ? <VoiceCard att={att} head={item.snapshot && item.snapshot.head} removed={item.availability === 'removed'} /> : null}
|
{item.type === 'voice' ? <VoiceCard att={att} head={item.snapshot && item.snapshot.head} removed={item.availability === 'removed'} /> : null}
|
||||||
@@ -335,7 +362,7 @@ function Favorites({view, tinode, onToast}) {
|
|||||||
list.length ? <>{list.map(item => <FavoriteItem key={item.favorite_id} item={{...item, _thumbUrl: fileUrls[item.favorite_id],
|
list.length ? <>{list.map(item => <FavoriteItem key={item.favorite_id} item={{...item, _thumbUrl: fileUrls[item.favorite_id],
|
||||||
source_display: sourceNames[item.source && item.source.topic]}}
|
source_display: sourceNames[item.source && item.source.topic]}}
|
||||||
selecting={selecting} selected={selected.includes(item.favorite_id)} onSelect={() => toggle(item.favorite_id)}
|
selecting={selecting} selected={selected.includes(item.favorite_id)} onSelect={() => toggle(item.favorite_id)}
|
||||||
onOpen={() => openItem(item)} />)}
|
onOpen={() => openItem(item)} query={query} onToast={onToast} />)}
|
||||||
{hasMore ? <button type="button" className="library-load-more" onClick={() => load(true)}>加载更多</button> : null}</> :
|
{hasMore ? <button type="button" className="library-load-more" onClick={() => load(true)}>加载更多</button> : null}</> :
|
||||||
<div className="library-empty"><Icon>bookmark_border</Icon><strong>暂无此类收藏</strong></div>}
|
<div className="library-empty"><Icon>bookmark_border</Icon><strong>暂无此类收藏</strong></div>}
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user