Skip to content

Commit

Permalink
feat(group): add markdown editor for group module
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnsonMao committed Nov 16, 2024
1 parent 27149f7 commit b687b0f
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 60 deletions.
38 changes: 24 additions & 14 deletions components/Group/Form/Fields/TextField.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import MuiTextField from '@mui/material/TextField';
import MarkdownEditor from '@/shared/components/MarkdownEditor/MarkdownEditor';

export default function TextField({
id,
Expand All @@ -12,20 +13,29 @@ export default function TextField({
}) {
return (
<>
<MuiTextField
inputRef={(element) => control.setRef?.(name, element)}
fullWidth
id={id}
name={name}
sx={{ '& legend': { display: 'none' } }}
size="small"
placeholder={placeholder}
value={value}
multiline={multiline}
rows={multiline && 6}
helperText={helperText}
{...control}
/>
{multiline ? (
<MarkdownEditor
rootClassName="p-px bg-basic-200 rounded-md focus-within:bg-primary-base"
className="bg-white rounded-md"
ref={(element) => control.setRef?.(name, element)}
value={value}
placeholder={placeholder}
onChange={(markdown) => control.onChange({ target: { name, value: markdown } })}
/>
) : (
<MuiTextField
inputRef={(element) => control.setRef?.(name, element)}
fullWidth
id={id}
name={name}
sx={{ '& legend': { display: 'none' } }}
size="small"
placeholder={placeholder}
value={value}
helperText={helperText}
{...control}
/>
)}
<span className="error-message">{error}</span>
</>
);
Expand Down
5 changes: 3 additions & 2 deletions components/Group/GroupList/GroupCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
import Image from '@/shared/components/Image';
import { timeDuration } from '@/utils/date';
import emptyCoverWithBackgroundImg from '@/public/assets/empty-cover-with-background.png';
import MarkdownEditor from '@/shared/components/MarkdownEditor';
import {
StyledAreas,
StyledContainer,
Expand Down Expand Up @@ -47,8 +48,8 @@ function GroupCard({
<span>{formatToString(partnerEducationStep, '皆可')}</span>
</StyledText>
</StyledInfo>
<StyledText lineClamp="2" fontSize="14px" style={{ minHeight: '42px' }}>
{content}
<StyledText lineClamp="2" fontSize="14px" style={{ height: '48px' }}>
<MarkdownEditor value={content?.split('\n')[0]} suppressLinkDefaultPrevent readOnly />
</StyledText>
<StyledAreas>
<LocationOnOutlinedIcon fontSize="16px" sx={{ color: '#536166' }} />
Expand Down
6 changes: 2 additions & 4 deletions components/Group/detail/NoticeCard.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import styled from '@emotion/styled';
import Skeleton from '@mui/material/Skeleton';
import { timeDuration } from '@/utils/date';
import TextWithLinks from '@/shared/components/TextWithLinks';
import MarkdownEditor from '@/shared/components/MarkdownEditor';

export const StyledTitle = styled.h2`
font-weight: bold;
Expand Down Expand Up @@ -49,9 +49,7 @@ function NoticeCard({ data = {}, isLoading }) {
<Skeleton width="60%" animation="wave" />
</div>
) : (
<div>
<TextWithLinks>{data?.notice}</TextWithLinks>
</div>
<MarkdownEditor readOnly value={data?.notice} />
)}
</StyledText>
<StyledTime>
Expand Down
4 changes: 2 additions & 2 deletions components/Group/detail/OrganizerCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Avatar from '@mui/material/Avatar';
import { EDUCATION_STEP, ROLE } from '@/constants/member';
import locationSvg from '@/public/assets/icons/location.svg';
import Chip from '@/shared/components/Chip';
import TextWithLinks from '@/shared/components/TextWithLinks';
import MarkdownEditor from '@/shared/components/MarkdownEditor';

const StyledHeader = styled.div`
display: flex;
Expand Down Expand Up @@ -139,7 +139,7 @@ function OrganizerCard({ data = {}, isLoading }) {
<Skeleton width="40%" animation="wave" />
</div>
) : (
<TextWithLinks>{data?.content}</TextWithLinks>
<MarkdownEditor readOnly value={data?.content} />
)}
</StyledText>
<StyledTags>
Expand Down
5 changes: 3 additions & 2 deletions components/Profile/MyGroup/GroupCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Image from '@/shared/components/Image';
import emptyCoverImg from '@/public/assets/empty-cover.png';
import useMutation from '@/hooks/useMutation';
import { timeDuration } from '@/utils/date';
import MarkdownEditor from '@/shared/components/MarkdownEditor';
import {
StyledAreas,
StyledContainer,
Expand Down Expand Up @@ -86,8 +87,8 @@ function GroupCard({
</StyledImageWrapper>
<StyledContainer>
<StyledTitle>{title}</StyledTitle>
<StyledText lineClamp="2" style={{ minHeight: '42px' }}>
{content}
<StyledText lineClamp="2" style={{ height: '42px' }}>
<MarkdownEditor readOnly value={content?.split('\n')[0]} suppressLinkDefaultPrevent />
</StyledText>
<StyledAreas>
<LocationOnOutlinedIcon fontSize="16px" sx={{ color: '#536166' }} />
Expand Down
2 changes: 1 addition & 1 deletion shared/components/CheckLink.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function InternalCheckLink(props, ref) {
const data = Array.isArray(trustWebsites) ? trustWebsites : [];
const newLink = new URL(href);

if (data.includes(newLink.hostname)) {
if (data.includes(newLink.hostname) || newLink.hostname === window.location.hostname) {
window.open(newLink.href, '_blank');
return;
}
Expand Down
103 changes: 68 additions & 35 deletions shared/components/MarkdownEditor/MarkdownEditor.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { useEffect, useId, useRef } from 'react';
import { forwardRef, useEffect, useId, useMemo, useRef } from 'react';
import {
BlockTypeSelect,
BoldItalicUnderlineToggles,
CodeToggle,
CreateLink,
DiffSourceToggleWrapper,
InsertThematicBreak,
ListsToggle,
UndoRedo,
MDXEditor,
codeBlockPlugin,
diffSourcePlugin,
headingsPlugin,
linkDialogPlugin,
Expand All @@ -24,6 +22,7 @@ import {
imagePlugin,
} from '@mdxeditor/editor';
import '@mdxeditor/editor/style.css';
import { cn } from '@/utils/cn';
import zhTW from './locales/zh-tw';
import { ImageDialog } from './ImageDialog';
import CheckLink from '../CheckLink';
Expand All @@ -32,53 +31,76 @@ const toolbarContents = () => (
<DiffSourceToggleWrapper>
<UndoRedo />
<Separator />
<BoldItalicUnderlineToggles />
<Separator />
<CodeToggle />
<Separator />
<ListsToggle />
<Separator />
<BlockTypeSelect />
<Separator />
<BoldItalicUnderlineToggles />
<CreateLink />
<InsertImage />
<InsertThematicBreak />
<ListsToggle />
</DiffSourceToggleWrapper>
);

const generatePlugins = (diffMarkdown = '') => [
codeBlockPlugin(),
diffSourcePlugin({ viewMode: 'rich-text', diffMarkdown }),
headingsPlugin({ allowedHeadingLevels: [1, 2, 3] }),
imagePlugin({ ImageDialog }),
linkDialogPlugin(),
linkPlugin(),
listsPlugin(),
markdownShortcutPlugin(),
quotePlugin(),
thematicBreakPlugin()
];

const generatePluginWithToolbar = (diffMarkdown = '') => [
...generatePlugins(diffMarkdown),
toolbarPlugin({ toolbarContents })
];
const generatePluginsSettings = ({ diffMarkdown = '' }) => ({
diffSource: diffSourcePlugin({ viewMode: 'rich-text', diffMarkdown }),
headings: headingsPlugin({ allowedHeadingLevels: [1, 2, 3] }),
image: imagePlugin({ ImageDialog }),
linkDialog: linkDialogPlugin(),
link: linkPlugin(),
lists: listsPlugin(),
quote: quotePlugin(),
markdownShortcut: markdownShortcutPlugin(),
thematicBreak: thematicBreakPlugin(),
toolbar: toolbarPlugin({ toolbarContents }),
});

export default function MarkdownEditor({ readOnly = false, value = '123123', onChange }) {
function InternalMarkdownEditor(
{
readOnly = false,
hasHeadings,
value = '',
placeholder,
rootClassName,
className,
editorClassName,
onChange,
suppressLinkDefaultPrevent = false,
},
ref
) {
const id = useId();
const checkLinkRef = useRef(null);
const markdown = useRef(value);
const pluginsSettings = useMemo(
() =>
Object.entries(generatePluginsSettings({ diffMarkdown: markdown.current }))
.filter(([key]) => {
if (readOnly) {
return key !== 'toolbar';
}
if (key === 'headings') {
return hasHeadings;
}
return true;
})
.map(([_, plugin]) => plugin),
[markdown.current]
);

useEffect(() => {
const editor = document.getElementById(id).querySelector('.prose');

const handleClick = (e) => {
if (suppressLinkDefaultPrevent) {
return;
}

let { target } = e;
e.preventDefault();
while (target.tagName !== 'A') {
if (editor === target) break;
target = target.parentElement;
}
if (target.tagName === 'A') {
e.preventDefault();
checkLinkRef.current?.check(target.href);
}
};
Expand All @@ -88,24 +110,35 @@ export default function MarkdownEditor({ readOnly = false, value = '123123', onC
}, []);

return (
<div id={id}>
<div id={id} className={rootClassName}>
<MDXEditor
key={readOnly ? 'readOnly' : 'withToolbar'}
ref={ref}
readOnly={readOnly}
markdown={value}
markdown={markdown.current}
onChange={onChange}
placeholder={placeholder}
suppressHtmlProcessing
contentEditableClassName="prose"
plugins={readOnly ? generatePlugins(value) : generatePluginWithToolbar(value)}
translation={(keyString, defaultValue, interpolations) => {
className={className}
contentEditableClassName={cn(
'prose',
readOnly && '!p-0',
editorClassName
)}
plugins={pluginsSettings}
translation={(keyString, defaultText, interpolations) => {
const keys = keyString.split('.');
const text = keys.reduce((acc, key) => acc?.[key], zhTW);
return typeof text === 'string'
? text.replace(/{{([^{}]+)}}/g, (_, p1) => interpolations?.[p1] || '')
: defaultValue;
: defaultText;
}}
/>
<CheckLink ref={checkLinkRef} />
</div>
);
}

const MarkdownEditor = forwardRef(InternalMarkdownEditor);

export default MarkdownEditor;

0 comments on commit b687b0f

Please sign in to comment.