]> Untitled Git - lemmy-ui.git/blobdiff - src/shared/components/common/markdown-textarea.tsx
Merge branch 'LemmyNet:main' into multiple-images-upload
[lemmy-ui.git] / src / shared / components / common / markdown-textarea.tsx
index b453acc3ce2383f6d635545b19a16d5d3e9d5927..498c262579a569eb22cb79e841c1fe6b4c0072b7 100644 (file)
@@ -2,7 +2,7 @@ import { None, Option, Some } from "@sniptt/monads";
 import autosize from "autosize";
 import { Component, linkEvent } from "inferno";
 import { Prompt } from "inferno-router";
-import { toUndefined } from "lemmy-js-client";
+import { Language, toUndefined } from "lemmy-js-client";
 import { pictrsUri } from "../../env";
 import { i18n } from "../../i18next";
 import { UserService } from "../../services";
@@ -18,9 +18,11 @@ import {
   toast,
 } from "../../utils";
 import { Icon, Spinner } from "./icon";
+import { LanguageSelect } from "./language-select";
 
 interface MarkdownTextAreaProps {
   initialContent: Option<string>;
+  initialLanguageId: Option<number>;
   placeholder: Option<string>;
   buttonTitle: Option<string>;
   maxLength: Option<number>;
@@ -28,14 +30,21 @@ interface MarkdownTextAreaProps {
   focus?: boolean;
   disabled?: boolean;
   finished?: boolean;
+  showLanguage?: boolean;
   hideNavigationWarnings?: boolean;
   onContentChange?(val: string): any;
   onReplyCancel?(): any;
-  onSubmit?(msg: { val: string; formId: string }): any;
+  onSubmit?(msg: {
+    val: Option<string>;
+    formId: string;
+    languageId: Option<number>;
+  }): any;
+  allLanguages: Language[];
 }
 
 interface MarkdownTextAreaState {
   content: Option<string>;
+  languageId: Option<number>;
   previewMode: boolean;
   loading: boolean;
   imageLoading: boolean;
@@ -50,6 +59,7 @@ export class MarkdownTextArea extends Component<
   private tribute: any;
   private emptyState: MarkdownTextAreaState = {
     content: this.props.initialContent,
+    languageId: this.props.initialLanguageId,
     previewMode: false,
     loading: false,
     imageLoading: false,
@@ -58,6 +68,8 @@ export class MarkdownTextArea extends Component<
   constructor(props: any, context: any) {
     super(props, context);
 
+    this.handleLanguageChange = this.handleLanguageChange.bind(this);
+
     if (isBrowser()) {
       this.tribute = setupTribute();
     }
@@ -70,8 +82,7 @@ export class MarkdownTextArea extends Component<
       autosize(textarea);
       this.tribute.attach(textarea);
       textarea.addEventListener("tribute-replaced", () => {
-        this.state.content = Some(textarea.value);
-        this.setState(this.state);
+        this.setState({ content: Some(textarea.value) });
         autosize.update(textarea);
       });
 
@@ -96,10 +107,7 @@ export class MarkdownTextArea extends Component<
 
   componentWillReceiveProps(nextProps: MarkdownTextAreaProps) {
     if (nextProps.finished) {
-      this.state.previewMode = false;
-      this.state.loading = false;
-      this.state.content = None;
-      this.setState(this.state);
+      this.setState({ previewMode: false, loading: false, content: None });
       if (this.props.replyType) {
         this.props.onReplyCancel();
       }
@@ -108,7 +116,6 @@ export class MarkdownTextArea extends Component<
       let form: any = document.getElementById(this.formId);
       form.reset();
       setTimeout(() => autosize.update(textarea), 10);
-      this.setState(this.state);
     }
   }
 
@@ -125,7 +132,7 @@ export class MarkdownTextArea extends Component<
           }
           message={i18n.t("block_leaving")}
         />
-        <div class="form-group row">
+        <div className="form-group row">
           <div className={`col-sm-12`}>
             <textarea
               id={this.id}
@@ -150,17 +157,29 @@ export class MarkdownTextArea extends Component<
                 none: <></>,
               })}
           </div>
-          <label class="sr-only" htmlFor={this.id}>
+          <label className="sr-only" htmlFor={this.id}>
             {i18n.t("body")}
           </label>
         </div>
