mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-04-16 11:17:20 +02:00
highlight search results
This commit is contained in:
parent
58e1f185b2
commit
3c44a93e34
3
dist/styles/cards.css
vendored
3
dist/styles/cards.css
vendored
@ -80,6 +80,9 @@
|
||||
left: 0;
|
||||
background: #0003;
|
||||
}
|
||||
.card span.h {
|
||||
background: #fce100b0;
|
||||
}
|
||||
|
||||
.default-card {
|
||||
display: inline-block;
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
)
|
||||
|
||||
|
51
src/components/cards/highlights.tsx
Normal file
51
src/components/cards/highlights.tsx
Normal 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
|
@ -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>
|
||||
)
|
||||
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
})
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user