diff options
author | crupest <crupest@outlook.com> | 2021-03-07 22:22:53 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-03-07 22:22:53 +0800 |
commit | 85113bd6e24d385bc3986e01b9f7c9e3b5e435e5 (patch) | |
tree | 3739c73d7d983016632856a87aa5cbd4cd6c5232 /FrontEnd/src | |
parent | 486663e6b4b2aa4addc4c84d24e1ce5252941858 (diff) | |
parent | fa3b2bad71eae374d639073077030af9c5a908ff (diff) | |
download | timeline-85113bd6e24d385bc3986e01b9f7c9e3b5e435e5.tar.gz timeline-85113bd6e24d385bc3986e01b9f7c9e3b5e435e5.tar.bz2 timeline-85113bd6e24d385bc3986e01b9f7c9e3b5e435e5.zip |
Merge pull request #348 from crupest/post-markdown
Post markdown edit.
Diffstat (limited to 'FrontEnd/src')
-rw-r--r-- | FrontEnd/src/app/locales/en/translation.json | 5 | ||||
-rw-r--r-- | FrontEnd/src/app/locales/zh/translation.json | 5 | ||||
-rw-r--r-- | FrontEnd/src/app/views/common/Menu.tsx | 5 | ||||
-rw-r--r-- | FrontEnd/src/app/views/common/common.sass | 3 | ||||
-rw-r--r-- | FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx | 156 |
5 files changed, 135 insertions, 39 deletions
diff --git a/FrontEnd/src/app/locales/en/translation.json b/FrontEnd/src/app/locales/en/translation.json index 63b2a1be..4002ee4f 100644 --- a/FrontEnd/src/app/locales/en/translation.json +++ b/FrontEnd/src/app/locales/en/translation.json @@ -99,6 +99,11 @@ "notMatch": "Name does not match." }, "post": { + "type": { + "text": "Plain Text", + "markdown": "Markdown", + "image": "Image" + }, "deleteDialog": { "title": "Confirm Delete", "prompt": "Are you sure to delete the post? This operation is not recoverable." diff --git a/FrontEnd/src/app/locales/zh/translation.json b/FrontEnd/src/app/locales/zh/translation.json index 296966c4..3f966d7c 100644 --- a/FrontEnd/src/app/locales/zh/translation.json +++ b/FrontEnd/src/app/locales/zh/translation.json @@ -99,6 +99,11 @@ "notMatch": "名字不匹配" }, "post": { + "type": { + "text": "纯文本", + "markdown": "Markdown", + "image": "图片" + }, "deleteDialog": { "title": "确认删除", "prompt": "确定删除这个消息?这个操作不可撤销。" diff --git a/FrontEnd/src/app/views/common/Menu.tsx b/FrontEnd/src/app/views/common/Menu.tsx index c2110c9c..54650f22 100644 --- a/FrontEnd/src/app/views/common/Menu.tsx +++ b/FrontEnd/src/app/views/common/Menu.tsx @@ -12,6 +12,7 @@ export type MenuItem = | { type: "button"; text: I18nText; + iconClassName?: string; color?: BootstrapThemeColor; onClick: () => void; }; @@ -44,6 +45,9 @@ const Menu: React.FC<MenuProps> = ({ items, className, onItemClicked }) => { onItemClicked?.(); }} > + {item.iconClassName != null ? ( + <i className={clsx(item.iconClassName, "cru-menu-item-icon")} /> + ) : null} {convertI18nText(item.text, t)} </div> ); @@ -67,7 +71,6 @@ export const PopupMenu: React.FC<PopupMenuProps> = ({ items, children }) => { return ( <OverlayTrigger trigger="click" - placement="bottom" rootClose overlay={ <Popover id="menu-popover"> diff --git a/FrontEnd/src/app/views/common/common.sass b/FrontEnd/src/app/views/common/common.sass index 819408a0..0a30d995 100644 --- a/FrontEnd/src/app/views/common/common.sass +++ b/FrontEnd/src/app/views/common/common.sass @@ -87,5 +87,8 @@ color: white
background-color: $value
+.cru-menu-item-icon
+ margin-right: 1em
+
.cru-menu-divider
border-top: 1px solid $gray-200
diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx index 5bc5b166..6c428b74 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx @@ -17,13 +17,40 @@ import { base64 } from "@/http/common"; import BlobImage from "../common/BlobImage"; import LoadingButton from "../common/LoadingButton"; +import { PopupMenu } from "../common/Menu"; + +interface TimelinePostEditTextProps { + text: string; + disabled: boolean; + onChange: (text: string) => void; + className?: string; + style?: React.CSSProperties; +} + +const TimelinePostEditText: React.FC<TimelinePostEditTextProps> = (props) => { + const { text, disabled, onChange, className, style } = props; + + return ( + <Form.Control + as="textarea" + value={text} + disabled={disabled} + onChange={(event) => { + onChange(event.target.value); + }} + className={className} + style={style} + /> + ); +}; interface TimelinePostEditImageProps { onSelect: (file: File | null) => void; + disabled: boolean; } const TimelinePostEditImage: React.FC<TimelinePostEditImageProps> = (props) => { - const { onSelect } = props; + const { onSelect, disabled } = props; const { t } = useTranslation(); @@ -41,12 +68,19 @@ const TimelinePostEditImage: React.FC<TimelinePostEditImageProps> = (props) => { } }; + React.useEffect(() => { + return () => { + onSelect(null); + }; + }, [onSelect]); + return ( <> <Form.File label={t("chooseImage")} onChange={onInputChange} accept="image/*" + disabled={disabled} className="mx-3 my-1 d-inline-block" /> {file != null && !error && ( @@ -65,6 +99,14 @@ const TimelinePostEditImage: React.FC<TimelinePostEditImageProps> = (props) => { ); }; +type PostKind = "text" | "markdown" | "image"; + +const postKindIconClassNameMap: Record<PostKind, string> = { + text: "bi-fonts", + markdown: "bi-markdown", + image: "bi-image", +}; + export interface TimelinePostEditProps { className?: string; timeline: HttpTimelineInfo; @@ -78,19 +120,27 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { const { t } = useTranslation(); const [process, setProcess] = React.useState<boolean>(false); - const [kind, setKind] = React.useState<"text" | "image">("text"); + + const [kind, setKind] = React.useState<PostKind>("text"); + const [text, setText] = React.useState<string>(""); + const [markdown, setMarkdown] = React.useState<string>(""); const [image, setImage] = React.useState<File | null>(null); - const draftLocalStorageKey = `timeline.${timeline.name}.postDraft`; + const draftTextLocalStorageKey = `timeline.${timeline.name}.postDraft.text`; + const draftMarkdownLocalStorageKey = `timeline.${timeline.name}.postDraft.markdown`; React.useEffect(() => { - setText(window.localStorage.getItem(draftLocalStorageKey) ?? ""); - }, [draftLocalStorageKey]); + setText(window.localStorage.getItem(draftTextLocalStorageKey) ?? ""); + setMarkdown( + window.localStorage.getItem(draftMarkdownLocalStorageKey) ?? "" + ); + }, [draftTextLocalStorageKey, draftMarkdownLocalStorageKey]); const canSend = (kind === "text" && text.length !== 0) || - (kind === "image" && image != null); + (kind === "image" && image != null) || + (kind === "markdown" && markdown.length !== 0); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const containerRef = React.useRef<HTMLDivElement>(null!); @@ -102,9 +152,7 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { }; React.useEffect(() => { - if (onHeightChange) { - onHeightChange(containerRef.current.clientHeight); - } + notifyHeightChange(); return () => { if (onHeightChange) { onHeightChange(0); @@ -112,11 +160,6 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { }; }); - const toggleKind = React.useCallback(() => { - setKind((oldKind) => (oldKind === "text" ? "image" : "text")); - setImage(null); - }, []); - const onSend = async (): Promise<void> => { setProcess(true); @@ -128,6 +171,12 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { data: await base64(new Blob([text])), }; break; + case "markdown": + requestData = { + contentType: "text/markdown", + data: await base64(new Blob([markdown])), + }; + break; case "image": if (image == null) { throw new UiLogicError( @@ -151,7 +200,10 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { (data) => { if (kind === "text") { setText(""); - window.localStorage.removeItem(draftLocalStorageKey); + window.localStorage.removeItem(draftTextLocalStorageKey); + } else if (kind === "markdown") { + setMarkdown(""); + window.localStorage.removeItem(draftMarkdownLocalStorageKey); } setProcess(false); setKind("text"); @@ -174,32 +226,60 @@ const TimelinePostEdit: React.FC<TimelinePostEditProps> = (props) => { > <Row> <Col className="px-1 py-1"> - {kind === "text" ? ( - <Form.Control - as="textarea" - className="w-100 h-100 timeline-post-edit" - value={text} - disabled={process} - onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => { - const value = event.currentTarget.value; - setText(value); - window.localStorage.setItem(draftLocalStorageKey, value); - }} - /> - ) : ( - <TimelinePostEditImage onSelect={setImage} /> - )} + {(() => { + if (kind === "text") { + return ( + <TimelinePostEditText + className="w-100 h-100 timeline-post-edit" + text={text} + disabled={process} + onChange={(t) => { + setText(t); + window.localStorage.setItem(draftTextLocalStorageKey, t); + }} + /> + ); + } else if (kind === "image") { + return ( + <TimelinePostEditImage onSelect={setImage} disabled={process} /> + ); + } else if (kind === "markdown") { + return ( + <TimelinePostEditText + className="w-100 h-100 timeline-post-edit" + text={markdown} + disabled={process} + onChange={(t) => { + setMarkdown(t); + window.localStorage.setItem( + draftMarkdownLocalStorageKey, + t + ); + }} + /> + ); + } + })()} </Col> <Col xs="auto" className="align-self-end m-1"> <div className="d-block text-center mt-1 mb-2"> - <i - onLoad={notifyHeightChange} - className={clsx( - kind === "text" ? "bi-image" : "bi-card-text", - "icon-button" - )} - onClick={process ? undefined : toggleKind} - /> + <PopupMenu + items={(["text", "image", "markdown"] as const).map((kind) => ({ + type: "button", + text: `timeline.post.type.${kind}`, + iconClassName: postKindIconClassNameMap[kind], + onClick: () => { + setKind(kind); + }, + }))} + > + <i + className={clsx( + postKindIconClassNameMap[kind], + "icon-button large" + )} + /> + </PopupMenu> </div> <LoadingButton variant="primary" |