diff --git a/dist/styles/cards.css b/dist/styles/cards.css index 3a512fe..c848344 100644 --- a/dist/styles/cards.css +++ b/dist/styles/cards.css @@ -80,6 +80,9 @@ left: 0; background: #0003; } +.card span.h { + background: #fce100b0; +} .default-card { display: inline-block; diff --git a/src/components/cards/card.tsx b/src/components/cards/card.tsx index 3ee436d..6f6f9a8 100644 --- a/src/components/cards/card.tsx +++ b/src/components/cards/card.tsx @@ -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) } } \ No newline at end of file diff --git a/src/components/cards/compact-card.tsx b/src/components/cards/compact-card.tsx index 9fb2f3a..e3b09f7 100644 --- a/src/components/cards/compact-card.tsx +++ b/src/components/cards/compact-card.tsx @@ -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 = (props) => (
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.title} - {props.item.snippet.slice(0, 325)} + +
diff --git a/src/components/cards/default-card.tsx b/src/components/cards/default-card.tsx index 5f25e6a..4fd58f4 100644 --- a/src/components/cards/default-card.tsx +++ b/src/components/cards/default-card.tsx @@ -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 = (props) => (
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 = (props) => ( ) : null} -

{props.item.title}

-

{props.item.snippet.slice(0, 325)}

+

+

+ +

) diff --git a/src/components/cards/highlights.tsx b/src/components/cards/highlights.tsx new file mode 100644 index 0000000..b77a836 --- /dev/null +++ b/src/components/cards/highlights.tsx @@ -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 = (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 ? {text} : text)} + +} + +export default Highlights \ No newline at end of file diff --git a/src/components/cards/list-card.tsx b/src/components/cards/list-card.tsx index ad6c697..1292dc1 100644 --- a/src/components/cards/list-card.tsx +++ b/src/components/cards/list-card.tsx @@ -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 = (props) => (
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 = (props) => ( ) : null}
-

{props.item.title}

+

) diff --git a/src/components/cards/magazine-card.tsx b/src/components/cards/magazine-card.tsx index ac7ea5d..1d18324 100644 --- a/src/components/cards/magazine-card.tsx +++ b/src/components/cards/magazine-card.tsx @@ -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 = (props) => (
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 = (props) => ( ) : null}
-

{props.item.title}

-

{props.item.snippet.slice(0, 325)}

+

+

diff --git a/src/components/feeds/cards-feed.tsx b/src/components/feeds/cards-feed.tsx index f198114..c30e196 100644 --- a/src/components/feeds/cards-feed.tsx +++ b/src/components/feeds/cards-feed.tsx @@ -47,6 +47,7 @@ class CardsFeed extends React.Component { 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} diff --git a/src/components/feeds/feed.tsx b/src/components/feeds/feed.tsx index ae6ed6e..67cf1ca 100644 --- a/src/components/feeds/feed.tsx +++ b/src/components/feeds/feed.tsx @@ -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 diff --git a/src/components/feeds/list-feed.tsx b/src/components/feeds/list-feed.tsx index 74a8629..7fc1205 100644 --- a/src/components/feeds/list-feed.tsx +++ b/src/components/feeds/list-feed.tsx @@ -16,6 +16,7 @@ class ListFeed extends React.Component { 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, diff --git a/src/containers/feed-container.tsx b/src/containers/feed-container.tsx index 940cbb4..3ea8dda 100644 --- a/src/containers/feed-container.tsx +++ b/src/containers/feed-container.tsx @@ -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 }) ) diff --git a/src/scripts/utils.ts b/src/scripts/utils.ts index 39853f1..5618684 100644 --- a/src/scripts/utils.ts +++ b/src/scripts/utils.ts @@ -182,9 +182,9 @@ export function calculateItemSize(): Promise { }) } -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 }