feat: add MemoContent component

This commit is contained in:
Steven 2022-09-10 21:22:26 +08:00
parent 7b0987610c
commit 6e4577f721
13 changed files with 196 additions and 139 deletions

View File

@ -3,10 +3,9 @@ import * as utils from "../helpers/utils";
import useI18n from "../hooks/useI18n"; import useI18n from "../hooks/useI18n";
import useToggle from "../hooks/useToggle"; import useToggle from "../hooks/useToggle";
import { memoService } from "../services"; import { memoService } from "../services";
import { formatMemoContent } from "../helpers/marked";
import Only from "./common/OnlyWhen";
import Image from "./Image";
import toastHelper from "./Toast"; import toastHelper from "./Toast";
import MemoContent from "./MemoContent";
import MemoResources from "./MemoResources";
import "../less/memo.less"; import "../less/memo.less";
interface Props { interface Props {
@ -72,14 +71,8 @@ const ArchivedMemo: React.FC<Props> = (props: Props) => {
</span> </span>
</div> </div>
</div> </div>
<div className="memo-content-text" dangerouslySetInnerHTML={{ __html: formatMemoContent(memo.content) }}></div> <MemoContent className="memo-content-wrapper" content={memo.content} />
<Only when={imageUrls.length > 0}> <MemoResources memo={memo} />
<div className="images-wrapper">
{imageUrls.map((imgUrl, idx) => (
<Image className="memo-img" key={idx} imgUrl={imgUrl} />
))}
</div>
</Only>
</div> </div>
); );
}; };

View File

