From e83da106a259f4a8ab11324eceef2c15a9a08bf0 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 18 Mar 2021 21:01:07 +0800 Subject: ... --- FrontEnd/src/app/index.sass | 5 + FrontEnd/src/app/services/TimelinePostBuilder.ts | 32 ++-- FrontEnd/src/app/views/common/TabPages.tsx | 74 +++++++++ FrontEnd/src/app/views/common/common.sass | 4 + .../app/views/timeline-common/MarkdownPostEdit.tsx | 147 ++++++++++-------- .../timeline-common/TimelinePostContentView.tsx | 2 +- .../app/views/timeline-common/TimelinePostEdit.tsx | 167 ++++++++++----------- .../app/views/timeline-common/timeline-common.sass | 8 + 8 files changed, 269 insertions(+), 170 deletions(-) create mode 100644 FrontEnd/src/app/views/common/TabPages.tsx (limited to 'FrontEnd/src') diff --git a/FrontEnd/src/app/index.sass b/FrontEnd/src/app/index.sass index 39dce693..60f274c2 100644 --- a/FrontEnd/src/app/index.sass +++ b/FrontEnd/src/app/index.sass @@ -95,3 +95,8 @@ textarea i line-height: 1 + +.markdown-container + img + max-height: 200px + max-width: 100% diff --git a/FrontEnd/src/app/services/TimelinePostBuilder.ts b/FrontEnd/src/app/services/TimelinePostBuilder.ts index f6c8f881..92d8484b 100644 --- a/FrontEnd/src/app/services/TimelinePostBuilder.ts +++ b/FrontEnd/src/app/services/TimelinePostBuilder.ts @@ -1,8 +1,3 @@ -import { - escapeHtml, - replaceEntities, - unescapeMd, -} from "remarkable/lib/common/utils"; import { Remarkable } from "remarkable"; import { UiLogicError } from "@/common"; @@ -18,28 +13,16 @@ export default class TimelinePostBuilder { constructor(onChange: () => void) { this._onChange = onChange; + const oldImageRenderer = this._md.renderer.rules.image; this._md.renderer.rules.image = (( _t: TimelinePostBuilder ): Remarkable.Rule => function (tokens, idx, options /*, env */) { const i = parseInt(tokens[idx].src); - const src = - ' src="' + - (isNaN(i) && i > 0 && i <= _t._images.length - ? escapeHtml(tokens[idx].src) - : _t._images[i - 1].url) + - '"'; - const title = tokens[idx].title - ? ' title="' + escapeHtml(replaceEntities(tokens[idx].title)) + '"' - : ""; - const alt = - ' alt="' + - (tokens[idx].alt - ? escapeHtml(replaceEntities(unescapeMd(tokens[idx].alt))) - : "") + - '"'; - const suffix = options?.xhtmlOut ? " /" : ""; - return ""; + if (!isNaN(i) && i > 0 && i <= _t._images.length) { + tokens[idx].src = _t._images[i - 1].url; + } + return oldImageRenderer(tokens, idx, options); })(this); } @@ -49,6 +32,7 @@ export default class TimelinePostBuilder { } appendImage(file: File): void { + this._images = this._images.slice(); this._images.push({ file, url: URL.createObjectURL(file), @@ -69,6 +53,8 @@ export default class TimelinePostBuilder { newIndex = this._images.length - 1; } + this._images = this._images.slice(); + const [old] = this._images.splice(oldIndex, 1); this._images.splice(newIndex, 0, old); @@ -80,6 +66,8 @@ export default class TimelinePostBuilder { throw new UiLogicError("Old index out of range."); } + this._images = this._images.slice(); + URL.revokeObjectURL(this._images[index].url); this._images.splice(index, 1); diff --git a/FrontEnd/src/app/views/common/TabPages.tsx b/FrontEnd/src/app/views/common/TabPages.tsx new file mode 100644 index 00000000..424e769f --- /dev/null +++ b/FrontEnd/src/app/views/common/TabPages.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { Nav } from "react-bootstrap"; +import { useTranslation } from "react-i18next"; + +import { convertI18nText, I18nText, UiLogicError } from "@/common"; + +export interface TabPage { + id: string; + tabText: I18nText; + page: React.ReactNode; +} + +export interface TabPagesProps { + pages: TabPage[]; + actions?: React.ReactNode; + className?: string; + style?: React.CSSProperties; + navClassName?: string; + navStyle?: React.CSSProperties; + pageContainerClassName?: string; + pageContainerStyle?: React.CSSProperties; +} + +const TabPages: React.FC = ({ + pages, + actions, + className, + style, + navClassName, + navStyle, + pageContainerClassName, + pageContainerStyle, +}) => { + if (pages.length === 0) { + throw new UiLogicError("Page list can't be empty."); + } + + const { t } = useTranslation(); + + const [tab, setTab] = React.useState(pages[0].id); + + const currentPage = pages.find((p) => p.id === tab); + + if (currentPage == null) { + throw new UiLogicError("Current tab value is bad."); + } + + return ( +
+ +
+ {currentPage.page} +
+
+ ); +}; + +export default TabPages; diff --git a/FrontEnd/src/app/views/common/common.sass b/FrontEnd/src/app/views/common/common.sass index 0a30d995..f3022d19 100644 --- a/FrontEnd/src/app/views/common/common.sass +++ b/FrontEnd/src/app/views/common/common.sass @@ -92,3 +92,7 @@ .cru-menu-divider border-top: 1px solid $gray-200 + +.cru-tab-pages-action-area + display: flex + align-items: center diff --git a/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx b/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx index 079344e1..d43077b4 100644 --- a/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx +++ b/FrontEnd/src/app/views/timeline-common/MarkdownPostEdit.tsx @@ -1,24 +1,31 @@ import React from "react"; -import { Nav, Form } from "react-bootstrap"; +import { Form } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import { getHttpTimelineClient, HttpTimelinePostInfo } from "@/http/timeline"; +import TabPages from "../common/TabPages"; import TimelinePostBuilder from "@/services/TimelinePostBuilder"; export interface MarkdownPostEditProps { timeline: string; onPosted: (post: HttpTimelinePostInfo) => void; + onPostError: () => void; + onClose: () => void; + className?: string; + style?: React.CSSProperties; } const MarkdownPostEdit: React.FC = ({ timeline: timelineName, onPosted, + onClose, + onPostError, + className, + style, }) => { const { t } = useTranslation(); - const [tab, setTab] = React.useState<"text" | "images" | "preview">("text"); - const [process, setProcess] = React.useState(false); const [text, _setText] = React.useState(""); @@ -48,68 +55,88 @@ const MarkdownPostEdit: React.FC = ({ }, []); const send = async (): Promise => { - const dataList = await getBuilder().build(); - const post = await getHttpTimelineClient().postPost(timelineName, { - dataList, - }); - onPosted(post); + setProcess(true); + try { + const dataList = await getBuilder().build(); + const post = await getHttpTimelineClient().postPost(timelineName, { + dataList, + }); + onPosted(post); + onClose(); + } catch (e) { + setProcess(false); + onPostError(); + } }; return ( -
- -
- {(() => { - if (tab === "text") { - return ( - { - getBuilder().setMarkdownText(event.currentTarget.value); + +
+ {t("operationDialog.cancel")} +
+
+ {t("timeline.send")} +
+ + } + pages={[ + { + id: "text", + tabText: "edit", + page: ( + { + getBuilder().setMarkdownText(event.currentTarget.value); + }} + /> + ), + }, + { + id: "images", + tabText: "image", + page: ( +
+ {images.map((image) => ( + + ))} + ) => { + const { files } = event.currentTarget; + if (files != null && files.length !== 0) { + getBuilder().appendImage(files[0]); + } }} + disabled={process} /> - ); - } else if (tab === "images") { - return
; - } else { - return
; - } - })()} -
-
+
+ ), + }, + { + id: "preview", + tabText: "preview", + page: ( +
+ ), + }, + ]} + /> ); }; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx index d836d1db..58fae4c7 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePostContentView.tsx @@ -152,7 +152,7 @@ const MarkdownView: React.FC = (props) => { } return (
= (props) => { const [process, setProcess] = React.useState(false); - const [kind, setKind] = React.useState("text"); + const [kind, setKind] = React.useState>("text"); + const [showMarkdown, setShowMarkdown] = React.useState(false); const [text, setText] = React.useState(""); - const [markdown, setMarkdown] = React.useState(""); const [image, setImage] = React.useState(null); const draftTextLocalStorageKey = `timeline.${timeline.name}.postDraft.text`; - const draftMarkdownLocalStorageKey = `timeline.${timeline.name}.postDraft.markdown`; React.useEffect(() => { setText(window.localStorage.getItem(draftTextLocalStorageKey) ?? ""); - setMarkdown( - window.localStorage.getItem(draftMarkdownLocalStorageKey) ?? "" - ); - }, [draftTextLocalStorageKey, draftMarkdownLocalStorageKey]); + }, [draftTextLocalStorageKey]); const canSend = (kind === "text" && text.length !== 0) || - (kind === "image" && image != null) || - (kind === "markdown" && markdown.length !== 0); + (kind === "image" && image != null); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const containerRef = React.useRef(null!); @@ -160,6 +156,13 @@ const TimelinePostEdit: React.FC = (props) => { }; }); + const onPostError = (): void => { + pushAlert({ + type: "danger", + message: "timeline.sendPostFailed", + }); + }; + const onSend = async (): Promise => { setProcess(true); @@ -171,12 +174,6 @@ const TimelinePostEdit: React.FC = (props) => { data: await base64(text), }; break; - case "markdown": - requestData = { - contentType: "text/markdown", - data: await base64(markdown), - }; - break; case "image": if (image == null) { throw new UiLogicError( @@ -201,20 +198,14 @@ const TimelinePostEdit: React.FC = (props) => { if (kind === "text") { setText(""); window.localStorage.removeItem(draftTextLocalStorageKey); - } else if (kind === "markdown") { - setMarkdown(""); - window.localStorage.removeItem(draftMarkdownLocalStorageKey); } setProcess(false); setKind("text"); onPosted(data); }, (_) => { - pushAlert({ - type: "danger", - message: "timeline.sendPostFailed", - }); setProcess(false); + onPostError(); } ); }; @@ -224,73 +215,75 @@ const TimelinePostEdit: React.FC = (props) => { ref={containerRef} className={clsx("container-fluid bg-light", className)} > - - - {(() => { - if (kind === "text") { - return ( - { - setText(t); - window.localStorage.setItem(draftTextLocalStorageKey, t); - }} - /> - ); - } else if (kind === "image") { - return ( - - ); - } else if (kind === "markdown") { - return ( - { - setMarkdown(t); - window.localStorage.setItem( - draftMarkdownLocalStorageKey, - t - ); - }} + {showMarkdown ? ( + setShowMarkdown(false)} + timeline={timeline.name} + onPosted={onPosted} + onPostError={onPostError} + /> + ) : ( + + + {(() => { + if (kind === "text") { + return ( + { + setText(t); + window.localStorage.setItem(draftTextLocalStorageKey, t); + }} + /> + ); + } else if (kind === "image") { + return ( + + ); + } + })()} + + +
+ ({ + type: "button", + text: `timeline.post.type.${kind}`, + iconClassName: postKindIconClassNameMap[kind], + onClick: () => { + if (kind === "markdown") { + setShowMarkdown(true); + } else { + setKind(kind); + } + }, + }))} + > + - ); - } - })()} - - -
- ({ - type: "button", - text: `timeline.post.type.${kind}`, - iconClassName: postKindIconClassNameMap[kind], - onClick: () => { - setKind(kind); - }, - }))} + +
+ - -
-
- - {t("timeline.send")} - - -
+ {t("timeline.send")} + + +
+ )}
); }; diff --git a/FrontEnd/src/app/views/timeline-common/timeline-common.sass b/FrontEnd/src/app/views/timeline-common/timeline-common.sass index 43c4d0f7..04318674 100644 --- a/FrontEnd/src/app/views/timeline-common/timeline-common.sass +++ b/FrontEnd/src/app/views/timeline-common/timeline-common.sass @@ -170,3 +170,11 @@ $timeline-line-color-current: #36c2e6 top: 56px right: 0 margin: 0.5em + +.timeline-markdown-post-edit-page + overflow: scroll + max-height: 300px + +.timeline-markdown-post-edit-image + max-width: 100% + max-height: 200px -- cgit v1.2.3