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,
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";
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;
// 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 id={this.formId} onSubmit={linkEvent(this, this.handleSubmit)}>
+ <form
+ className="markdown-textarea"
+ id={this.formId}
+ onSubmit={linkEvent(this, this.handleSubmit)}
+ >
<NavigationPrompt
when={
!this.props.hideNavigationWarnings &&
<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 />
name="file"
className="d-none"
multiple
- disabled={
- !UserService.Instance.myUserInfo || this.isDisabled
- }
+ disabled={!UserService.Instance.myUserInfo}
onChange={linkEvent(this, this.handleImageUpload)}
/>
</form>
{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" />
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>
{/* 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>
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>
if (files.length > maxUploadImages) {
toast(
- i18n.t("too_many_images_upload", {
+ I18NextService.i18n.t("too_many_images_upload", {
count: Number(maxUploadImages),
formattedCount: numToSI(maxUploadImages),
}),
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);
}
// 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);
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);
}
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}`,
});