]> Untitled Git - lemmy-ui.git/blobdiff - src/shared/components/common/markdown-textarea.tsx
Merge branch 'main' into feat/markdown-format-bar-above
[lemmy-ui.git] / src / shared / components / common / markdown-textarea.tsx
index 38a2c2ed5ae7c30c5ac1332598430880d0e39a1b..ef7ba0187df3d4a7e9c0916eb0c5fd5df22672c6 100644 (file)
@@ -1,10 +1,10 @@
 import autosize from "autosize";
+import classNames from "classnames";
 import { NoOptionI18nKeys } from "i18next";
 import { Component, linkEvent } from "inferno";
-import { Prompt } from "inferno-router";
 import { Language } from "lemmy-js-client";
 import { i18n } from "../../i18next";
-import { UserService } from "../../services";
+import { HttpService, UserService } from "../../services";
 import {
   concurrentImageUpload,
   customEmojisLookup,
@@ -20,11 +20,11 @@ import {
   setupTippy,
   setupTribute,
   toast,
-  uploadImage,
 } from "../../utils";
 import { EmojiPicker } from "./emoji-picker";
 import { Icon, Spinner } from "./icon";
 import { LanguageSelect } from "./language-select";
+import NavigationPrompt from "./navigation-prompt";
 import ProgressBar from "./progress-bar";
 
 interface MarkdownTextAreaProps {
@@ -39,9 +39,9 @@ interface MarkdownTextAreaProps {
   finished?: boolean;
   showLanguage?: boolean;
   hideNavigationWarnings?: boolean;
-  onContentChange?(val: string): any;
-  onReplyCancel?(): any;
-  onSubmit?(msg: { val?: string; formId: string; languageId?: number }): any;
+  onContentChange?(val: string): void;
+  onReplyCancel?(): void;
+  onSubmit?(content: string, formId: string, languageId?: number): void;
   allLanguages: Language[]; // TODO should probably be nullable
   siteLanguages: number[]; // TODO same
 }
@@ -55,16 +55,18 @@ interface MarkdownTextAreaState {
   content?: string;
   languageId?: number;
   previewMode: boolean;
-  loading: boolean;
   imageUploadStatus?: ImageUploadStatus;
+  loading: boolean;
+  submitted: boolean;
 }
 
 export class MarkdownTextArea extends Component<
   MarkdownTextAreaProps,
   MarkdownTextAreaState
 > {
-  private id = `comment-textarea-${randomStr()}`;
-  private formId = `comment-form-${randomStr()}`;
+  private id = `markdown-textarea-${randomStr()}`;
+  private formId = `markdown-form-${randomStr()}`;
+
   private tribute: any;
 
   state: MarkdownTextAreaState = {
@@ -72,6 +74,7 @@ export class MarkdownTextArea extends Component<
     languageId: this.props.initialLanguageId,
     previewMode: false,
     loading: false,
+    submitted: false,
   };
 
   constructor(props: any, context: any) {
@@ -105,17 +108,14 @@ export class MarkdownTextArea extends Component<
     }
   }
 
-  componentDidUpdate() {
-    if (!this.props.hideNavigationWarnings && this.state.content) {
-      window.onbeforeunload = () => true;
-    } else {
-      window.onbeforeunload = null;
-    }
-  }
-
   componentWillReceiveProps(nextProps: MarkdownTextAreaProps) {
     if (nextProps.finished) {
-      this.setState({ previewMode: false, loading: false, content: undefined });
+      this.setState({
+        previewMode: false,
+        imageUploadStatus: undefined,
+        loading: false,
+        content: undefined,
+      });
       if (this.props.replyType) {
         this.props.onReplyCancel?.();
       }
@@ -127,65 +127,161 @@ export class MarkdownTextArea extends Component<
     }
   }
 
-  componentWillUnmount() {
-    window.onbeforeunload = null;
-  }
-
   render() {
     const languageId = this.state.languageId;
 
+    // TODO add these prompts back in at some point
+    // <Prompt
+    //   when={!this.props.hideNavigationWarnings && this.state.content}
+    //   message={i18n.t("block_leaving")}
+    // />
     return (
       <form id={this.formId} onSubmit={linkEvent(this, this.handleSubmit)}>
-        <Prompt
-          when={!this.props.hideNavigationWarnings && this.state.content}
-          message={i18n.t("block_leaving")}
+        <NavigationPrompt
+          when={
+            !this.props.hideNavigationWarnings &&
+            !!this.state.content &&
+            !this.state.submitted
+          }
         />
         <div className="form-group row">
-          <div className={`col-sm-12`}>
-            <textarea
-              id={this.id}
-              className={`form-control ${this.state.previewMode && "d-none"}`}
-              value={this.state.content}
-              onInput={linkEvent(this, this.handleContentChange)}
-              onPaste={linkEvent(this, this.handleImageUploadPaste)}
-              onKeyDown={linkEvent(this, this.handleKeyBinds)}
-              required
-              disabled={this.isDisabled}
-              rows={2}
-              maxLength={this.props.maxLength ?? markdownFieldCharacterLimit}
-              placeholder={this.props.placeholder}
-            />
-            {this.state.previewMode && this.state.content && (
+          <div className="col-12">
+            <div
+              className="rounded bg-light overflow-hidden"
+              style={{
+                border: "1px solid var(--medium-light)",
+              }}
+            >
               <div
-                className="card border-secondary card-body md-div"
-                dangerouslySetInnerHTML={mdToHtml(this.state.content)}
-              />
-            )}
-            {this.state.imageUploadStatus &&
-              this.state.imageUploadStatus.total > 1 && (
-                <ProgressBar
-                  className="mt-2"
-                  striped
-                  animated
-                  value={this.state.imageUploadStatus.uploaded}
-                  max={this.state.imageUploadStatus.total}
-                  text={i18n.t("pictures_uploded_progess", {
-                    uploaded: this.state.imageUploadStatus.uploaded,
-                    total: this.state.imageUploadStatus.total,
+                className="d-flex flex-wrap"
+                style={{
+                  "border-bottom": "1px solid var(--medium-light)",
+                }}
+              >
+                {this.getFormatButton("bold", this.handleInsertBold)}
+                {this.getFormatButton("italic", this.handleInsertItalic)}
+                {this.getFormatButton("link", this.handleInsertLink)}
+                <EmojiPicker
+                  onEmojiClick={e => this.handleEmoji(this, e)}
+                  disabled={this.isDisabled}
+                ></EmojiPicker>
+                <form className="btn btn-sm text-muted font-weight-bold">
+                  <label
+                    htmlFor={`file-upload-${this.id}`}
+                    className={`mb-0 ${
+                      UserService.Instance.myUserInfo && "pointer"
+                    }`}
+                    data-tippy-content={i18n.t("upload_image")}
+                  >
+                    {this.state.imageUploadStatus ? (
+                      <Spinner />
+                    ) : (
+                      <Icon icon="image" classes="icon-inline" />
+                    )}
+                  </label>
+                  <input
+                    id={`file-upload-${this.id}`}
+                    type="file"
+                    accept="image/*,video/*"
+                    name="file"
+                    className="d-none"
+                    multiple
+                    disabled={
+                      !UserService.Instance.myUserInfo || this.isDisabled
+                    }
+                    onChange={linkEvent(this, this.handleImageUpload)}
+                  />
+                </form>
+                {this.getFormatButton("header", this.handleInsertHeader)}
+                {this.getFormatButton(
+                  "strikethrough",
+                  this.handleInsertStrikethrough
+                )}
+                {this.getFormatButton("quote", this.handleInsertQuote)}
+                {this.getFormatButton("list", this.handleInsertList)}
+                {this.getFormatButton("code", this.handleInsertCode)}
+                {this.getFormatButton("subscript", this.handleInsertSubscript)}
+                {this.getFormatButton(
+                  "superscript",
+                  this.handleInsertSuperscript
+                )}
+                {this.getFormatButton("spoiler", this.handleInsertSpoiler)}
+                <a
+                  href={markdownHelpUrl}
+                  className="btn btn-sm text-muted font-weight-bold"
+                  title={i18n.t("formatting_help")}
+                  rel={relTags}
+                >
+                  <Icon icon="help-circle" classes="icon-inline" />
+                </a>
+              </div>
+
+              <div>
+                <textarea
+                  id={this.id}
+                  className={classNames("form-control border-0 rounded-0", {
+                    "d-none": this.state.previewMode,
                   })}
+                  value={this.state.content}
+                  onInput={linkEvent(this, this.handleContentChange)}
+                  onPaste={linkEvent(this, this.handleImageUploadPaste)}
+                  onKeyDown={linkEvent(this, this.handleKeyBinds)}
+                  required
+                  disabled={this.isDisabled}
+                  rows={2}
+                  maxLength={
+                    this.props.maxLength ?? markdownFieldCharacterLimit
+                  }
+                  placeholder={this.props.placeholder}
                 />
-              )}
+                {this.state.previewMode && this.state.content && (
+                  <div
+                    className="card border-secondary card-body md-div"
+                    dangerouslySetInnerHTML={mdToHtml(this.state.content)}
+                  />
+                )}
+                {this.state.imageUploadStatus &&
+                  this.state.imageUploadStatus.total > 1 && (
+                    <ProgressBar
+                      className="mt-2"
+                      striped
+                      animated
+                      value={this.state.imageUploadStatus.uploaded}
+                      max={this.state.imageUploadStatus.total}
+                      text={i18n.t("pictures_uploded_progess", {
+                        uploaded: this.state.imageUploadStatus.uploaded,
+                        total: this.state.imageUploadStatus.total,
+                      })}
+                    />
+                  )}
+              </div>
+              <label className="sr-only" htmlFor={this.id}>
+                {i18n.t("body")}
+              </label>
+            </div>
           </div>
-          <label className="sr-only" htmlFor={this.id}>
-            {i18n.t("body")}
-          </label>
-        </div>
-        <div className="row">
-          <div className="col-sm-12 d-flex flex-wrap">
+
+          <div className="col-12 d-flex align-items-center flex-wrap mt-2">
+            {this.props.showLanguage && (
+              <LanguageSelect
+                iconVersion
+                allLanguages={this.props.allLanguages}
+                selectedLanguageIds={
+                  languageId ? Array.of(languageId) : undefined
+                }
+                siteLanguages={this.props.siteLanguages}
+                onChange={this.handleLanguageChange}
+                disabled={this.isDisabled}
+              />
+            )}
+
+            {/* A flex expander */}
+            <div className="flex-grow-1"></div>
+
             {this.props.buttonTitle && (
               <button
                 type="submit"
-                className="btn btn-sm btn-secondary mr-2"
+                className="btn btn-sm btn-secondary ml-2"
                 disabled={this.isDisabled}
               >
                 {this.state.loading ? (
@@ -198,7 +294,7 @@ export class MarkdownTextArea extends Component<
             {this.props.replyType && (
               <button
                 type="button"
-                className="btn btn-sm btn-secondary mr-2"
+                className="btn btn-sm btn-secondary ml-2"
                 onClick={linkEvent(this, this.handleReplyCancel)}
               >
                 {i18n.t("cancel")}
@@ -206,7 +302,7 @@ export class MarkdownTextArea extends Component<
             )}
             {this.state.content && (
               <button
-                className={`btn btn-sm btn-secondary mr-2 ${
+                className={`btn btn-sm btn-secondary ml-2 ${
                   this.state.previewMode && "active"
                 }`}
                 onClick={linkEvent(this, this.handlePreviewToggle)}
@@ -214,72 +310,6 @@ export class MarkdownTextArea extends Component<
                 {this.state.previewMode ? i18n.t("edit") : i18n.t("preview")}
               </button>
             )}
-            {/* A flex expander */}
-            <div className="flex-grow-1"></div>
-
-            {this.props.showLanguage && (
-              <LanguageSelect
-                iconVersion
-                allLanguages={this.props.allLanguages}
-                selectedLanguageIds={
-                  languageId ? Array.of(languageId) : undefined
-                }
-                siteLanguages={this.props.siteLanguages}
-                onChange={this.handleLanguageChange}
-                disabled={this.isDisabled}
-              />
-            )}
-            {this.getFormatButton("bold", this.handleInsertBold)}
-            {this.getFormatButton("italic", this.handleInsertItalic)}
-            {this.getFormatButton("link", this.handleInsertLink)}
-            <EmojiPicker
-              onEmojiClick={e => this.handleEmoji(this, e)}
-              disabled={this.isDisabled}
-            ></EmojiPicker>
-            <form className="btn btn-sm text-muted font-weight-bold">
-              <label
-                htmlFor={`file-upload-${this.id}`}
-                className={`mb-0 ${
-                  UserService.Instance.myUserInfo && "pointer"
-                }`}
-                data-tippy-content={i18n.t("upload_image")}
-              >
-                {this.state.imageUploadStatus ? (
-                  <Spinner />
-                ) : (
-                  <Icon icon="image" classes="icon-inline" />
-                )}
-              </label>
-              <input
-                id={`file-upload-${this.id}`}
-                type="file"
-                accept="image/*,video/*"
-                name="file"
-                className="d-none"
-                multiple
-                disabled={!UserService.Instance.myUserInfo || this.isDisabled}
-                onChange={linkEvent(this, this.handleImageUpload)}
-              />
-            </form>
-            {this.getFormatButton("header", this.handleInsertHeader)}
-            {this.getFormatButton(
-              "strikethrough",
-              this.handleInsertStrikethrough
-            )}
-            {this.getFormatButton("quote", this.handleInsertQuote)}
-            {this.getFormatButton("list", this.handleInsertList)}
-            {this.getFormatButton("code", this.handleInsertCode)}
-            {this.getFormatButton("subscript", this.handleInsertSubscript)}
-            {this.getFormatButton("superscript", this.handleInsertSuperscript)}
-            {this.getFormatButton("spoiler", this.handleInsertSpoiler)}
-            <a
-              href={markdownHelpUrl}
-              className="btn btn-sm text-muted font-weight-bold"
-              title={i18n.t("formatting_help")}
-              rel={relTags}
-            >
-              <Icon icon="help-circle" classes="icon-inline" />
-            </a>
           </div>
         </div>
       </form>
@@ -393,29 +423,29 @@ export class MarkdownTextArea extends Component<
     }
   }
 
-  async uploadSingleImage(i: MarkdownTextArea, file: File) {
-    try {
-      const res = await uploadImage(file);
-      console.log("pictrs upload:");
-      console.log(res);
-      if (res.msg === "ok") {
-        const imageMarkdown = `![](${res.url})`;
+  async uploadSingleImage(i: MarkdownTextArea, image: File) {
+    const res = await HttpService.client.uploadImage({ image });
+    console.log("pictrs upload:");
+    console.log(res);
+    if (res.state === "success") {
+      if (res.data.msg === "ok") {
+        const imageMarkdown = `![](${res.data.url})`;
         i.setState(({ content }) => ({
           content: content ? `${content}\n${imageMarkdown}` : imageMarkdown,
         }));
         i.contentChange();
         const textarea: any = document.getElementById(i.id);
         autosize.update(textarea);
-        pictrsDeleteToast(file.name, res.delete_url as string);
+        pictrsDeleteToast(image.name, res.data.delete_url as string);
       } else {
-        throw JSON.stringify(res);
+        throw JSON.stringify(res.data);
       }
-    } catch (error) {
+    } else if (res.state === "failed") {
       i.setState({ imageUploadStatus: undefined });
-      console.error(error);
-      toast(error, "danger");
+      console.error(res.msg);
+      toast(res.msg, "danger");
 
-      throw error;
+      throw res.msg;
     }
   }
 
@@ -489,13 +519,10 @@ export class MarkdownTextArea extends Component<
 
   handleSubmit(i: MarkdownTextArea, event: any) {
     event.preventDefault();
-    i.setState({ loading: true });
-    const msg = {
-      val: i.state.content,
-      formId: i.formId,
-      languageId: i.state.languageId,
-    };
-    i.props.onSubmit?.(msg);
+    if (i.state.content) {
+      i.setState({ loading: true, submitted: true });
+      i.props.onSubmit?.(i.state.content, i.formId, i.state.languageId);
+    }
   }
 
   handleReplyCancel(i: MarkdownTextArea) {