mirror of
https://github.com/yang991178/fluent-reader.git
synced 2025-04-25 15:38:49 +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;
|
left: 0;
|
||||||
background: #0003;
|
background: #0003;
|
||||||
}
|
}
|
||||||
|
.card span.h {
|
||||||
|
background: #fce100b0;
|
||||||
|
}
|
||||||
|
|
||||||
.default-card {
|
.default-card {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -7,18 +7,25 @@ export namespace Card {
|
|||||||
feedId: string
|
feedId: string
|
||||||
item: RSSItem
|
item: RSSItem
|
||||||
source: RSSSource
|
source: RSSSource
|
||||||
|
keyword: string
|
||||||
shortcuts: (item: RSSItem, key: string) => void
|
shortcuts: (item: RSSItem, key: string) => void
|
||||||
markRead: (item: RSSItem) => void
|
markRead: (item: RSSItem) => void
|
||||||
contextMenu: (feedId: string, item: RSSItem, e) => void
|
contextMenu: (feedId: string, item: RSSItem, e) => void
|
||||||
showItem: (fid: string, item: RSSItem) => void
|
showItem: (fid: string, item: RSSItem) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const openInBrowser = (props: Props) => {
|
const openInBrowser = (props: Props) => {
|
||||||
props.markRead(props.item)
|
props.markRead(props.item)
|
||||||
window.utils.openExternal(props.item.link)
|
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.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
switch (props.source.openTarget) {
|
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.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
switch (e.button) {
|
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)
|
props.shortcuts(props.item, e.key)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,6 +2,7 @@ import * as React from "react"
|
|||||||
import { Card } from "./card"
|
import { Card } from "./card"
|
||||||
import CardInfo from "./info"
|
import CardInfo from "./info"
|
||||||
import Time from "../utils/time"
|
import Time from "../utils/time"
|
||||||
|
import Highlights from "./highlights"
|
||||||
|
|
||||||
const className = (props: Card.Props) => {
|
const className = (props: Card.Props) => {
|
||||||
let cn = ["card", "compact-card"]
|
let cn = ["card", "compact-card"]
|
||||||
@ -12,15 +13,13 @@ const className = (props: Card.Props) => {
|
|||||||
const CompactCard: React.FunctionComponent<Card.Props> = (props) => (
|
const CompactCard: React.FunctionComponent<Card.Props> = (props) => (
|
||||||
<div
|
<div
|
||||||
className={className(props)}
|
className={className(props)}
|
||||||
onClick={e => Card.onClick(props, e)}
|
{...Card.bindEventsToProps(props)}
|
||||||
onMouseUp={e => Card.onMouseUp(props, e)}
|
|
||||||
onKeyDown={e => Card.onKeyDown(props, e)}
|
|
||||||
data-iid={props.item._id}
|
data-iid={props.item._id}
|
||||||
data-is-focusable>
|
data-is-focusable>
|
||||||
<CardInfo source={props.source} item={props.item} hideTime />
|
<CardInfo source={props.source} item={props.item} hideTime />
|
||||||
<div className="data">
|
<div className="data">
|
||||||
<span className="title">{props.item.title}</span>
|
<span className="title"><Highlights text={props.item.title} keyword={props.keyword} title /></span>
|
||||||
<span className={"snippet" + (props.item.thumb ? "" : " show")}>{props.item.snippet.slice(0, 325)}</span>
|
<span className="snippet"><Highlights text={props.item.snippet} keyword={props.keyword} /></span>
|
||||||
</div>
|
</div>
|
||||||
<Time date={props.item.date} />
|
<Time date={props.item.date} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Card } from "./card"
|
import { Card } from "./card"
|
||||||
import CardInfo from "./info"
|
import CardInfo from "./info"
|
||||||
|
import Highlights from "./highlights"
|
||||||
|
|
||||||
const className = (props: Card.Props) => {
|
const className = (props: Card.Props) => {
|
||||||
let cn = ["card", "default-card"]
|
let cn = ["card", "default-card"]
|
||||||
@ -12,9 +13,7 @@ const className = (props: Card.Props) => {
|
|||||||
const DefaultCard: React.FunctionComponent<Card.Props> = (props) => (
|
const DefaultCard: React.FunctionComponent<Card.Props> = (props) => (
|
||||||
<div
|
<div
|
||||||
className={className(props)}
|
className={className(props)}
|
||||||
onClick={e => Card.onClick(props, e)}
|
{...Card.bindEventsToProps(props)}
|
||||||
onMouseUp={e => Card.onMouseUp(props, e)}
|
|
||||||
onKeyDown={e => Card.onKeyDown(props, e)}
|
|
||||||
data-iid={props.item._id}
|
data-iid={props.item._id}
|
||||||
data-is-focusable>
|
data-is-focusable>
|
||||||
{props.item.thumb ? (
|
{props.item.thumb ? (
|
||||||
@ -25,8 +24,10 @@ const DefaultCard: React.FunctionComponent<Card.Props> = (props) => (
|
|||||||
<img className="head" src={props.item.thumb} />
|
<img className="head" src={props.item.thumb} />
|
||||||
) : null}
|
) : null}
|
||||||
<CardInfo source={props.source} item={props.item} />
|
<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>
|
||||||
<p className={"snippet" + (props.item.thumb ? "" : " show")}>{props.item.snippet.slice(0, 325)}</p>
|
<p className={"snippet" + (props.item.thumb ? "" : " show")}>
|
||||||
|
<Highlights text={props.item.snippet} keyword={props.keyword} />
|
||||||
|
</p>
|
||||||
</div>
|
</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 * as React from "react"
|
||||||
import { Card } from "./card"
|
import { Card } from "./card"
|
||||||
import CardInfo from "./info"
|
import CardInfo from "./info"
|
||||||
|
import Highlights from "./highlights"
|
||||||
|
|
||||||
const className = (props: Card.Props) => {
|
const className = (props: Card.Props) => {
|
||||||
let cn = ["card", "list-card"]
|
let cn = ["card", "list-card"]
|
||||||
@ -11,9 +12,7 @@ const className = (props: Card.Props) => {
|
|||||||
const ListCard: React.FunctionComponent<Card.Props> = (props) => (
|
const ListCard: React.FunctionComponent<Card.Props> = (props) => (
|
||||||
<div
|
<div
|
||||||
className={className(props)}
|
className={className(props)}
|
||||||
onClick={e => Card.onClick(props, e)}
|
{...Card.bindEventsToProps(props)}
|
||||||
onMouseUp={e => Card.onMouseUp(props, e)}
|
|
||||||
onKeyDown={e => Card.onKeyDown(props, e)}
|
|
||||||
data-iid={props.item._id}
|
data-iid={props.item._id}
|
||||||
data-is-focusable>
|
data-is-focusable>
|
||||||
{props.item.thumb ? (
|
{props.item.thumb ? (
|
||||||
@ -21,7 +20,7 @@ const ListCard: React.FunctionComponent<Card.Props> = (props) => (
|
|||||||
) : null}
|
) : null}
|
||||||
<div className="data">
|
<div className="data">
|
||||||
<CardInfo source={props.source} item={props.item} />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Card } from "./card"
|
import { Card } from "./card"
|
||||||
import CardInfo from "./info"
|
import CardInfo from "./info"
|
||||||
|
import Highlights from "./highlights"
|
||||||
|
|
||||||
const className = (props: Card.Props) => {
|
const className = (props: Card.Props) => {
|
||||||
let cn = ["card", "magazine-card"]
|
let cn = ["card", "magazine-card"]
|
||||||
@ -12,9 +13,7 @@ const className = (props: Card.Props) => {
|
|||||||
const MagazineCard: React.FunctionComponent<Card.Props> = (props) => (
|
const MagazineCard: React.FunctionComponent<Card.Props> = (props) => (
|
||||||
<div
|
<div
|
||||||
className={className(props)}
|
className={className(props)}
|
||||||
onClick={e => Card.onClick(props, e)}
|
{...Card.bindEventsToProps(props)}
|
||||||
onMouseUp={e => Card.onMouseUp(props, e)}
|
|
||||||
onKeyDown={e => Card.onKeyDown(props, e)}
|
|
||||||
data-iid={props.item._id}
|
data-iid={props.item._id}
|
||||||
data-is-focusable>
|
data-is-focusable>
|
||||||
{props.item.thumb ? (
|
{props.item.thumb ? (
|
||||||
@ -22,8 +21,8 @@ const MagazineCard: React.FunctionComponent<Card.Props> = (props) => (
|
|||||||
) : null}
|
) : null}
|
||||||
<div className="data">
|
<div className="data">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="title">{props.item.title}</h3>
|
<h3 className="title"><Highlights text={props.item.title} keyword={props.keyword} title /></h3>
|
||||||
<p className={"snippet" + (props.item.thumb ? "" : " show")}>{props.item.snippet.slice(0, 325)}</p>
|
<p className="snippet"><Highlights text={props.item.snippet} keyword={props.keyword} /></p>
|
||||||
</div>
|
</div>
|
||||||
<CardInfo source={props.source} item={props.item} />
|
<CardInfo source={props.source} item={props.item} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,6 +47,7 @@ class CardsFeed extends React.Component<FeedProps> {
|
|||||||
key={item._id}
|
key={item._id}
|
||||||
item={item}
|
item={item}
|
||||||
source={this.props.sourceMap[item.source]}
|
source={this.props.sourceMap[item.source]}
|
||||||
|
keyword={this.props.keyword}
|
||||||
shortcuts={this.props.shortcuts}
|
shortcuts={this.props.shortcuts}
|
||||||
markRead={this.props.markRead}
|
markRead={this.props.markRead}
|
||||||
contextMenu={this.props.contextMenu}
|
contextMenu={this.props.contextMenu}
|
||||||
|
@ -11,6 +11,7 @@ export type FeedProps = FeedReduxProps & {
|
|||||||
viewType: ViewType
|
viewType: ViewType
|
||||||
items: RSSItem[]
|
items: RSSItem[]
|
||||||
sourceMap: Object
|
sourceMap: Object
|
||||||
|
keyword: string
|
||||||
shortcuts: (item: RSSItem, key: string) => void
|
shortcuts: (item: RSSItem, key: string) => void
|
||||||
markRead: (item: RSSItem) => void
|
markRead: (item: RSSItem) => void
|
||||||
contextMenu: (feedId: string, item: RSSItem, e) => void
|
contextMenu: (feedId: string, item: RSSItem, e) => void
|
||||||
|
@ -16,6 +16,7 @@ class ListFeed extends React.Component<FeedProps> {
|
|||||||
key: item._id,
|
key: item._id,
|
||||||
item: item,
|
item: item,
|
||||||
source: this.props.sourceMap[item.source],
|
source: this.props.sourceMap[item.source],
|
||||||
|
keyword: this.props.keyword,
|
||||||
shortcuts: this.props.shortcuts,
|
shortcuts: this.props.shortcuts,
|
||||||
markRead: this.props.markRead,
|
markRead: this.props.markRead,
|
||||||
contextMenu: this.props.contextMenu,
|
contextMenu: this.props.contextMenu,
|
||||||
|
@ -16,15 +16,17 @@ interface FeedContainerProps {
|
|||||||
const getSources = (state: RootState) => state.sources
|
const getSources = (state: RootState) => state.sources
|
||||||
const getItems = (state: RootState) => state.items
|
const getItems = (state: RootState) => state.items
|
||||||
const getFeed = (state: RootState, props: FeedContainerProps) => state.feeds[props.feedId]
|
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 getView = (_, props: FeedContainerProps) => props.viewType
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
[getSources, getItems, getFeed, getView],
|
[getSources, getItems, getFeed, getView, getKeyword],
|
||||||
(sources, items, feed, viewType) => ({
|
(sources, items, feed, viewType, keyword) => ({
|
||||||
feed: feed,
|
feed: feed,
|
||||||
items: feed.iids.map(iid => items[iid]),
|
items: feed.iids.map(iid => items[iid]),
|
||||||
sourceMap: sources,
|
sourceMap: sources,
|
||||||
|
keyword: keyword,
|
||||||
viewType: viewType
|
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 {
|
try {
|
||||||
return new RegExp(regex)
|
return new RegExp(regex, flags)
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user