mirror of
https://github.com/usememos/memos.git
synced 2024-12-19 09:02:49 +03:00
feat: add MemoContent
component
This commit is contained in:
parent
7b0987610c
commit
6e4577f721
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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) => (
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 ? (
|
||||
|
Loading…
Reference in New Issue
Block a user