]> Untitled Git - lemmy-ui.git/blobdiff - src/shared/components/common/markdown-textarea.tsx
Merge branch 'main' into fix/fix-badges-spacing-componentize
[lemmy-ui.git] / src / shared / components / common / markdown-textarea.tsx
index 7992c72b967060fa7de9ce66600747bc475a1902..1a707a23455550ca438d42a964e52bf68c0e4455 100644 (file)
@@ -1,26 +1,21 @@
+import { isBrowser } from "@utils/browser";
+import { numToSI, randomStr } from "@utils/helpers";
 import autosize from "autosize";
 import classNames from "classnames";
 import { NoOptionI18nKeys } from "i18next";
 import { Component, linkEvent } from "inferno";
 import { Language } from "lemmy-js-client";
-import { i18n } from "../../i18next";
-import { HttpService, UserService } from "../../services";
 import {
   concurrentImageUpload,
-  customEmojisLookup,
-  isBrowser,
   markdownFieldCharacterLimit,
   markdownHelpUrl,
   maxUploadImages,
-  mdToHtml,
-  numToSI,
-  pictrsDeleteToast,
-  randomStr,
   relTags,
-  setupTippy,
-  setupTribute,
-  toast,
-} from "../../utils";
+} from "../../config";
+import { customEmojisLookup, mdToHtml, setupTribute } from "../../markdown";
+import { HttpService, I18NextService, UserService } from "../../services";
+import { setupTippy } from "../../tippy";
+import { pictrsDeleteToast, toast } from "../../toast";
 import { EmojiPicker } from "./emoji-picker";
 import { Icon, Spinner } from "./icon";
 import { LanguageSelect } from "./language-select";
