highlight search results

This commit is contained in:
刘浩远 2020-07-23 11:53:37 +08:00
parent 58e1f185b2
commit 3c44a93e34
12 changed files with 91 additions and 27 deletions

View File

@ -80,6 +80,9 @@
left: 0;
background: #0003;
}
.card span.h {
background: #fce100b0;
}
.default-card {
display: inline-block;

View File

@ -7,18 +7,25 @@ export namespace Card {
feedId: string
item: RSSItem
source: RSSSource
keyword: string
shortcuts: (item: RSSItem, key: string) => void
markRead: (item: RSSItem) => void
contextMenu: (feedId: string, item: RSSItem, e) => void
showItem: (fid: string, item: RSSItem) => void
}
export const openInBrowser = (props: Props) => {
const openInBrowser = (props: Props) => {
props.markRead(props.item)
window.utils.openExternal(props.item.link)
}
export const onClick = (props: Props, e: React.MouseEvent) => {
export const bindEventsToProps = (props: Props) => ({
onClick: (e: React.MouseEvent) => onClick(props, e),
onMouseUp: (e: React.MouseEvent) => onMouseUp(props, e),
onKeyDown: (e: React.KeyboardEvent) => onKeyDown(props, e),
})
const onClick = (props: Props, e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
switch (props.source.openTarget) {
@ -35,7 +42,7 @@ export namespace Card {
}
}
export const onMouseUp = (props: Props, e: React.MouseEvent) => {
const onMouseUp = (props: Props, e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
switch (e.button) {
@ -47,7 +54,7 @@ export namespace Card {
}
}
export const onKeyDown = (props: Props, e: React.KeyboardEvent) => {
const onKeyDown = (props: Props, e: React.KeyboardEvent) => {
props.shortcuts(props.item, e.key)
}
}

View File

@ -2,6 +2,7 @@ import * as React from "react"
import { Card } from "./card"
import CardInfo from "./info"
import Time from "../utils/time"
import Highlights from "./highlights"
const className = (props: Card.Props) => {
let cn = ["card", "compact-card"]
@ -12,15 +13,13 @@ const className = (props: Card.Props) => {
const CompactCard: React.FunctionComponent<Card.Props> = (props) => (
<div
className={className(props)}
onClick={e => Card.onClick(props, e)}
onMouseUp={e => Card.onMouseUp(props, e)}
onKeyDown={e => Card.onKeyDown(props, e)}
{...Card.bindEventsToProps(props)}
data-iid={props.item._id}
data-is-focusable>
<CardInfo source={props.source} item={props.item} hideTime />
<div className="data">
<span className="title">{props.item.title}</span>
<span className={"snippet" + (props.item.thumb ? "" : " show")}>{props.item.snippet.slice(0, 325)}</span>
<span className="title"><Highlights text={props.item.title} keyword={props.keyword} title /></span>
<span className="snippet"><Highlights text={props.item.snippet} keyword={props.keyword} /></span>
</div>
<Time date={props.item.date} />
</div>

View File

@ -1,6 +1,7 @@
import * as React from "react"
import { Card } from "./card"
import CardInfo from "./info"
import Highlights from "./highlights"
const className = (props: Card.Props) => {
let cn = ["card", "default-card"]
@ -12,9 +13,7 @@ const className = (props: Card.Props) => {
const DefaultCard: React.FunctionComponent<Card.Props> = (props) => (
<div
className={className(props)}
onClick={e => Card.onClick(props, e)}
onMouseUp={e => Card.onMouseUp(props, e)}
onKeyDown={e => Card.onKeyDown(props, e)}
{...Card.bindEventsToProps(props)}
data-iid={props.item._id}
data-is-focusable>
{props.item.thumb ? (
@ -25,8 +24,10 @@ const DefaultCard: React.FunctionComponent<Card.Props> = (props) => (
<img className="head" src={props.item.thumb} />
) : null}
<CardInfo source={props.source} item={props.item} />
<h3 className="title">{props.item.title}</h3>
<p className={"snippet" + (props.item.thumb ? "" : " show")}>{props.item.snippet.slice(0, 325)}</p>
<h3 className="title"><Highlights text={props.item.title} keyword={props.keyword} title /></h3>
<p className={"snippet" + (props.item.thumb ? "" : " show")}>
<Highlights text={props.item.snippet} keyword={props.keyword} />
</p>
</div>
)

View File

@ -0,0 +1,51 @@
import * as React from "react"
import { validateRegex } from "../../scripts/utils"
type HighlightsProps = {
text: string
keyword: string
title?: boolean
}
const Highlights: React.FunctionComponent<HighlightsProps> = (props) => {
const spans: [string, boolean][] = new Array()
let regex: RegExp
if (props.keyword === "" || !(regex = validateRegex(props.keyword, "g"))) {
if (props.title) spans.push([props.text, false])
else spans.push([props.text.substr(0, 325), false])
} else if (props.title) {
let match: RegExpExecArray
do {
const startIndex = regex.lastIndex
match = regex.exec(props.text)
if (match) {
if (startIndex != match.index) {
spans.push([props.text.substring(startIndex, match.index), false])
}
spans.push([match[0], true])
} else {
spans.push([props.text.substr(startIndex), false])
}
} while (match && regex.lastIndex < props.text.length)
} else {
const match = regex.exec(props.text)
if (match) {
if (match.index != 0) {
const startIndex = Math.max(match.index - 25, props.text.lastIndexOf(" ", Math.max(match.index - 10, 0)))
spans.push([props.text.substring(Math.max(0, startIndex), match.index), false])
}
spans.push([match[0], true])
if (regex.lastIndex < props.text.length) {
spans.push([props.text.substr(regex.lastIndex, 300), false])
}
} else {
spans.push([props.text.substr(0, 325), false])
}
}
return <>
{spans.map(([text, flag]) => flag ? <span className="h">{text}</span> : text)}
</>
}
export default Highlights

View File

@ -1,6 +1,7 @@
import * as React from "react"
import { Card } from "./card"
import CardInfo from "./info"
import Highlights from "./highlights"
const className = (props: Card.Props) => {
let cn = ["card", "list-card"]
@ -11,9 +12,7 @@ const className = (props: Card.Props) => {
const ListCard: React.FunctionComponent<Card.Props> = (props) => (
<div
className={className(props)}
onClick={e => Card.onClick(props, e)}
onMouseUp={e => Card.onMouseUp(props, e)}
onKeyDown={e => Card.onKeyDown(props, e)}
{...Card.bindEventsToProps(props)}
data-iid={props.item._id}
data-is-focusable>
{props.item.thumb ? (
@ -21,7 +20,7 @@ const ListCard: React.FunctionComponent<Card.Props> = (props) => (
) : null}
<div className="data">
<CardInfo source={props.source} item={props.item} />
<h3 className="title">{props.item.title}</h3>
<h3 className="title"><Highlights text={props.item.title} keyword={props.keyword} title /></h3>
</div>
</div>
)

View File

@ -1,6 +1,7 @@
import * as React from "react"
import { Card } from "./card"
import CardInfo from "./info"
import Highlights from "./highlights"
const className = (props: Card.Props) => {
let cn = ["card", "magazine-card"]
@ -12,9 +13,7 @@ const className = (props: Card.Props) => {
const MagazineCard: React.FunctionComponent<Card.Props> = (props) => (
<div
className={className(props)}
onClick={e => Card.onClick(props, e)}
onMouseUp={e => Card.onMouseUp(props, e)}
onKeyDown={e => Card.onKeyDown(props, e)}
{...Card.bindEventsToProps(props)}
data-iid={props.item._id}
data-is-focusable>
{props.item.thumb ? (
@ -22,8 +21,8 @@ const MagazineCard: React.FunctionComponent<Card.Props> = (props) => (
) : null}
<div className="data">
<div>
<h3 className="title">{props.item.title}</h3>
<p className={"snippet" + (props.item.thumb ? "" : " show")}>{props.item.snippet.slice(0, 325)}</p>
<h3 className="title"><Highlights text={props.item.title} keyword={props.keyword} title /></h3>
<p className="snippet"><Highlights text={props.item.snippet} keyword={props.keyword} /></p>
</div>
<CardInfo source={props.source} item={props.item} />
</div>

View File

@ -47,6 +47,7 @@ class CardsFeed extends React.Component<FeedProps> {
key={item._id}
item={item}
source={this.props.sourceMap[item.source]}
keyword={this.props.keyword}
shortcuts={this.props.shortcuts}
markRead={this.props.markRead}
contextMenu={this.props.contextMenu}

View File

@ -11,6 +11,7 @@ export type FeedProps = FeedReduxProps & {
viewType: ViewType
items: RSSItem[]
sourceMap: Object
keyword: string
shortcuts: (item: RSSItem, key: string) => void
markRead: (item: RSSItem) => void
contextMenu: (feedId: string, item: RSSItem, e) => void

View File

@ -16,6 +16,7 @@ class ListFeed extends React.Component<FeedProps> {
key: item._id,
item: item,
source: this.props.sourceMap[item.source],
keyword: this.props.keyword,
shortcuts: this.props.shortcuts,
markRead: this.props.markRead,
contextMenu: this.props.contextMenu,

View File

@ -16,15 +16,17 @@ interface FeedContainerProps {
const getSources = (state: RootState) => state.sources
const getItems = (state: RootState) => state.items
const getFeed = (state: RootState, props: FeedContainerProps) => state.feeds[props.feedId]
const getKeyword = (state: RootState) => state.page.filter.search
const getView = (_, props: FeedContainerProps) => props.viewType
const makeMapStateToProps = () => {
return createSelector(
[getSources, getItems, getFeed, getView],
(sources, items, feed, viewType) => ({
[getSources, getItems, getFeed, getView, getKeyword],
(sources, items, feed, viewType, keyword) => ({
feed: feed,
items: feed.iids.map(iid => items[iid]),
sourceMap: sources,
keyword: keyword,
viewType: viewType
})
)

View File

@ -182,9 +182,9 @@ export function calculateItemSize(): Promise<number> {
})
}
export function validateRegex(regex: string): RegExp {
export function validateRegex(regex: string, flags = ""): RegExp {
try {
return new RegExp(regex)
return new RegExp(regex, flags)
} catch {
return null
}