Adding markdown buttons. Fixes #977
This commit is contained in:
parent
2d4c41d2be
commit
d1991d8644
8 changed files with 618 additions and 448 deletions
290
ui/src/components/comment-form.tsx
vendored
290
ui/src/components/comment-form.tsx
vendored
|
@ -1,8 +1,7 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Component } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
import { Prompt } from 'inferno-router';
|
||||
import {
|
||||
CommentNode as CommentNodeI,
|
||||
CommentForm as CommentFormI,
|
||||
|
@ -10,22 +9,11 @@ import {
|
|||
UserOperation,
|
||||
CommentResponse,
|
||||
} from '../interfaces';
|
||||
import {
|
||||
capitalizeFirstLetter,
|
||||
mdToHtml,
|
||||
randomStr,
|
||||
markdownHelpUrl,
|
||||
toast,
|
||||
setupTribute,
|
||||
wsJsonToRes,
|
||||
pictrsDeleteToast,
|
||||
} from '../utils';
|
||||
import { capitalizeFirstLetter, wsJsonToRes } from '../utils';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import autosize from 'autosize';
|
||||
import Tribute from 'tributejs/src/Tribute.js';
|
||||
import emojiShortName from 'emoji-short-name';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
import { MarkdownTextArea } from './markdown-textarea';
|
||||
|
||||
interface CommentFormProps {
|
||||
postId?: number;
|
||||
|
@ -39,15 +27,10 @@ interface CommentFormProps {
|
|||
interface CommentFormState {
|
||||
commentForm: CommentFormI;
|
||||
buttonTitle: string;
|
||||
previewMode: boolean;
|
||||
loading: boolean;
|
||||
imageLoading: boolean;
|
||||
finished: boolean;
|
||||
}
|
||||
|
||||
export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
private id = `comment-textarea-${randomStr()}`;
|
||||
private formId = `comment-form-${randomStr()}`;
|
||||
private tribute: Tribute;
|
||||
private subscription: Subscription;
|
||||
private emptyState: CommentFormState = {
|
||||
commentForm: {
|
||||
|
@ -65,15 +48,14 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
: this.props.edit
|
||||
? capitalizeFirstLetter(i18n.t('save'))
|
||||
: capitalizeFirstLetter(i18n.t('reply')),
|
||||
previewMode: false,
|
||||
loading: false,
|
||||
imageLoading: false,
|
||||
finished: false,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.tribute = setupTribute();
|
||||
this.handleCommentSubmit = this.handleCommentSubmit.bind(this);
|
||||
this.handleReplyCancel = this.handleReplyCancel.bind(this);
|
||||
|
||||
this.state = this.emptyState;
|
||||
|
||||
|
@ -98,160 +80,24 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let textarea: any = document.getElementById(this.id);
|
||||
if (textarea) {
|
||||
autosize(textarea);
|
||||
this.tribute.attach(textarea);
|
||||
textarea.addEventListener('tribute-replaced', () => {
|
||||
this.state.commentForm.content = textarea.value;
|
||||
this.setState(this.state);
|
||||
autosize.update(textarea);
|
||||
});
|
||||
|
||||
// Quoting of selected text
|
||||
let selectedText = window.getSelection().toString();
|
||||
if (selectedText) {
|
||||
let quotedText =
|
||||
selectedText
|
||||
.split('\n')
|
||||
.map(t => `> ${t}`)
|
||||
.join('\n') + '\n\n';
|
||||
this.state.commentForm.content = quotedText;
|
||||
this.setState(this.state);
|
||||
// Not sure why this needs a delay
|
||||
setTimeout(() => autosize.update(textarea), 10);
|
||||
}
|
||||
|
||||
if (this.props.focus) {
|
||||
textarea.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.state.commentForm.content) {
|
||||
window.onbeforeunload = () => true;
|
||||
} else {
|
||||
window.onbeforeunload = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscription.unsubscribe();
|
||||
window.onbeforeunload = null;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="mb-3">
|
||||
<Prompt
|
||||
when={this.state.commentForm.content}
|
||||
message={i18n.t('block_leaving')}
|
||||
/>
|
||||
{UserService.Instance.user ? (
|
||||
<form
|
||||
id={this.formId}
|
||||
onSubmit={linkEvent(this, this.handleCommentSubmit)}
|
||||
>
|
||||
<div class="form-group row">
|
||||
<div className={`col-sm-12`}>
|
||||
<textarea
|
||||
id={this.id}
|
||||
className={`form-control ${
|
||||
this.state.previewMode && 'd-none'
|
||||
}`}
|
||||
value={this.state.commentForm.content}
|
||||
onInput={linkEvent(this, this.handleCommentContentChange)}
|
||||
onPaste={linkEvent(this, this.handleImageUploadPaste)}
|
||||
required
|
||||
disabled={this.props.disabled}
|
||||
rows={2}
|
||||
maxLength={10000}
|
||||
/>
|
||||
{this.state.previewMode && (
|
||||
<div
|
||||
className="card card-body md-div"
|
||||
dangerouslySetInnerHTML={mdToHtml(
|
||||
this.state.commentForm.content
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-sm btn-secondary mr-2"
|
||||
disabled={this.props.disabled || this.state.loading}
|
||||
>
|
||||
{this.state.loading ? (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
) : (
|
||||
<span>{this.state.buttonTitle}</span>
|
||||
)}
|
||||
</button>
|
||||
{this.state.commentForm.content && (
|
||||
<button
|
||||
className={`btn btn-sm mr-2 btn-secondary ${
|
||||
this.state.previewMode && 'active'
|
||||
}`}
|
||||
onClick={linkEvent(this, this.handlePreviewToggle)}
|
||||
>
|
||||
{i18n.t('preview')}
|
||||
</button>
|
||||
)}
|
||||
{this.props.node && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-secondary mr-2"
|
||||
onClick={linkEvent(this, this.handleReplyCancel)}
|
||||
>
|
||||
{i18n.t('cancel')}
|
||||
</button>
|
||||
)}
|
||||
<a
|
||||
href={markdownHelpUrl}
|
||||
target="_blank"
|
||||
class="d-inline-block float-right text-muted font-weight-bold"
|
||||
title={i18n.t('formatting_help')}
|
||||
rel="noopener"
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-help-circle"></use>
|
||||
</svg>
|
||||
</a>
|
||||
<form class="d-inline-block mr-3 float-right text-muted font-weight-bold">
|
||||
<label
|
||||
htmlFor={`file-upload-${this.id}`}
|
||||
className={`${UserService.Instance.user && 'pointer'}`}
|
||||
data-tippy-content={i18n.t('upload_image')}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-image"></use>
|
||||
</svg>
|
||||
</label>
|
||||
<input
|
||||
id={`file-upload-${this.id}`}
|
||||
type="file"
|
||||
accept="image/*,video/*"
|
||||
name="file"
|
||||
class="d-none"
|
||||
disabled={!UserService.Instance.user}
|
||||
onChange={linkEvent(this, this.handleImageUpload)}
|
||||
/>
|
||||
</form>
|
||||
{this.state.imageLoading && (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<MarkdownTextArea
|
||||
initialContent={this.state.commentForm.content}
|
||||
buttonTitle={this.state.buttonTitle}
|
||||
finished={this.state.finished}
|
||||
replyType={!!this.props.node}
|
||||
focus={this.props.focus}
|
||||
disabled={this.props.disabled}
|
||||
onSubmit={this.handleCommentSubmit}
|
||||
onReplyCancel={this.handleReplyCancel}
|
||||
/>
|
||||
) : (
|
||||
<div class="alert alert-light" role="alert">
|
||||
<svg class="icon icon-inline mr-2">
|
||||
|
@ -290,107 +136,23 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
|||
op == UserOperation.EditComment &&
|
||||
data.comment.content)
|
||||
) {
|
||||
this.state.previewMode = false;
|
||||
this.state.loading = false;
|
||||
this.state.commentForm.content = '';
|
||||
this.setState(this.state);
|
||||
let form: any = document.getElementById(this.formId);
|
||||
form.reset();
|
||||
if (this.props.node) {
|
||||
this.props.onReplyCancel();
|
||||
}
|
||||
autosize.update(form);
|
||||
this.state.finished = true;
|
||||
this.setState(this.state);
|
||||
}
|
||||
}
|
||||
|
||||
handleCommentSubmit(i: CommentForm, event: any) {
|
||||
event.preventDefault();
|
||||
if (i.props.edit) {
|
||||
WebSocketService.Instance.editComment(i.state.commentForm);
|
||||
handleCommentSubmit(val: string) {
|
||||
this.state.commentForm.content = val;
|
||||
if (this.props.edit) {
|
||||
WebSocketService.Instance.editComment(this.state.commentForm);
|
||||
} else {
|
||||
WebSocketService.Instance.createComment(i.state.commentForm);
|
||||
WebSocketService.Instance.createComment(this.state.commentForm);
|
||||
}
|
||||
|
||||
i.state.loading = true;
|
||||
i.setState(i.state);
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleCommentContentChange(i: CommentForm, event: any) {
|
||||
i.state.commentForm.content = event.target.value;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handlePreviewToggle(i: CommentForm, event: any) {
|
||||
event.preventDefault();
|
||||
i.state.previewMode = !i.state.previewMode;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleReplyCancel(i: CommentForm) {
|
||||
i.props.onReplyCancel();
|
||||
}
|
||||
|
||||
handleImageUploadPaste(i: CommentForm, event: any) {
|
||||
let image = event.clipboardData.files[0];
|
||||
if (image) {
|
||||
i.handleImageUpload(i, image);
|
||||
}
|
||||
}
|
||||
|
||||
handleImageUpload(i: CommentForm, event: any) {
|
||||
let file: any;
|
||||
if (event.target) {
|
||||
event.preventDefault();
|
||||
file = event.target.files[0];
|
||||
} else {
|
||||
file = event;
|
||||
}
|
||||
|
||||
const imageUploadUrl = `/pictrs/image`;
|
||||
const formData = new FormData();
|
||||
formData.append('images[]', file);
|
||||
|
||||
i.state.imageLoading = true;
|
||||
i.setState(i.state);
|
||||
|
||||
fetch(imageUploadUrl, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
console.log('pictrs upload:');
|
||||
console.log(res);
|
||||
if (res.msg == 'ok') {
|
||||
let hash = res.files[0].file;
|
||||
let url = `${window.location.origin}/pictrs/image/${hash}`;
|
||||
let deleteToken = res.files[0].delete_token;
|
||||
let deleteUrl = `${window.location.origin}/pictrs/image/delete/${deleteToken}/${hash}`;
|
||||
let imageMarkdown = `![](${url})`;
|
||||
let content = i.state.commentForm.content;
|
||||
content = content ? `${content}\n${imageMarkdown}` : imageMarkdown;
|
||||
i.state.commentForm.content = content;
|
||||
i.state.imageLoading = false;
|
||||
i.setState(i.state);
|
||||
let textarea: any = document.getElementById(i.id);
|
||||
autosize.update(textarea);
|
||||
pictrsDeleteToast(
|
||||
i18n.t('click_to_delete_picture'),
|
||||
i18n.t('picture_deleted'),
|
||||
deleteUrl
|
||||
);
|
||||
} else {
|
||||
i.state.imageLoading = false;
|
||||
i.setState(i.state);
|
||||
toast(JSON.stringify(res), 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
i.state.imageLoading = false;
|
||||
i.setState(i.state);
|
||||
toast(error, 'danger');
|
||||
});
|
||||
handleReplyCancel() {
|
||||
this.props.onReplyCancel();
|
||||
}
|
||||
|
||||
parseMessage(msg: WebSocketJsonResponse) {
|
||||
|
|
44
ui/src/components/community-form.tsx
vendored
44
ui/src/components/community-form.tsx
vendored
|
@ -11,18 +11,11 @@ import {
|
|||
WebSocketJsonResponse,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService } from '../services';
|
||||
import {
|
||||
wsJsonToRes,
|
||||
capitalizeFirstLetter,
|
||||
toast,
|
||||
randomStr,
|
||||
setupTribute,
|
||||
} from '../utils';
|
||||
import Tribute from 'tributejs/src/Tribute.js';
|
||||
import autosize from 'autosize';
|
||||
import { wsJsonToRes, capitalizeFirstLetter, toast, randomStr } from '../utils';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
import { Community } from '../interfaces';
|
||||
import { MarkdownTextArea } from './markdown-textarea';
|
||||
|
||||
interface CommunityFormProps {
|
||||
community?: Community; // If a community is given, that means this is an edit
|
||||
|
@ -43,7 +36,6 @@ export class CommunityForm extends Component<
|
|||
CommunityFormState
|
||||
> {
|
||||
private id = `community-form-${randomStr()}`;
|
||||
private tribute: Tribute;
|
||||
private subscription: Subscription;
|
||||
|
||||
private emptyState: CommunityFormState = {
|
||||
|
@ -60,9 +52,12 @@ export class CommunityForm extends Component<
|
|||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.tribute = setupTribute();
|
||||
this.state = this.emptyState;
|
||||
|
||||
this.handleCommunityDescriptionChange = this.handleCommunityDescriptionChange.bind(
|
||||
this
|
||||
);
|
||||
|
||||
if (this.props.community) {
|
||||
this.state.communityForm = {
|
||||
name: this.props.community.name,
|
||||
|
@ -86,17 +81,6 @@ export class CommunityForm extends Component<
|
|||
WebSocketService.Instance.listCategories();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
var textarea: any = document.getElementById(this.id);
|
||||
autosize(textarea);
|
||||
this.tribute.attach(textarea);
|
||||
textarea.addEventListener('tribute-replaced', () => {
|
||||
this.state.communityForm.description = textarea.value;
|
||||
this.setState(this.state);
|
||||
autosize.update(textarea);
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (
|
||||
!this.state.loading &&
|
||||
|
@ -171,13 +155,9 @@ export class CommunityForm extends Component<
|
|||
{i18n.t('sidebar')}
|
||||
</label>
|
||||
<div class="col-12">
|
||||
<textarea
|
||||
id={this.id}
|
||||
value={this.state.communityForm.description}
|
||||
onInput={linkEvent(this, this.handleCommunityDescriptionChange)}
|
||||
class="form-control"
|
||||
rows={3}
|
||||
maxLength={10000}
|
||||
<MarkdownTextArea
|
||||
initialContent={this.state.communityForm.description}
|
||||
onContentChange={this.handleCommunityDescriptionChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -271,9 +251,9 @@ export class CommunityForm extends Component<
|
|||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleCommunityDescriptionChange(i: CommunityForm, event: any) {
|
||||
i.state.communityForm.description = event.target.value;
|
||||
i.setState(i.state);
|
||||
handleCommunityDescriptionChange(val: string) {
|
||||
this.state.communityForm.description = val;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleCommunityCategoryChange(i: CommunityForm, event: any) {
|
||||
|
|
509
ui/src/components/markdown-textarea.tsx
vendored
Normal file
509
ui/src/components/markdown-textarea.tsx
vendored
Normal file
|
@ -0,0 +1,509 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Prompt } from 'inferno-router';
|
||||
import {
|
||||
mdToHtml,
|
||||
randomStr,
|
||||
markdownHelpUrl,
|
||||
toast,
|
||||
setupTribute,
|
||||
pictrsDeleteToast,
|
||||
setupTippy,
|
||||
} from '../utils';
|
||||
import { UserService } from '../services';
|
||||
import autosize from 'autosize';
|
||||
import Tribute from 'tributejs/src/Tribute.js';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
interface MarkdownTextAreaProps {
|
||||
initialContent: string;
|
||||
finished?: boolean;
|
||||
buttonTitle?: string;
|
||||
replyType?: boolean;
|
||||
focus?: boolean;
|
||||
disabled?: boolean;
|
||||
onSubmit?(val: string): any;
|
||||
onContentChange?(val: string): any;
|
||||
onReplyCancel?(): any;
|
||||
}
|
||||
|
||||
interface MarkdownTextAreaState {
|
||||
content: string;
|
||||
previewMode: boolean;
|
||||
loading: boolean;
|
||||
imageLoading: boolean;
|
||||
}
|
||||
|
||||
export class MarkdownTextArea extends Component<
|
||||
MarkdownTextAreaProps,
|
||||
MarkdownTextAreaState
|
||||
> {
|
||||
private id = `comment-textarea-${randomStr()}`;
|
||||
private formId = `comment-form-${randomStr()}`;
|
||||
private tribute: Tribute;
|
||||
private emptyState: MarkdownTextAreaState = {
|
||||
content: this.props.initialContent,
|
||||
previewMode: false,
|
||||
loading: false,
|
||||
imageLoading: false,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.tribute = setupTribute();
|
||||
this.state = this.emptyState;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let textarea: any = document.getElementById(this.id);
|
||||
if (textarea) {
|
||||
autosize(textarea);
|
||||
this.tribute.attach(textarea);
|
||||
textarea.addEventListener('tribute-replaced', () => {
|
||||
this.state.content = textarea.value;
|
||||
this.setState(this.state);
|
||||
autosize.update(textarea);
|
||||
});
|
||||
|
||||
this.quoteInsert();
|
||||
|
||||
if (this.props.focus) {
|
||||
textarea.focus();
|
||||
}
|
||||
|
||||
// TODO this is slow for some reason
|
||||
setupTippy();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.state.content) {
|
||||
window.onbeforeunload = () => true;
|
||||
} else {
|
||||
window.onbeforeunload = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: MarkdownTextAreaProps) {
|
||||
if (nextProps.finished) {
|
||||
this.state.previewMode = false;
|
||||
this.state.loading = false;
|
||||
this.state.content = '';
|
||||
this.setState(this.state);
|
||||
if (this.props.replyType) {
|
||||
this.props.onReplyCancel();
|
||||
}
|
||||
|
||||
let textarea: any = document.getElementById(this.id);
|
||||
let form: any = document.getElementById(this.formId);
|
||||
form.reset();
|
||||
setTimeout(() => autosize.update(textarea), 10);
|
||||
this.setState(this.state);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.onbeforeunload = null;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<form id={this.formId} onSubmit={linkEvent(this, this.handleSubmit)}>
|
||||
<Prompt when={this.state.content} message={i18n.t('block_leaving')} />
|
||||
<div class="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)}
|
||||
required
|
||||
disabled={this.props.disabled}
|
||||
rows={2}
|
||||
maxLength={10000}
|
||||
/>
|
||||
{this.state.previewMode && (
|
||||
<div
|
||||
className="card card-body md-div"
|
||||
dangerouslySetInnerHTML={mdToHtml(this.state.content)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 d-flex flex-wrap">
|
||||
{this.props.buttonTitle && (
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-sm btn-secondary mr-2"
|
||||
disabled={this.props.disabled || this.state.loading}
|
||||
>
|
||||
{this.state.loading ? (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
) : (
|
||||
<span>{this.props.buttonTitle}</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{this.props.replyType && (
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-secondary mr-2"
|
||||
onClick={linkEvent(this, this.handleReplyCancel)}
|
||||
>
|
||||
{i18n.t('cancel')}
|
||||
</button>
|
||||
)}
|
||||
{this.state.content && (
|
||||
<button
|
||||
className={`btn btn-sm btn-secondary mr-2 ${
|
||||
this.state.previewMode && 'active'
|
||||
}`}
|
||||
onClick={linkEvent(this, this.handlePreviewToggle)}
|
||||
>
|
||||
{i18n.t('preview')}
|
||||
</button>
|
||||
)}
|
||||
{/* A flex expander */}
|
||||
<div class="flex-grow-1"></div>
|
||||
<button
|
||||
class="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t('bold')}
|
||||
onClick={linkEvent(this, this.handleInsertBold)}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-bold"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t('italic')}
|
||||
onClick={linkEvent(this, this.handleInsertItalic)}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-italic"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t('link')}
|
||||
onClick={linkEvent(this, this.handleInsertLink)}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-link"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<form class="btn btn-sm text-muted font-weight-bold">
|
||||
<label
|
||||
htmlFor={`file-upload-${this.id}`}
|
||||
className={`mb-0 ${UserService.Instance.user && 'pointer'}`}
|
||||
data-tippy-content={i18n.t('upload_image')}
|
||||
>
|
||||
{this.state.imageLoading ? (
|
||||
<svg class="icon icon-spinner spin">
|
||||
<use xlinkHref="#icon-spinner"></use>
|
||||
</svg>
|
||||
) : (
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-image"></use>
|
||||
</svg>
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
id={`file-upload-${this.id}`}
|
||||
type="file"
|
||||
accept="image/*,video/*"
|
||||
name="file"
|
||||
class="d-none"
|
||||
disabled={!UserService.Instance.user}
|
||||
onChange={linkEvent(this, this.handleImageUpload)}
|
||||
/>
|
||||
</form>
|
||||
<button
|
||||
class="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t('header')}
|
||||
onClick={linkEvent(this, this.handleInsertHeader)}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-header"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t('strikethrough')}
|
||||
onClick={linkEvent(this, this.handleInsertStrikethrough)}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-strikethrough"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t('quote')}
|
||||
onClick={linkEvent(this, this.handleInsertQuote)}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-format_quote"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t('list')}
|
||||
onClick={linkEvent(this, this.handleInsertList)}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-list"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t('code')}
|
||||
onClick={linkEvent(this, this.handleInsertCode)}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-code"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm text-muted"
|
||||
data-tippy-content={i18n.t('spoiler')}
|
||||
onClick={linkEvent(this, this.handleInsertSpoiler)}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-alert-triangle"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<a
|
||||
href={markdownHelpUrl}
|
||||
target="_blank"
|
||||
class="btn btn-sm text-muted font-weight-bold"
|
||||
title={i18n.t('formatting_help')}
|
||||
rel="noopener"
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-help-circle"></use>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
handleImageUploadPaste(i: MarkdownTextArea, event: any) {
|
||||
let image = event.clipboardData.files[0];
|
||||
if (image) {
|
||||
i.handleImageUpload(i, image);
|
||||
}
|
||||
}
|
||||
|
||||
handleImageUpload(i: MarkdownTextArea, event: any) {
|
||||
let file: any;
|
||||
if (event.target) {
|
||||
event.preventDefault();
|
||||
file = event.target.files[0];
|
||||
} else {
|
||||
file = event;
|
||||
}
|
||||
|
||||
const imageUploadUrl = `/pictrs/image`;
|
||||
const formData = new FormData();
|
||||
formData.append('images[]', file);
|
||||
|
||||
i.state.imageLoading = true;
|
||||
i.setState(i.state);
|
||||
|
||||
fetch(imageUploadUrl, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
console.log('pictrs upload:');
|
||||
console.log(res);
|
||||
if (res.msg == 'ok') {
|
||||
let hash = res.files[0].file;
|
||||
let url = `${window.location.origin}/pictrs/image/${hash}`;
|
||||
let deleteToken = res.files[0].delete_token;
|
||||
let deleteUrl = `${window.location.origin}/pictrs/image/delete/${deleteToken}/${hash}`;
|
||||
let imageMarkdown = `![](${url})`;
|
||||
let content = i.state.content;
|
||||
content = content ? `${content}\n${imageMarkdown}` : imageMarkdown;
|
||||
i.state.content = content;
|
||||
i.state.imageLoading = false;
|
||||
i.setState(i.state);
|
||||
let textarea: any = document.getElementById(i.id);
|
||||
autosize.update(textarea);
|
||||
pictrsDeleteToast(
|
||||
i18n.t('click_to_delete_picture'),
|
||||
i18n.t('picture_deleted'),
|
||||
deleteUrl
|
||||
);
|
||||
} else {
|
||||
i.state.imageLoading = false;
|
||||
i.setState(i.state);
|
||||
toast(JSON.stringify(res), 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
i.state.imageLoading = false;
|
||||
i.setState(i.state);
|
||||
toast(error, 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
handleContentChange(i: MarkdownTextArea, event: any) {
|
||||
i.state.content = event.target.value;
|
||||
i.setState(i.state);
|
||||
if (i.props.onContentChange) {
|
||||
i.props.onContentChange(i.state.content);
|
||||
}
|
||||
}
|
||||
|
||||
handlePreviewToggle(i: MarkdownTextArea, event: any) {
|
||||
event.preventDefault();
|
||||
i.state.previewMode = !i.state.previewMode;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleSubmit(i: MarkdownTextArea, event: any) {
|
||||
event.preventDefault();
|
||||
i.state.loading = true;
|
||||
i.setState(i.state);
|
||||
i.props.onSubmit(i.state.content);
|
||||
}
|
||||
|
||||
handleReplyCancel(i: MarkdownTextArea) {
|
||||
i.props.onReplyCancel();
|
||||
}
|
||||
|
||||
handleInsertLink(i: MarkdownTextArea, event: any) {
|
||||
event.preventDefault();
|
||||
if (!i.state.content) {
|
||||
i.state.content = '';
|
||||
}
|
||||
let textarea: any = document.getElementById(i.id);
|
||||
let start: number = textarea.selectionStart;
|
||||
let end: number = textarea.selectionEnd;
|
||||
|
||||
if (start !== end) {
|
||||
let selectedText = i.state.content.substring(start, end);
|
||||
i.state.content = `${i.state.content.substring(
|
||||
0,
|
||||
start
|
||||
)} [${selectedText}]() ${i.state.content.substring(end)}`;
|
||||
textarea.focus();
|
||||
setTimeout(() => (textarea.selectionEnd = end + 4), 10);
|
||||
} else {
|
||||
i.state.content += '[]()';
|
||||
textarea.focus();
|
||||
setTimeout(() => (textarea.selectionEnd -= 1), 10);
|
||||
}
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
simpleSurround(chars: string) {
|
||||
this.simpleSurroundBeforeAfter(chars, chars);
|
||||
}
|
||||
|
||||
simpleSurroundBeforeAfter(beforeChars: string, afterChars: string) {
|
||||
if (!this.state.content) {
|
||||
this.state.content = '';
|
||||
}
|
||||
let textarea: any = document.getElementById(this.id);
|
||||
let start: number = textarea.selectionStart;
|
||||
let end: number = textarea.selectionEnd;
|
||||
|
||||
if (start !== end) {
|
||||
let selectedText = this.state.content.substring(start, end);
|
||||
this.state.content = `${this.state.content.substring(
|
||||
0,
|
||||
start - 1
|
||||
)} ${beforeChars}${selectedText}${afterChars} ${this.state.content.substring(
|
||||
end + 1
|
||||
)}`;
|
||||
} else {
|
||||
this.state.content += `${beforeChars}___${afterChars}`;
|
||||
}
|
||||
this.setState(this.state);
|
||||
setTimeout(() => {
|
||||
autosize.update(textarea);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
handleInsertBold(i: MarkdownTextArea, event: any) {
|
||||
event.preventDefault();
|
||||
i.simpleSurround('**');
|
||||
}
|
||||
|
||||
handleInsertItalic(i: MarkdownTextArea, event: any) {
|
||||
event.preventDefault();
|
||||
i.simpleSurround('*');
|
||||
}
|
||||
|
||||
handleInsertCode(i: MarkdownTextArea, event: any) {
|
||||
event.preventDefault();
|
||||
i.simpleSurround('`');
|
||||
}
|
||||
|
||||
handleInsertStrikethrough(i: MarkdownTextArea, event: any) {
|
||||
event.preventDefault();
|
||||
i.simpleSurround('~~');
|
||||
}
|
||||
|
||||
handleInsertList(i: MarkdownTextArea, event: any) {
|
||||
event.preventDefault();
|
||||
i.simpleInsert('-');
|
||||
}
|
||||
|
||||
handleInsertQuote(i: MarkdownTextArea, event: any) {
|
||||
event.preventDefault();
|
||||
i.simpleInsert('>');
|
||||
}
|
||||
|
||||
handleInsertHeader(i: MarkdownTextArea, event: any) {
|
||||
event.preventDefault();
|
||||
i.simpleInsert('#');
|
||||
}
|
||||
|
||||
simpleInsert(chars: string) {
|
||||
if (!this.state.content) {
|
||||
this.state.content = `${chars} `;
|
||||
} else {
|
||||
this.state.content += `\n${chars} `;
|
||||
}
|
||||
|
||||
let textarea: any = document.getElementById(this.id);
|
||||
textarea.focus();
|
||||
setTimeout(() => {
|
||||
autosize.update(textarea);
|
||||
}, 10);
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleInsertSpoiler(i: MarkdownTextArea, event: any) {
|
||||
event.preventDefault();
|
||||
let beforeChars = `\n::: spoiler ${i18n.t('spoiler')}\n`;
|
||||
let afterChars = '\n:::\n';
|
||||
i.simpleSurroundBeforeAfter(beforeChars, afterChars);
|
||||
}
|
||||
|
||||
quoteInsert() {
|
||||
let textarea: any = document.getElementById(this.id);
|
||||
let selectedText = window.getSelection().toString();
|
||||
if (selectedText) {
|
||||
let quotedText =
|
||||
selectedText
|
||||
.split('\n')
|
||||
.map(t => `> ${t}`)
|
||||
.join('\n') + '\n\n';
|
||||
this.state.content = quotedText;
|
||||
this.setState(this.state);
|
||||
// Not sure why this needs a delay
|
||||
setTimeout(() => autosize.update(textarea), 10);
|
||||
}
|
||||
}
|
||||
}
|
62
ui/src/components/post-form.tsx
vendored
62
ui/src/components/post-form.tsx
vendored
|
@ -1,6 +1,7 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Prompt } from 'inferno-router';
|
||||
import { PostListings } from './post-listings';
|
||||
import { MarkdownTextArea } from './markdown-textarea';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { retryWhen, delay, take } from 'rxjs/operators';
|
||||
import {
|
||||
|
@ -24,22 +25,16 @@ import {
|
|||
getPageTitle,
|
||||
validURL,
|
||||
capitalizeFirstLetter,
|
||||
markdownHelpUrl,
|
||||
archiveUrl,
|
||||
mdToHtml,
|
||||
debounce,
|
||||
isImage,
|
||||
toast,
|
||||
randomStr,
|
||||
setupTribute,
|
||||
setupTippy,
|
||||
hostname,
|
||||
pictrsDeleteToast,
|
||||
validTitle,
|
||||
} from '../utils';
|
||||
import autosize from 'autosize';
|
||||
import Tribute from 'tributejs/src/Tribute.js';
|
||||
import emojiShortName from 'emoji-short-name';
|
||||
import Choices from 'choices.js';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
|
@ -68,7 +63,6 @@ interface PostFormState {
|
|||
|
||||
export class PostForm extends Component<PostFormProps, PostFormState> {
|
||||
private id = `post-form-${randomStr()}`;
|
||||
private tribute: Tribute;
|
||||
private subscription: Subscription;
|
||||
private choices: Choices;
|
||||
private emptyState: PostFormState = {
|
||||
|
@ -94,8 +88,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
super(props, context);
|
||||
this.fetchSimilarPosts = debounce(this.fetchSimilarPosts).bind(this);
|
||||
this.fetchPageTitle = debounce(this.fetchPageTitle).bind(this);
|
||||
|
||||
this.tribute = setupTribute();
|
||||
this.handlePostBodyChange = this.handlePostBodyChange.bind(this);
|
||||
|
||||
this.state = this.emptyState;
|
||||
|
||||
|
@ -140,14 +133,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
var textarea: any = document.getElementById(this.id);
|
||||
autosize(textarea);
|
||||
this.tribute.attach(textarea);
|
||||
textarea.addEventListener('tribute-replaced', () => {
|
||||
this.state.postForm.body = textarea.value;
|
||||
this.setState(this.state);
|
||||
autosize.update(textarea);
|
||||
});
|
||||
setupTippy();
|
||||
}
|
||||
|
||||
|
@ -305,41 +290,10 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
{i18n.t('body')}
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea
|
||||
id={this.id}
|
||||
value={this.state.postForm.body}
|
||||
onInput={linkEvent(this, this.handlePostBodyChange)}
|
||||
className={`form-control ${this.state.previewMode && 'd-none'}`}
|
||||
rows={4}
|
||||
maxLength={10000}
|
||||
<MarkdownTextArea
|
||||
initialContent={this.state.postForm.body}
|
||||
onContentChange={this.handlePostBodyChange}
|
||||
/>
|
||||
{this.state.previewMode && (
|
||||
<div
|
||||
className="card card-body md-div"
|
||||
dangerouslySetInnerHTML={mdToHtml(this.state.postForm.body)}
|
||||
/>
|
||||
)}
|
||||
{this.state.postForm.body && (
|
||||
<button
|
||||
className={`mt-1 mr-2 btn btn-sm btn-secondary ${
|
||||
this.state.previewMode && 'active'
|
||||
}`}
|
||||
onClick={linkEvent(this, this.handlePreviewToggle)}
|
||||
>
|
||||
{i18n.t('preview')}
|
||||
</button>
|
||||
)}
|
||||
<a
|
||||
href={markdownHelpUrl}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="d-inline-block float-right text-muted font-weight-bold"
|
||||
title={i18n.t('formatting_help')}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-help-circle"></use>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{!this.props.post && (
|
||||
|
@ -499,9 +453,9 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
|
|||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handlePostBodyChange(i: PostForm, event: any) {
|
||||
i.state.postForm.body = event.target.value;
|
||||
i.setState(i.state);
|
||||
handlePostBodyChange(val: string) {
|
||||
this.state.postForm.body = val;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handlePostCommunityChange(i: PostForm, event: any) {
|
||||
|
|
94
ui/src/components/private-message-form.tsx
vendored
94
ui/src/components/private-message-form.tsx
vendored
|
@ -18,17 +18,12 @@ import {
|
|||
import { WebSocketService } from '../services';
|
||||
import {
|
||||
capitalizeFirstLetter,
|
||||
markdownHelpUrl,
|
||||
mdToHtml,
|
||||
wsJsonToRes,
|
||||
toast,
|
||||
randomStr,
|
||||
setupTribute,
|
||||
setupTippy,
|
||||
} from '../utils';
|
||||
import { UserListing } from './user-listing';
|
||||
import Tribute from 'tributejs/src/Tribute.js';
|
||||
import autosize from 'autosize';
|
||||
import { MarkdownTextArea } from './markdown-textarea';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
||||
|
@ -52,8 +47,6 @@ export class PrivateMessageForm extends Component<
|
|||
PrivateMessageFormProps,
|
||||
PrivateMessageFormState
|
||||
> {
|
||||
private id = `message-form-${randomStr()}`;
|
||||
private tribute: Tribute;
|
||||
private subscription: Subscription;
|
||||
private emptyState: PrivateMessageFormState = {
|
||||
privateMessageForm: {
|
||||
|
@ -69,9 +62,10 @@ export class PrivateMessageForm extends Component<
|
|||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.tribute = setupTribute();
|
||||
this.state = this.emptyState;
|
||||
|
||||
this.handleContentChange = this.handleContentChange.bind(this);
|
||||
|
||||
if (this.props.privateMessage) {
|
||||
this.state.privateMessageForm = {
|
||||
content: this.props.privateMessage.content,
|
||||
|
@ -99,14 +93,6 @@ export class PrivateMessageForm extends Component<
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
var textarea: any = document.getElementById(this.id);
|
||||
autosize(textarea);
|
||||
this.tribute.attach(textarea);
|
||||
textarea.addEventListener('tribute-replaced', () => {
|
||||
this.state.privateMessageForm.content = textarea.value;
|
||||
this.setState(this.state);
|
||||
autosize.update(textarea);
|
||||
});
|
||||
setupTippy();
|
||||
}
|
||||
|
||||
|
@ -153,24 +139,23 @@ export class PrivateMessageForm extends Component<
|
|||
</div>
|
||||
)}
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label">{i18n.t('message')}</label>
|
||||
<label class="col-sm-2 col-form-label">
|
||||
{i18n.t('message')}
|
||||
<span
|
||||
onClick={linkEvent(this, this.handleShowDisclaimer)}
|
||||
class="ml-2 pointer text-danger"
|
||||
data-tippy-content={i18n.t('disclaimer')}
|
||||
>
|
||||
<svg class={`icon icon-inline`}>
|
||||
<use xlinkHref="#icon-alert-triangle"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea
|
||||
id={this.id}
|
||||
value={this.state.privateMessageForm.content}
|
||||
onInput={linkEvent(this, this.handleContentChange)}
|
||||
className={`form-control ${this.state.previewMode && 'd-none'}`}
|
||||
rows={4}
|
||||
maxLength={10000}
|
||||
<MarkdownTextArea
|
||||
initialContent={this.state.privateMessageForm.content}
|
||||
onContentChange={this.handleContentChange}
|
||||
/>
|
||||
{this.state.previewMode && (
|
||||
<div
|
||||
className="card card-body md-div"
|
||||
dangerouslySetInnerHTML={mdToHtml(
|
||||
this.state.privateMessageForm.content
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -184,7 +169,7 @@ export class PrivateMessageForm extends Component<
|
|||
class="alert-link"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href="https://about.riot.im/"
|
||||
href="https://element.io/get-started"
|
||||
>
|
||||
#
|
||||
</a>
|
||||
|
@ -210,16 +195,6 @@ export class PrivateMessageForm extends Component<
|
|||
capitalizeFirstLetter(i18n.t('send_message'))
|
||||
)}
|
||||
</button>
|
||||
{this.state.privateMessageForm.content && (
|
||||
<button
|
||||
className={`btn btn-secondary mr-2 ${
|
||||
this.state.previewMode && 'active'
|
||||
}`}
|
||||
onClick={linkEvent(this, this.handlePreviewToggle)}
|
||||
>
|
||||
{i18n.t('preview')}
|
||||
</button>
|
||||
)}
|
||||
{this.props.privateMessage && (
|
||||
<button
|
||||
type="button"
|
||||
|
@ -230,30 +205,7 @@ export class PrivateMessageForm extends Component<
|
|||
</button>
|
||||
)}
|
||||
<ul class="d-inline-block float-right list-inline mb-1 text-muted font-weight-bold">
|
||||
<li class="list-inline-item">
|
||||
<span
|
||||
onClick={linkEvent(this, this.handleShowDisclaimer)}
|
||||
class="pointer"
|
||||
data-tippy-content={i18n.t('disclaimer')}
|
||||
>
|
||||
<svg class={`icon icon-inline`}>
|
||||
<use xlinkHref="#icon-alert-triangle"></use>
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
<li class="list-inline-item">
|
||||
<a
|
||||
href={markdownHelpUrl}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="text-muted"
|
||||
title={i18n.t('formatting_help')}
|
||||
>
|
||||
<svg class="icon icon-inline">
|
||||
<use xlinkHref="#icon-help-circle"></use>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
<li class="list-inline-item"></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -284,9 +236,9 @@ export class PrivateMessageForm extends Component<
|
|||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleContentChange(i: PrivateMessageForm, event: any) {
|
||||
i.state.privateMessageForm.content = event.target.value;
|
||||
i.setState(i.state);
|
||||
handleContentChange(val: string) {
|
||||
this.state.privateMessageForm.content = val;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleCancel(i: PrivateMessageForm) {
|
||||
|
|
37
ui/src/components/site-form.tsx
vendored
37
ui/src/components/site-form.tsx
vendored
|
@ -1,10 +1,9 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Prompt } from 'inferno-router';
|
||||
import { MarkdownTextArea } from './markdown-textarea';
|
||||
import { Site, SiteForm as SiteFormI } from '../interfaces';
|
||||
import { WebSocketService } from '../services';
|
||||
import { capitalizeFirstLetter, randomStr, setupTribute } from '../utils';
|
||||
import autosize from 'autosize';
|
||||
import Tribute from 'tributejs/src/Tribute.js';
|
||||
import { capitalizeFirstLetter, randomStr } from '../utils';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
interface SiteFormProps {
|
||||
|
@ -19,7 +18,6 @@ interface SiteFormState {
|
|||
|
||||
export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
||||
private id = `site-form-${randomStr()}`;
|
||||
private tribute: Tribute;
|
||||
private emptyState: SiteFormState = {
|
||||
siteForm: {
|
||||
enable_downvotes: true,
|
||||
|
@ -33,8 +31,10 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
|||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.tribute = setupTribute();
|
||||
this.state = this.emptyState;
|
||||
this.handleSiteDescriptionChange = this.handleSiteDescriptionChange.bind(
|
||||
this
|
||||
);
|
||||
|
||||
if (this.props.site) {
|
||||
this.state.siteForm = {
|
||||
|
@ -47,17 +47,6 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
var textarea: any = document.getElementById(this.id);
|
||||
autosize(textarea);
|
||||
this.tribute.attach(textarea);
|
||||
textarea.addEventListener('tribute-replaced', () => {
|
||||
this.state.siteForm.description = textarea.value;
|
||||
this.setState(this.state);
|
||||
autosize.update(textarea);
|
||||
});
|
||||
}
|
||||
|
||||
// Necessary to stop the loading
|
||||
componentWillReceiveProps() {
|
||||
this.state.loading = false;
|
||||
|
@ -119,13 +108,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
|||
{i18n.t('sidebar')}
|
||||
</label>
|
||||
<div class="col-12">
|
||||
<textarea
|
||||
id={this.id}
|
||||
value={this.state.siteForm.description}
|
||||
onInput={linkEvent(this, this.handleSiteDescriptionChange)}
|
||||
class="form-control"
|
||||
rows={3}
|
||||
maxLength={10000}
|
||||
<MarkdownTextArea
|
||||
initialContent={this.state.siteForm.description}
|
||||
onContentChange={this.handleSiteDescriptionChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -238,9 +223,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
|||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleSiteDescriptionChange(i: SiteForm, event: any) {
|
||||
i.state.siteForm.description = event.target.value;
|
||||
i.setState(i.state);
|
||||
handleSiteDescriptionChange(val: string) {
|
||||
this.state.siteForm.description = val;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleSiteEnableNsfwChange(i: SiteForm, event: any) {
|
||||
|
|
21
ui/src/components/symbols.tsx
vendored
21
ui/src/components/symbols.tsx
vendored
File diff suppressed because one or more lines are too long
9
ui/translations/en.json
vendored
9
ui/translations/en.json
vendored
|
@ -51,6 +51,13 @@
|
|||
"unsticky": "unsticky",
|
||||
"link": "link",
|
||||
"archive_link": "archive link",
|
||||
"bold": "bold",
|
||||
"italic": "italic",
|
||||
"header": "header",
|
||||
"strikethrough": "strikethrough",
|
||||
"quote": "quote",
|
||||
"spoiler": "spoiler",
|
||||
"list": "list",
|
||||
"mod": "mod",
|
||||
"mods": "mods",
|
||||
"moderates": "Moderates",
|
||||
|
@ -154,7 +161,7 @@
|
|||
"email": "Email",
|
||||
"matrix_user_id": "Matrix User",
|
||||
"private_message_disclaimer":
|
||||
"Warning: Private messages in Lemmy are not secure. Please create an account on <1>Riot.im</1> for secure messaging.",
|
||||
"Warning: Private messages in Lemmy are not secure. Please create an account on <1>Element.io</1> for secure messaging.",
|
||||
"send_notifications_to_email": "Send notifications to Email",
|
||||
"optional": "Optional",
|
||||
"expires": "Expires",
|
||||
|
|
Loading…
Reference in a new issue