-        <div class="row">
-          <div class="col-sm-12 d-flex flex-wrap">
+        {this.props.showLanguage && (
+          <div className="row justify-content-end">
+            <div className="col-sm-8">
+              <LanguageSelect
+                allLanguages={this.props.allLanguages}
+                selectedLanguageIds={this.state.languageId.map(Array.of)}
+                multiple={false}
+                onChange={this.handleLanguageChange}
+              />
+            </div>
+          </div>
+        )}
+        <div className="row">
+          <div className="col-sm-12 d-flex flex-wrap">
             {this.props.buttonTitle.match({
               some: buttonTitle => (
                 <button
                   type="submit"
-                  class="btn btn-sm btn-secondary mr-2"
+                  className="btn btn-sm btn-secondary mr-2"
                   disabled={this.props.disabled || this.state.loading}
                 >
                   {this.state.loading ? (
@@ -175,7 +194,7 @@ export class MarkdownTextArea extends Component<
             {this.props.replyType && (
               <button
                 type="button"
-                class="btn btn-sm btn-secondary mr-2"
+                className="btn btn-sm btn-secondary mr-2"
                 onClick={linkEvent(this, this.handleReplyCancel)}
               >
                 {i18n.t("cancel")}
@@ -192,9 +211,9 @@ export class MarkdownTextArea extends Component<
               </button>
             )}
             {/* A flex expander */}
-            <div class="flex-grow-1"></div>
+            <div className="flex-grow-1"></div>
             <button
-              class="btn btn-sm text-muted"
+              className="btn btn-sm text-muted"
               data-tippy-content={i18n.t("bold")}
               aria-label={i18n.t("bold")}
               onClick={linkEvent(this, this.handleInsertBold)}
@@ -202,7 +221,7 @@ export class MarkdownTextArea extends Component<
               <Icon icon="bold" classes="icon-inline" />
             </button>
             <button
-              class="btn btn-sm text-muted"
+              className="btn btn-sm text-muted"
               data-tippy-content={i18n.t("italic")}
               aria-label={i18n.t("italic")}
               onClick={linkEvent(this, this.handleInsertItalic)}
@@ -210,14 +229,14 @@ export class MarkdownTextArea extends Component<
               <Icon icon="italic" classes="icon-inline" />
             </button>
             <button
-              class="btn btn-sm text-muted"
+              className="btn btn-sm text-muted"
               data-tippy-content={i18n.t("link")}
               aria-label={i18n.t("link")}
               onClick={linkEvent(this, this.handleInsertLink)}
             >
               <Icon icon="link" classes="icon-inline" />
             </button>
-            <form class="btn btn-sm text-muted font-weight-bold">
+            <form className="btn btn-sm text-muted font-weight-bold">
               <label
                 htmlFor={`file-upload-${this.id}`}
                 className={`mb-0 ${
@@ -236,13 +255,13 @@ export class MarkdownTextArea extends Component<
                 type="file"
                 accept="image/*,video/*"
                 name="file"
-                class="d-none"
+                className="d-none"
                 disabled={UserService.Instance.myUserInfo.isNone()}
                 onChange={linkEvent(this, this.handleImageUpload)}
               />
             </form>
             <button
-              class="btn btn-sm text-muted"
+              className="btn btn-sm text-muted"
               data-tippy-content={i18n.t("header")}
               aria-label={i18n.t("header")}
               onClick={linkEvent(this, this.handleInsertHeader)}
@@ -250,7 +269,7 @@ export class MarkdownTextArea extends Component<
               <Icon icon="header" classes="icon-inline" />
             </button>
             <button
-              class="btn btn-sm text-muted"
+              className="btn btn-sm text-muted"
               data-tippy-content={i18n.t("strikethrough")}
               aria-label={i18n.t("strikethrough")}
               onClick={linkEvent(this, this.handleInsertStrikethrough)}
@@ -258,7 +277,7 @@ export class MarkdownTextArea extends Component<
               <Icon icon="strikethrough" classes="icon-inline" />
             </button>
             <button
-              class="btn btn-sm text-muted"
+              className="btn btn-sm text-muted"
               data-tippy-content={i18n.t("quote")}
               aria-label={i18n.t("quote")}
               onClick={linkEvent(this, this.handleInsertQuote)}
@@ -266,7 +285,7 @@ export class MarkdownTextArea extends Component<
               <Icon icon="format_quote" classes="icon-inline" />
             </button>
             <button
-              class="btn btn-sm text-muted"
+              className="btn btn-sm text-muted"
               data-tippy-content={i18n.t("list")}
               aria-label={i18n.t("list")}
               onClick={linkEvent(this, this.handleInsertList)}
@@ -274,7 +293,7 @@ export class MarkdownTextArea extends Component<
               <Icon icon="list" classes="icon-inline" />
             </button>
             <button
-              class="btn btn-sm text-muted"
+              className="btn btn-sm text-muted"
               data-tippy-content={i18n.t("code")}
               aria-label={i18n.t("code")}
               onClick={linkEvent(this, this.handleInsertCode)}
@@ -282,7 +301,7 @@ export class MarkdownTextArea extends Component<
               <Icon icon="code" classes="icon-inline" />
             </button>
             <button
-              class="btn btn-sm text-muted"
+              className="btn btn-sm text-muted"
               data-tippy-content={i18n.t("subscript")}
               aria-label={i18n.t("subscript")}
               onClick={linkEvent(this, this.handleInsertSubscript)}
@@ -290,7 +309,7 @@ export class MarkdownTextArea extends Component<
               <Icon icon="subscript" classes="icon-inline" />
             </button>
             <button
-              class="btn btn-sm text-muted"
+              className="btn btn-sm text-muted"
               data-tippy-content={i18n.t("superscript")}
               aria-label={i18n.t("superscript")}
               onClick={linkEvent(this, this.handleInsertSuperscript)}
@@ -298,7 +317,7 @@ export class MarkdownTextArea extends Component<
               <Icon icon="superscript" classes="icon-inline" />
             </button>
             <button
-              class="btn btn-sm text-muted"
+              className="btn btn-sm text-muted"
               data-tippy-content={i18n.t("spoiler")}
               aria-label={i18n.t("spoiler")}
               onClick={linkEvent(this, this.handleInsertSpoiler)}
@@ -307,7 +326,7 @@ export class MarkdownTextArea extends Component<
             </button>
             <a
               href={markdownHelpUrl}
-              class="btn btn-sm text-muted font-weight-bold"
+              className="btn btn-sm text-muted font-weight-bold"
               title={i18n.t("formatting_help")}
               rel={relTags}
             >
@@ -338,8 +357,7 @@ export class MarkdownTextArea extends Component<
     const formData = new FormData();
     formData.append("images[]", file);
 
-    i.state.imageLoading = true;
-    i.setState(i.state);
+    i.setState({ imageLoading: true });
 
     fetch(pictrsUri, {
       method: "POST",
@@ -355,15 +373,16 @@ export class MarkdownTextArea extends Component<
           let deleteToken = res.files[0].delete_token;
           let deleteUrl = `${pictrsUri}/delete/${deleteToken}/${hash}`;
           let imageMarkdown = `![](${url})`;
-          i.state.content = Some(
-            i.state.content.match({
-              some: content => `${content}\n${imageMarkdown}`,
-              none: imageMarkdown,
-            })
-          );
-          i.state.imageLoading = false;
+          i.setState({
+            content: Some(
+              i.state.content.match({
+                some: content => `${content}\n${imageMarkdown}`,
+                none: imageMarkdown,
+              })
+            ),
+            imageLoading: false,
+          });
           i.contentChange();
-          i.setState(i.state);
           let textarea: any = document.getElementById(i.id);
           autosize.update(textarea);
           pictrsDeleteToast(
@@ -373,14 +392,12 @@ export class MarkdownTextArea extends Component<
             deleteUrl
           );
         } else {
-          i.state.imageLoading = false;
-          i.setState(i.state);
+          i.setState({ imageLoading: false });
           toast(JSON.stringify(res), "danger");
         }
       })
       .catch(error => {
-        i.state.imageLoading = false;
-        i.setState(i.state);
+        i.setState({ imageLoading: false });
         console.error(error);
         toast(error, "danger");
       });
@@ -393,22 +410,27 @@ export class MarkdownTextArea extends Component<
   }
 
   handleContentChange(i: MarkdownTextArea, event: any) {
-    i.state.content = Some(event.target.value);
+    i.setState({ content: Some(event.target.value) });
     i.contentChange();
-    i.setState(i.state);
   }
 
   handlePreviewToggle(i: MarkdownTextArea, event: any) {
     event.preventDefault();
-    i.state.previewMode = !i.state.previewMode;
-    i.setState(i.state);
+    i.setState({ previewMode: !i.state.previewMode });
+  }
+
+  handleLanguageChange(val: number[]) {
+    this.setState({ languageId: Some(val[0]) });
   }
 
   handleSubmit(i: MarkdownTextArea, event: any) {
     event.preventDefault();
-    i.state.loading = true;
-    i.setState(i.state);
-    let msg = { val: toUndefined(i.state.content), formId: i.formId };
+    i.setState({ loading: true });
+    let msg = {
+      val: i.state.content,
+      formId: i.formId,
+      languageId: i.state.languageId,
+    };
     i.props.onSubmit(msg);
   }
 
@@ -424,27 +446,28 @@ export class MarkdownTextArea extends Component<
     let end: number = textarea.selectionEnd;
 
     if (i.state.content.isNone()) {
-      i.state.content = Some("");
+      i.setState({ content: Some("") });
     }
 
     let content = i.state.content.unwrap();
 
     if (start !== end) {
       let selectedText = content.substring(start, end);
-      i.state.content = Some(
-        `${content.substring(0, start)}[${selectedText}]()${content.substring(
-          end
-        )}`
-      );
+      i.setState({
+        content: Some(
+          `${content.substring(0, start)}[${selectedText}]()${content.substring(
+            end
+          )}`
+        ),
+      });
       textarea.focus();
       setTimeout(() => (textarea.selectionEnd = end + 3), 10);
     } else {
-      i.state.content = Some(`${content} []()`);
+      i.setState({ content: Some(`${content} []()`) });
       textarea.focus();
       setTimeout(() => (textarea.selectionEnd -= 1), 10);
     }
     i.contentChange();
-    i.setState(i.state);
   }
 
   simpleSurround(chars: string) {
@@ -461,7 +484,7 @@ export class MarkdownTextArea extends Component<
     emptyChars = "___"
   ) {
     if (this.state.content.isNone()) {
-      this.state.content = Some("");
+      this.setState({ content: Some("") });
     }
     let textarea: any = document.getElementById(this.id);
     let start: number = textarea.selectionStart;
@@ -471,19 +494,20 @@ export class MarkdownTextArea extends Component<
 
     if (start !== end) {
       let selectedText = content.substring(start, end);
-      this.state.content = Some(
-        `${content.substring(
-          0,
-          start
-        )}${beforeChars}${selectedText}${afterChars}${content.substring(end)}`
-      );
+      this.setState({
+        content: Some(
+          `${content.substring(
+            0,
+            start
+          )}${beforeChars}${selectedText}${afterChars}${content.substring(end)}`
+        ),
+      });
     } else {
-      this.state.content = Some(
-        `${content}${beforeChars}${emptyChars}${afterChars}`
-      );
+      this.setState({
+        content: Some(`${content}${beforeChars}${emptyChars}${afterChars}`),
+      });
     }
     this.contentChange();
-    this.setState(this.state);
 
     textarea.focus();
 
@@ -555,9 +579,11 @@ export class MarkdownTextArea extends Component<
 
   simpleInsert(chars: string) {
     if (this.state.content.isNone()) {
-      this.state.content = Some(`${chars} `);
+      this.setState({ content: Some(`${chars} `) });
     } else {
-      this.state.content = Some(`${this.state.content.unwrap()}\n${chars} `);
+      this.setState({
+        content: Some(`${this.state.content.unwrap()}\n${chars} `),
+      });
     }
 
     let textarea: any = document.getElementById(this.id);
@@ -566,7 +592,6 @@ export class MarkdownTextArea extends Component<
       autosize.update(textarea);
     }, 10);
     this.contentChange();
-    this.setState(this.state);
   }
 
   handleInsertSpoiler(i: MarkdownTextArea, event: any) {
@@ -586,13 +611,14 @@ export class MarkdownTextArea extends Component<
           .map(t => `> ${t}`)
           .join("\n") + "\n\n";
       if (this.state.content.isNone()) {
-        this.state.content = Some("");
+        this.setState({ content: Some("") });
       } else {
-        this.state.content = Some(`${this.state.content.unwrap()}\n`);
+        this.setState({ content: Some(`${this.state.content.unwrap()}\n`) });
       }
-      this.state.content = Some(`${this.state.content.unwrap()}${quotedText}`);
+      this.setState({
+        content: Some(`${this.state.content.unwrap()}${quotedText}`),
+      });
       this.contentChange();
-      this.setState(this.state);
       // Not sure why this needs a delay
       setTimeout(() => autosize.update(textarea), 10);
     }