@ -1,5 +1,5 @@
import * as utils from "../helpers/utils"; import * as utils from "../helpers/utils";
import { formatMemoContent } from "../helpers/marked"; import MemoContent, { DisplayConfig } from "./MemoContent";
import "../less/daily-memo.less"; import "../less/daily-memo.less";
interface DailyMemo extends Memo { interface DailyMemo extends Memo {
@ -18,22 +18,17 @@ const DailyMemo: React.FC<Props> = (props: Props) => {
createdAtStr: utils.getDateTimeString(propsMemo.createdTs), createdAtStr: utils.getDateTimeString(propsMemo.createdTs),
timeStr: utils.getTimeString(propsMemo.createdTs), timeStr: utils.getTimeString(propsMemo.createdTs),
}; };
const displayConfig: DisplayConfig = {
enableExpand: false,
showInlineImage: true,
};
return ( return (
<div className="daily-memo-wrapper"> <div className="daily-memo-wrapper">
<div className="time-wrapper"> <div className="time-wrapper">
<span className="normal-text">{memo.timeStr}</span> <span className="normal-text">{memo.timeStr}</span>
</div> </div>
<div className="memo-content-container"> <MemoContent className="memo-content-container" content={memo.content} displayConfig={displayConfig} />
<div
className="memo-content-text"
dangerouslySetInnerHTML={{
__html: formatMemoContent(memo.content, {
inlineImage: true,
}),
}}
></div>
</div>
<div className="split-line"></div> <div className="split-line"></div>
</div> </div>
); );

View File

@ -5,11 +5,12 @@ import { memo, useEffect, useRef, useState } from "react";
import "dayjs/locale/zh"; import "dayjs/locale/zh";
import useI18n from "../hooks/useI18n"; import useI18n from "../hooks/useI18n";
import { UNKNOWN_ID } from "../helpers/consts"; import { UNKNOWN_ID } from "../helpers/consts";
import { DONE_BLOCK_REG, formatMemoContent, TODO_BLOCK_REG } from "../helpers/marked"; import { DONE_BLOCK_REG, TODO_BLOCK_REG } from "../helpers/marked";
import { editorStateService, locationService, memoService, userService } from "../services"; import { editorStateService, locationService, memoService, userService } from "../services";
import Icon from "./Icon"; import Icon from "./Icon";
import Only from "./common/OnlyWhen"; import Only from "./common/OnlyWhen";
import toastHelper from "./Toast"; import toastHelper from "./Toast";
import MemoContent from "./MemoContent";
import MemoResources from "./MemoResources"; import MemoResources from "./MemoResources";
import showMemoCardDialog from "./MemoCardDialog"; import showMemoCardDialog from "./MemoCardDialog";
import showShareMemoImageDialog from "./ShareMemoImageDialog"; import showShareMemoImageDialog from "./ShareMemoImageDialog";
@ -17,18 +18,10 @@ import "../less/memo.less";
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
const MAX_MEMO_CONTAINER_HEIGHT = 384;
type ExpandButtonStatus = -1 | 0 | 1;
interface Props { interface Props {
memo: Memo; memo: Memo;
} }
interface State {
expandButtonStatus: ExpandButtonStatus;
}
export const getFormatedMemoCreatedAtStr = (createdTs: number, locale = "en"): string => { export const getFormatedMemoCreatedAtStr = (createdTs: number, locale = "en"): string => {
if (Date.now() - createdTs < 1000 * 60 * 60 * 24) { if (Date.now() - createdTs < 1000 * 60 * 60 * 24) {
return dayjs(createdTs).locale(locale).fromNow(); return dayjs(createdTs).locale(locale).fromNow();
@ -40,26 +33,12 @@ export const getFormatedMemoCreatedAtStr = (createdTs: number, locale = "en"): s
const Memo: React.FC<Props> = (props: Props) => { const Memo: React.FC<Props> = (props: Props) => {
const memo = props.memo; const memo = props.memo;
const { t, locale } = useI18n(); const { t, locale } = useI18n();
const [state, setState] = useState<State>({
expandButtonStatus: -1,
});
const [createdAtStr, setCreatedAtStr] = useState<string>(getFormatedMemoCreatedAtStr(memo.createdTs, locale)); const [createdAtStr, setCreatedAtStr] = useState<string>(getFormatedMemoCreatedAtStr(memo.createdTs, locale));
const memoContainerRef = useRef<HTMLDivElement>(null); const memoContainerRef = useRef<HTMLDivElement>(null);
const memoContentContainerRef = useRef<HTMLDivElement>(null); const memoContentContainerRef = useRef<HTMLDivElement>(null);
const isVisitorMode = userService.isVisitorMode(); const isVisitorMode = userService.isVisitorMode();
useEffect(() => { useEffect(() => {
if (!memoContentContainerRef) {
return;
}
if (Number(memoContentContainerRef.current?.clientHeight) > MAX_MEMO_CONTAINER_HEIGHT) {
setState({
...state,
expandButtonStatus: 0,
});
}
let intervalFlag = -1; let intervalFlag = -1;
if (Date.now() - memo.createdTs < 1000 * 60 * 60 * 24) { if (Date.now() - memo.createdTs < 1000 * 60 * 60 * 24) {
intervalFlag = setInterval(() => { intervalFlag = setInterval(() => {
@ -185,17 +164,6 @@ const Memo: React.FC<Props> = (props: Props) => {
editorStateService.setEditMemoWithId(memo.id); editorStateService.setEditMemoWithId(memo.id);
}; };
const handleExpandBtnClick = () => {
const expandButtonStatus = Boolean(!state.expandButtonStatus);
if (!expandButtonStatus) {
memoContainerRef.current?.scrollIntoView();
}
setState({
expandButtonStatus: Number(expandButtonStatus) as ExpandButtonStatus,
});
};
return ( return (
<div className={`memo-wrapper ${"memos-" + memo.id} ${memo.pinned ? "pinned" : ""}`} ref={memoContainerRef}> <div className={`memo-wrapper ${"memos-" + memo.id} ${memo.pinned ? "pinned" : ""}`} ref={memoContainerRef}>
<div className="memo-top-wrapper"> <div className="memo-top-wrapper">
@ -238,21 +206,12 @@ const Memo: React.FC<Props> = (props: Props) => {
</div> </div>
</div> </div>
</div> </div>
<div <MemoContent
ref={memoContentContainerRef} className=""
className={`memo-content-text ${state.expandButtonStatus === 0 ? "expanded" : ""}`} content={memo.content}
onClick={handleMemoContentClick} onMemoContentClick={handleMemoContentClick}
onDoubleClick={handleMemoContentDoubleClick} onMemoContentDoubleClick={handleMemoContentDoubleClick}
dangerouslySetInnerHTML={{ __html: formatMemoContent(memo.content) }} />
></div>
{state.expandButtonStatus !== -1 && (
<div className="expand-btn-container">
<span className={`btn ${state.expandButtonStatus === 0 ? "expand-btn" : "fold-btn"}`} onClick={handleExpandBtnClick}>
{state.expandButtonStatus === 0 ? "Expand" : "Fold"}
<Icon.ChevronRight className="icon-img" />
</span>
</div>
)}
<MemoResources memo={memo} /> <MemoResources memo={memo} />
</div> </div>
); );

View File

@ -9,6 +9,7 @@ import toastHelper from "./Toast";
import { generateDialog } from "./Dialog"; import { generateDialog } from "./Dialog";
import Icon from "./Icon"; import Icon from "./Icon";
import Selector from "./common/Selector"; import Selector from "./common/Selector";
import MemoContent from "./MemoContent";
import MemoResources from "./MemoResources"; import MemoResources from "./MemoResources";
import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog"; import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog";
import "../less/memo-card-dialog.less"; import "../less/memo-card-dialog.less";
@ -161,11 +162,12 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
</div> </div>
</div> </div>
<div className="memo-container"> <div className="memo-container">
<div <MemoContent
className="memo-content-text" className=""
onClick={handleMemoContentClick} displayConfig={{ enableExpand: false }}
dangerouslySetInnerHTML={{ __html: formatMemoContent(memo.content) }} content={memo.content}
></div> onMemoContentClick={handleMemoContentClick}
/>
<MemoResources memo={memo} /> <MemoResources memo={memo} />
</div> </div>
<div className="layer-container"></div> <div className="layer-container"></div>

View File

@ -1,28 +1,100 @@
import { useRef } from "react"; import { useEffect, useRef, useState } from "react";
import { formatMemoContent } from "../helpers/marked"; import { formatMemoContent } from "../helpers/marked";
import Icon from "./Icon";
import "../less/memo-content.less"; import "../less/memo-content.less";
const defaultDisplayConfig: DisplayConfig = {
enableExpand: true,
showInlineImage: false,
};
export interface DisplayConfig {
enableExpand: boolean;
showInlineImage: boolean;
}
interface Props { interface Props {
className: string; className: string;
content: string; content: string;
onMemoContentClick: (e: React.MouseEvent) => void; displayConfig?: Partial<DisplayConfig>;
onMemoContentClick?: (e: React.MouseEvent) => void;
onMemoContentDoubleClick?: (e: React.MouseEvent) => void;
} }
type ExpandButtonStatus = -1 | 0 | 1;
interface State {
expandButtonStatus: ExpandButtonStatus;
}
const MAX_MEMO_CONTAINER_HEIGHT = 384;
const MemoContent: React.FC<Props> = (props: Props) => { const MemoContent: React.FC<Props> = (props: Props) => {
const { className, content, onMemoContentClick } = props; const { className, content, onMemoContentClick, onMemoContentDoubleClick } = props;
const [state, setState] = useState<State>({
expandButtonStatus: -1,
});
const memoContentContainerRef = useRef<HTMLDivElement>(null); const memoContentContainerRef = useRef<HTMLDivElement>(null);
const displayConfig = {
...defaultDisplayConfig,
...props.displayConfig,
};
const formatConfig = {
inlineImage: displayConfig.showInlineImage,
};
useEffect(() => {
if (!memoContentContainerRef) {
return;
}
if (displayConfig.enableExpand) {
if (Number(memoContentContainerRef.current?.clientHeight) > MAX_MEMO_CONTAINER_HEIGHT) {
setState({
...state,
expandButtonStatus: 0,
});
}
}
}, []);
const handleMemoContentClick = async (e: React.MouseEvent) => { const handleMemoContentClick = async (e: React.MouseEvent) => {
onMemoContentClick(e); if (onMemoContentClick) {
onMemoContentClick(e);
}
};
const handleMemoContentDoubleClick = async (e: React.MouseEvent) => {
if (onMemoContentDoubleClick) {
onMemoContentDoubleClick(e);
}
};
const handleExpandBtnClick = () => {
const expandButtonStatus = Boolean(!state.expandButtonStatus);
setState({
expandButtonStatus: Number(expandButtonStatus) as ExpandButtonStatus,
});
}; };
return ( return (
<div <div className={`memo-content-wrapper ${className}`}>
ref={memoContentContainerRef} <div
className={`memo-content-text ${className}`} ref={memoContentContainerRef}
onClick={handleMemoContentClick} className={`memo-content-text ${state.expandButtonStatus === 0 ? "expanded" : ""}`}
dangerouslySetInnerHTML={{ __html: formatMemoContent(content) }} onClick={handleMemoContentClick}
></div> onDoubleClick={handleMemoContentDoubleClick}
dangerouslySetInnerHTML={{ __html: formatMemoContent(content, formatConfig) }}
></div>
{state.expandButtonStatus !== -1 && (
<div className="expand-btn-container">
<span className={`btn ${state.expandButtonStatus === 0 ? "expand-btn" : "fold-btn"}`} onClick={handleExpandBtnClick}>
{state.expandButtonStatus === 0 ? "Expand" : "Fold"}
<Icon.ChevronRight className="icon-img" />
</span>
</div>
)}
</div>
); );
}; };

View File

@ -4,11 +4,12 @@ import toImage from "../labs/html2image";
import { ANIMATION_DURATION } from "../helpers/consts"; import { ANIMATION_DURATION } from "../helpers/consts";
import useI18n from "../hooks/useI18n"; import useI18n from "../hooks/useI18n";
import * as utils from "../helpers/utils"; import * as utils from "../helpers/utils";
import { formatMemoContent, IMAGE_URL_REG } from "../helpers/marked"; import { IMAGE_URL_REG } from "../helpers/marked";
import Only from "./common/OnlyWhen"; import Only from "./common/OnlyWhen";
import Icon from "./Icon"; import Icon from "./Icon";
import { generateDialog } from "./Dialog"; import { generateDialog } from "./Dialog";
import toastHelper from "./Toast"; import toastHelper from "./Toast";
import MemoContent from "./MemoContent";
import "../less/share-memo-image-dialog.less"; import "../less/share-memo-image-dialog.less";
interface Props extends DialogProps { interface Props extends DialogProps {
@ -91,7 +92,7 @@ const ShareMemoImageDialog: React.FC<Props> = (props: Props) => {
<img className="memo-shortcut-img" onClick={handleDownloadBtnClick} src={shortcutImgUrl} /> <img className="memo-shortcut-img" onClick={handleDownloadBtnClick} src={shortcutImgUrl} />
</Only> </Only>
<span className="time-text">{memo.createdAtStr}</span> <span className="time-text">{memo.createdAtStr}</span>
<div className="memo-content-text" dangerouslySetInnerHTML={{ __html: formatMemoContent(memo.content) }}></div> <MemoContent className="memo-content-wrapper" content={memo.content} displayConfig={{ enableExpand: false }} />
<Only when={imageUrls.length > 0}> <Only when={imageUrls.length > 0}>
<div className="images-container"> <div className="images-container">
{imageUrls.map((imgUrl, idx) => ( {imageUrls.map((imgUrl, idx) => (

View File

@ -43,10 +43,10 @@ const defaultFormatterConfig: FormatterConfig = {
inlineImage: false, inlineImage: false,
}; };
const formatMemoContent = (content: string, addtionConfig?: Partial<FormatterConfig>) => { const formatMemoContent = (content: string, additionConfig?: Partial<FormatterConfig>) => {
const config = { const config = {
...defaultFormatterConfig, ...defaultFormatterConfig,
...addtionConfig, ...additionConfig,
}; };
const tempElement = document.createElement("div"); const tempElement = document.createElement("div");
tempElement.innerHTML = parseMarkedToHtml(escape(content)); tempElement.innerHTML = parseMarkedToHtml(escape(content));

View File

@ -19,13 +19,5 @@
> .memo-content-container { > .memo-content-container {
@apply flex flex-col justify-start items-start w-full overflow-x-hidden p-0 text-base; @apply flex flex-col justify-start items-start w-full overflow-x-hidden p-0 text-base;
> .images-container {
@apply flex flex-col justify-start items-start mt-1 w-full;
> img {
@apply w-full h-auto rounded mb-2 last:mb-0;
}
}
} }
} }

View File

@ -11,8 +11,16 @@
@apply sticky top-0 z-10 max-w-2xl w-full min-h-full flex flex-row justify-between items-center px-4 sm:pr-6 pt-6 mb-2; @apply sticky top-0 z-10 max-w-2xl w-full min-h-full flex flex-row justify-between items-center px-4 sm:pr-6 pt-6 mb-2;
background-color: #f6f5f4; background-color: #f6f5f4;
> .logo-img { > .title-container {
@apply h-14 w-auto; @apply flex flex-row justify-start items-center;
> .logo-img {
@apply h-12 sm:h-14 w-auto mr-1;
}
> .title-text {
@apply text-xl sm:text-3xl font-mono text-gray-700;
}
} }
> .action-button-container { > .action-button-container {

View File

@ -55,12 +55,7 @@
} }
> .memo-container { > .memo-container {
.flex(column, flex-start, flex-start); @apply w-full flex flex-col justify-start items-start pt-2;
@apply w-full pt-2;
> .memo-content-text {
@apply w-full text-base;
}
} }
> .normal-text { > .normal-text {

View File

@ -1,41 +1,78 @@
@import "./mixin.less"; @import "./mixin.less";
.memo-content-text { .memo-content-wrapper {
@apply w-full whitespace-pre-wrap break-words text-base leading-7; @apply w-full flex flex-col justify-start items-start;
> p { > .memo-content-text {
@apply inline-block w-full h-auto mb-1 last:mb-0 text-base leading-7 whitespace-pre-wrap break-words; @apply w-full whitespace-pre-wrap break-words text-base leading-7;
&.expanded {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 8;
overflow: hidden;
}
> p {
@apply inline-block w-full h-auto mb-1 last:mb-0 text-base leading-7 whitespace-pre-wrap break-words;
}
.img {
@apply float-left max-w-full w-full;
}
.tag-span {
@apply inline-block w-auto font-mono text-blue-600;
}
.memo-link-text {
@apply inline-block text-blue-600 cursor-pointer font-bold border-none no-underline hover:opacity-80;
}
.link {
@apply inline-block text-blue-600 cursor-pointer underline break-all hover:opacity-80;
}
.counter-block,
.todo-block {
@apply float-left inline-block box-border text-center w-7 font-mono select-none;
}
.todo-block {
@apply w-4 h-4 leading-4 border rounded box-border text-lg cursor-pointer shadow-inner hover:opacity-80;
margin-top: 6px;
margin-left: 6px;
margin-right: 6px;
}
pre {
@apply w-full mt-1 py-2 px-3 rounded text-sm bg-gray-100 whitespace-pre-wrap;
}
} }
.img { > .expand-btn-container {
@apply float-left max-w-full w-full; @apply w-full relative flex flex-row justify-start items-center;
}
.tag-span { > .btn {
@apply inline-block w-auto font-mono text-blue-600; @apply flex flex-row justify-start items-center pl-2 pr-1 py-1 my-1 text-xs rounded-lg border bg-gray-100 border-gray-200 opacity-80 shadow hover:opacity-60;
}
.memo-link-text { &.expand-btn {
@apply inline-block text-blue-600 cursor-pointer font-bold border-none no-underline hover:opacity-80; @apply mt-2;
}
.link { > .icon-img {
@apply inline-block text-blue-600 cursor-pointer underline break-all hover:opacity-80; @apply rotate-90;
} }
}
.counter-block, &.fold-btn {
.todo-block { > .icon-img {
@apply float-left inline-block box-border text-center w-7 font-mono select-none; @apply -rotate-90;
} }
}
.todo-block { > .icon-img {
@apply w-4 h-4 leading-4 border rounded box-border text-lg cursor-pointer shadow-inner hover:opacity-80; @apply w-4 h-auto ml-1 transition-all;
margin-top: 6px; }
margin-left: 6px; }
margin-right: 6px;
}
pre {
@apply w-full mt-1 py-2 px-3 rounded text-sm bg-gray-100 whitespace-pre-wrap;
} }
} }

View File

@ -44,11 +44,11 @@
} }
> .time-text { > .time-text {
@apply w-full px-6 pt-5 text-xs text-gray-500 bg-white; @apply w-full px-6 pt-5 pb-2 text-xs text-gray-500 bg-white;
} }
> .memo-content-text { > .memo-content-wrapper {
@apply w-full pt-2 pb-4 px-6 text-base bg-white; @apply w-full px-6 text-base bg-white pb-2;
} }
> .images-container { > .images-container {

View File

@ -47,7 +47,10 @@ const Explore = () => {
<section className="page-wrapper explore"> <section className="page-wrapper explore">
<div className="page-container"> <div className="page-container">
<div className="page-header"> <div className="page-header">
<img className="logo-img" src="/logo-full.webp" alt="" /> <div className="title-container">
<img className="logo-img" src="/logo.webp" alt="" />
<span className="title-text">Explore</span>
</div>
<div className="action-button-container"> <div className="action-button-container">
<Only when={!loadingState.isLoading}> <Only when={!loadingState.isLoading}>
{user ? ( {user ? (