@@ -28,15 +23,28 @@ import NavigationPrompt from "./navigation-prompt";
 import ProgressBar from "./progress-bar";
 
 interface MarkdownTextAreaProps {
+  /**
+   * Initial content inside the textarea
+   */
   initialContent?: string;
+  /**
+   * Numerical ID of the language to select in dropdown
+   */
   initialLanguageId?: number;
   placeholder?: string;
   buttonTitle?: string;
   maxLength?: number;
+  /**
+   * Whether this form is for a reply to a Private Message.
+   * If true, a "Cancel" button is shown that will close the reply.
+   */
   replyType?: boolean;
   focus?: boolean;
   disabled?: boolean;
   finished?: boolean;
+  /**
+   * Whether to show the language selector
+   */
   showLanguage?: boolean;
   hideNavigationWarnings?: boolean;
   onContentChange?(val: string): void;
@@ -133,7 +141,7 @@ export class MarkdownTextArea extends Component<
     // TODO add these prompts back in at some point
     // <Prompt
     //   when={!this.props.hideNavigationWarnings && this.state.content}
-    //   message={i18n.t("block_leaving")}
+    //   message={I18NextService.i18n.t("block_leaving")}
     // />
     return (
       <form
@@ -151,21 +159,27 @@ export class MarkdownTextArea extends Component<
         <div className="mb-3 row">
           <div className="col-12">
             <div className="rounded bg-light border">
-              <div className="d-flex flex-wrap border-bottom">
+              <div
+                className={classNames("d-flex flex-wrap border-bottom", {
+                  "no-click": 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">
+                <form className="btn btn-sm text-muted fw-bold">
                   <label
                     htmlFor={`file-upload-${this.id}`}
+                    // TODO: Fix this linting violation
+                    // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
+                    tabIndex={0}
                     className={`mb-0 ${
                       UserService.Instance.myUserInfo && "pointer"
                     }`}
-                    data-tippy-content={i18n.t("upload_image")}
+                    data-tippy-content={I18NextService.i18n.t("upload_image")}
                   >
                     {this.state.imageUploadStatus ? (
                       <Spinner />
@@ -180,9 +194,7 @@ export class MarkdownTextArea extends Component<
                     name="file"
                     className="d-none"
                     multiple
-                    disabled={
-                      !UserService.Instance.myUserInfo || this.isDisabled
-                    }
+                    disabled={!UserService.Instance.myUserInfo}
                     onChange={linkEvent(this, this.handleImageUpload)}
                   />
                 </form>
@@ -202,8 +214,8 @@ export class MarkdownTextArea extends Component<
                 {this.getFormatButton("spoiler", this.handleInsertSpoiler)}
                 <a
                   href={markdownHelpUrl}
-                  className="btn btn-sm text-muted font-weight-bold"
-                  title={i18n.t("formatting_help")}
+                  className="btn btn-sm text-muted fw-bold"
+                  title={I18NextService.i18n.t("formatting_help")}
                   rel={relTags}
                 >
                   <Icon icon="help-circle" classes="icon-inline" />
@@ -245,15 +257,17 @@ export class MarkdownTextArea extends Component<
                       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,
-                      })}
+                      text={
+                        I18NextService.i18n.t("pictures_uploded_progess", {
+                          uploaded: this.state.imageUploadStatus.uploaded,
+                          total: this.state.imageUploadStatus.total,
+                        }) ?? undefined
+                      }
                     />
                   )}
               </div>
               <label className="visually-hidden" htmlFor={this.id}>
-                {i18n.t("body")}
+                {I18NextService.i18n.t("body")}
               </label>
             </div>
           </div>
@@ -275,36 +289,35 @@ export class MarkdownTextArea extends Component<
             {/* A flex expander */}
             <div className="flex-grow-1"></div>
 
-            {this.props.buttonTitle && (
-              <button
-                type="submit"
-                className="btn btn-sm btn-secondary ms-2"
-                disabled={this.isDisabled}
-              >
-                {this.state.loading ? (
-                  <Spinner />
-                ) : (
-                  <span>{this.props.buttonTitle}</span>
-                )}
-              </button>
-            )}
             {this.props.replyType && (
               <button
                 type="button"
                 className="btn btn-sm btn-secondary ms-2"
                 onClick={linkEvent(this, this.handleReplyCancel)}
               >
-                {i18n.t("cancel")}
+                {I18NextService.i18n.t("cancel")}
               </button>
             )}
-            {this.state.content && (
+            <button
+              type="button"
+              disabled={!this.state.content}
+              className={classNames("btn btn-sm btn-secondary ms-2", {
+                active: this.state.previewMode,
+              })}
+              onClick={linkEvent(this, this.handlePreviewToggle)}
+            >
+              {this.state.previewMode
+                ? I18NextService.i18n.t("edit")
+                : I18NextService.i18n.t("preview")}
+            </button>
+            {this.props.buttonTitle && (
               <button
-                className={`btn btn-sm btn-secondary ms-2 ${
-                  this.state.previewMode && "active"
-                }`}
-                onClick={linkEvent(this, this.handlePreviewToggle)}
+                type="submit"
+                className="btn btn-sm btn-secondary ms-2"
+                disabled={this.isDisabled || !this.state.content}
               >
-                {this.state.previewMode ? i18n.t("edit") : i18n.t("preview")}
+                {this.state.loading && <Spinner className="me-1" />}
+                {this.props.buttonTitle}
               </button>
             )}
           </div>
@@ -336,10 +349,9 @@ export class MarkdownTextArea extends Component<
     return (
       <button
         className="btn btn-sm text-muted"
-        data-tippy-content={i18n.t(type)}
-        aria-label={i18n.t(type)}
+        data-tippy-content={I18NextService.i18n.t(type)}
+        aria-label={I18NextService.i18n.t(type)}
         onClick={linkEvent(this, handleClick)}
-        disabled={this.isDisabled}
       >
         <Icon icon={iconType} classes="icon-inline" />
       </button>
@@ -380,7 +392,7 @@ export class MarkdownTextArea extends Component<
 
     if (files.length > maxUploadImages) {
       toast(
-        i18n.t("too_many_images_upload", {
+        I18NextService.i18n.t("too_many_images_upload", {
           count: Number(maxUploadImages),
           formattedCount: numToSI(maxUploadImages),
         }),
@@ -434,6 +446,10 @@ export class MarkdownTextArea extends Component<
         const textarea: any = document.getElementById(i.id);
         autosize.update(textarea);
         pictrsDeleteToast(image.name, res.data.delete_url as string);
+      } else if (res.data.msg === "too_large") {
+        toast(I18NextService.i18n.t("upload_too_large"), "danger");
+        i.setState({ imageUploadStatus: undefined });
+        throw JSON.stringify(res.data);
       } else {
         throw JSON.stringify(res.data);
       }
@@ -460,7 +476,7 @@ export class MarkdownTextArea extends Component<
   // Keybind handler
   // Keybinds inspired by github comment area
   handleKeyBinds(i: MarkdownTextArea, event: KeyboardEvent) {
-    if (event.ctrlKey) {
+    if (event.ctrlKey || event.metaKey) {
       switch (event.key) {
         case "k": {
           i.handleInsertLink(i, event);
@@ -681,7 +697,7 @@ export class MarkdownTextArea extends Component<
 
   handleInsertSpoiler(i: MarkdownTextArea, event: any) {
     event.preventDefault();
-    const beforeChars = `\n::: spoiler ${i18n.t("spoiler")}\n`;
+    const beforeChars = `\n::: spoiler ${I18NextService.i18n.t("spoiler")}\n`;
     const afterChars = "\n:::\n";
     i.simpleSurroundBeforeAfter(beforeChars, afterChars);
   }
@@ -689,18 +705,20 @@ export class MarkdownTextArea extends Component<
   quoteInsert() {
     const textarea: any = document.getElementById(this.id);
     const selectedText = window.getSelection()?.toString();
-    const { content } = this.state;
+    let { content } = this.state;
     if (selectedText) {
       const quotedText =
         selectedText
           .split("\n")
           .map(t => `> ${t}`)
           .join("\n") + "\n\n";
+
       if (!content) {
-        this.setState({ content: "" });
+        content = "";
       } else {
-        this.setState({ content: `${content}\n` });
+        content = `${content}\n\n`;
       }
+
       this.setState({
         content: `${content}${quotedText}`,
       });