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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import toastHelper from "./Toast";
import { generateDialog } from "./Dialog";
import Icon from "./Icon";
import Selector from "./common/Selector";
import MemoContent from "./MemoContent";
import MemoResources from "./MemoResources";
import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog";
import "../less/memo-card-dialog.less";
@ -161,11 +162,12 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
</div>
</div>
<div className="memo-container">
<div
className="memo-content-text"
onClick={handleMemoContentClick}
dangerouslySetInnerHTML={{ __html: formatMemoContent(memo.content) }}
></div>
<MemoContent
className=""
displayConfig={{ enableExpand: false }}
content={memo.content}
onMemoContentClick={handleMemoContentClick}
/>
<MemoResources memo={memo} />
</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 Icon from "./Icon";
import "../less/memo-content.less";
const defaultDisplayConfig: DisplayConfig = {
enableExpand: true,
showInlineImage: false,
};
export interface DisplayConfig {
enableExpand: boolean;
showInlineImage: boolean;
}
interface Props {
className: 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 { className, content, onMemoContentClick } = props;
const { className, content, onMemoContentClick, onMemoContentDoubleClick } = props;
const [state, setState] = useState<State>({
expandButtonStatus: -1,
});
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) => {
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 (
<div
ref={memoContentContainerRef}
className={`memo-content-text ${className}`}
onClick={handleMemoContentClick}
dangerouslySetInnerHTML={{ __html: formatMemoContent(content) }}
></div>
<div className={`memo-content-wrapper ${className}`}>
<div
ref={memoContentContainerRef}
className={`memo-content-text ${state.expandButtonStatus === 0 ? "expanded" : ""}`}
onClick={handleMemoContentClick}
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 useI18n from "../hooks/useI18n";
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 Icon from "./Icon";
import { generateDialog } from "./Dialog";
import toastHelper from "./Toast";
import MemoContent from "./MemoContent";
import "../less/share-memo-image-dialog.less";
interface Props extends DialogProps {
@ -91,7 +92,7 @@ const ShareMemoImageDialog: React.FC<Props> = (props: Props) => {
<img className="memo-shortcut-img" onClick={handleDownloadBtnClick} src={shortcutImgUrl} />
</Only>
<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}>
<div className="images-container">
{imageUrls.map((imgUrl, idx) => (

View File

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

View File

@ -19,13 +19,5 @@
> .memo-content-container {
@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;
background-color: #f6f5f4;
> .logo-img {
@apply h-14 w-auto;
> .title-container {
@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 {

View File

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

View File

@ -1,41 +1,78 @@
@import "./mixin.less";
.memo-content-text {
@apply w-full whitespace-pre-wrap break-words text-base leading-7;
.memo-content-wrapper {
@apply w-full flex flex-col justify-start items-start;
> p {
@apply inline-block w-full h-auto mb-1 last:mb-0 text-base leading-7 whitespace-pre-wrap break-words;
> .memo-content-text {
@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 {
@apply float-left max-w-full w-full;
}
> .expand-btn-container {
@apply w-full relative flex flex-row justify-start items-center;
.tag-span {
@apply inline-block w-auto font-mono text-blue-600;
}
> .btn {
@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 {
@apply inline-block text-blue-600 cursor-pointer font-bold border-none no-underline hover:opacity-80;
}
&.expand-btn {
@apply mt-2;
.link {
@apply inline-block text-blue-600 cursor-pointer underline break-all hover:opacity-80;
}
> .icon-img {
@apply rotate-90;
}
}
.counter-block,
.todo-block {
@apply float-left inline-block box-border text-center w-7 font-mono select-none;
}
&.fold-btn {
> .icon-img {
@apply -rotate-90;
}
}
.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;
> .icon-img {
@apply w-4 h-auto ml-1 transition-all;
}
}
}
}

View File

@ -44,11 +44,11 @@
}
> .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 {
@apply w-full pt-2 pb-4 px-6 text-base bg-white;
> .memo-content-wrapper {
@apply w-full px-6 text-base bg-white pb-2;
}
> .images-container {

View File

@ -47,7 +47,10 @@ const Explore = () => {
<section className="page-wrapper explore">
<div className="page-container">
<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">
<Only when={!loadingState.isLoading}>
{user ? (