aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2021-03-07 22:22:53 +0800
committerGitHub <noreply@github.com>2021-03-07 22:22:53 +0800
commit85113bd6e24d385bc3986e01b9f7c9e3b5e435e5 (patch)
tree3739c73d7d983016632856a87aa5cbd4cd6c5232 /FrontEnd/src
parent486663e6b4b2aa4addc4c84d24e1ce5252941858 (diff)
parentfa3b2bad71eae374d639073077030af9c5a908ff (diff)
downloadtimeline-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.json5
-rw-r--r--FrontEnd/src/app/locales/zh/translation.json5
-rw-r--r--FrontEnd/src/app/views/common/Menu.tsx5
-rw-r--r--FrontEnd/src/app/views/common/common.sass3
-rw-r--r--FrontEnd/src/app/views/timeline-common/TimelinePostEdit.tsx156
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"