separate out advanced textarea, fix comment channel selector width, a… #7634
18 changed files with 383 additions and 206 deletions
|
@ -2319,5 +2319,7 @@
|
|||
"Your hub has blocked this content because it subscribes to the following blocking channel:": "Your hub has blocked this content because it subscribes to the following blocking channel:",
|
||||
"Your hub has blocked access to this content do to a complaint received under the US Digital Millennium Copyright Act.": "Your hub has blocked access to this content do to a complaint received under the US Digital Millennium Copyright Act.",
|
||||
"Autoplay Next is on.": "Autoplay Next is on.",
|
||||
"This will be visible in a few minutes after you submit this form.": "This will be visible in a few minutes after you submit this form.",
|
||||
"Anon --[used in <%anonymous% Reposted>]--": "Anon",
|
||||
"--end--": "--end--"
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ import { useHistory } from 'react-router-dom';
|
|||
import { isNameValid, regexInvalidURI } from 'util/lbryURI';
|
||||
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
|
||||
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
||||
import { FormField } from 'component/common/form';
|
||||
import { FormField, FormFieldAreaAdvanced } from 'component/common/form';
|
||||
import { handleBidChange } from 'util/publish';
|
||||
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
||||
import { INVALID_NAME_ERROR } from 'constants/claim';
|
||||
|
@ -371,7 +371,7 @@ function CollectionForm(props: Props) {
|
|||
usePublishFormMode
|
||||
/>
|
||||
</fieldset-section>
|
||||
<FormField
|
||||
<FormFieldAreaAdvanced
|
||||
type="markdown"
|
||||
name="content_description2"
|
||||
label={__('Description')}
|
||||
|
|
|
@ -17,7 +17,7 @@ import CommentBadge from 'component/common/comment-badge'; // have this?
|
|||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import { Menu, MenuButton } from '@reach/menu-button';
|
||||
import Icon from 'component/common/icon';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import { FormFieldAreaAdvanced, Form } from 'component/common/form';
|
||||
import classnames from 'classnames';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import CommentReactions from 'component/commentReactions';
|
||||
|
@ -319,7 +319,7 @@ function CommentView(props: Props) {
|
|||
<div>
|
||||
{isEditing ? (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<FormField
|
||||
<FormFieldAreaAdvanced
|
||||
className="comment__edit-input"
|
||||
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||
name="editing_comment"
|
||||
|
|
32
ui/component/commentCreate/comment-create-header.jsx
Normal file
32
ui/component/commentCreate/comment-create-header.jsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import SelectChannel from 'component/selectChannel';
|
||||
import Button from 'component/button';
|
||||
import * as ICONS from 'constants/icons';
|
||||
|
||||
type Props = {
|
||||
isReply: boolean,
|
||||
advancedHandler: () => void,
|
||||
advanced: boolean,
|
||||
};
|
||||
|
||||
export default function CommentCreateHeader(props: Props) {
|
||||
const { isReply, advancedHandler, advanced } = props;
|
||||
|
||||
return (
|
||||
<div className="comment-create__header">
|
||||
<div className="comment-create__label-wrapper">
|
||||
<span className="comment-create__label">{(isReply ? __('Replying as') : __('Comment as')) + ' '}</span>
|
||||
<SelectChannel tiny />
|
||||
</div>
|
||||
<div className="form-field__quick-action">
|
||||
<Button
|
||||
button="alt"
|
||||
icon={advanced ? ICONS.SIMPLE_EDITOR : ICONS.ADVANCED_EDITOR}
|
||||
onClick={advancedHandler}
|
||||
aria-label={isReply ? undefined : advanced ? __('Simple Editor') : __('Advanced Editor')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -4,7 +4,7 @@ import 'scss/component/_comment-create.scss';
|
|||
|
||||
import { buildValidSticker } from 'util/comments';
|
||||
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import { FormFieldAreaAdvanced, Form } from 'component/common/form';
|
||||
import { getChannelIdFromClaim } from 'util/claim';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import { useHistory } from 'react-router';
|
||||
|
@ -22,8 +22,8 @@ import I18nMessage from 'component/i18nMessage';
|
|||
import Icon from 'component/common/icon';
|
||||
import OptimizedImage from 'component/optimizedImage';
|
||||
import React from 'react';
|
||||
import SelectChannel from 'component/selectChannel';
|
||||
import StickerSelector from './sticker-selector';
|
||||
import CommentCreateHeader from './comment-create-header';
|
||||
import type { ElementRef } from 'react';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
|
@ -409,7 +409,11 @@ export function CommentCreate(props: Props) {
|
|||
push(pathPlusRedirect);
|
||||
}}
|
||||
>
|
||||
<FormField type="textarea" name={'comment_signup_prompt'} placeholder={__('Say something about this...')} />
|
||||
<FormFieldAreaAdvanced
|
||||
type="textarea"
|
||||
name={'comment_signup_prompt'}
|
||||
placeholder={__('Say something about this...')}
|
||||
/>
|
||||
<div className="section__actions--no-margin">
|
||||
<Button disabled button="primary" label={__('Post --[button to submit something]--')} />
|
||||
</div>
|
||||
|
@ -420,22 +424,22 @@ export function CommentCreate(props: Props) {
|
|||
return (
|
||||
<Form
|
||||
onSubmit={() => {}}
|
||||
className={classnames('commentCreate', {
|
||||
'commentCreate--reply': isReply,
|
||||
'commentCreate--nestedReply': isNested,
|
||||
'commentCreate--bottom': bottom,
|
||||
className={classnames('comment-create', {
|
||||
'comment-create--reply': isReply,
|
||||
'comment-create--nestedReply': isNested,
|
||||
'comment-create--bottom': bottom,
|
||||
})}
|
||||
>
|
||||
{/* Input Box/Preview Box */}
|
||||
{stickerSelector ? (
|
||||
<StickerSelector onSelect={(sticker) => handleSelectSticker(sticker)} claimIsMine={claimIsMine} />
|
||||
) : isReviewingStickerComment && activeChannelClaim && selectedSticker ? (
|
||||
<div className="commentCreate__stickerPreview">
|
||||
<div className="commentCreate__stickerPreviewInfo">
|
||||
<div className="comment-create__stickerPreview">
|
||||
<div className="comment-create__stickerPreviewInfo">
|
||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||
</div>
|
||||
<div className="commentCreate__stickerPreviewImage">
|
||||
<div className="comment-create__stickerPreviewImage">
|
||||
<OptimizedImage src={selectedSticker && selectedSticker.url} waitLoad loading="lazy" />
|
||||
</div>
|
||||
{/* figure out lbc sticker prices */}
|
||||
|
@ -447,15 +451,15 @@ export function CommentCreate(props: Props) {
|
|||
)}
|
||||
</div>
|
||||
) : isReviewingSupportComment && activeChannelClaim ? (
|
||||
<div className="commentCreate__supportCommentPreview">
|
||||
<div className="comment-create__supportCommentPreview">
|
||||
<CreditAmount
|
||||
amount={tipAmount}
|
||||
className="commentCreate__supportCommentPreviewAmount"
|
||||
className="comment-create__supportCommentPreviewAmount"
|
||||
isFiat={activeTab === TAB_FIAT}
|
||||
size={activeTab === TAB_LBC ? 18 : 2}
|
||||
/>
|
||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||
<div className="commentCreate__supportCommentBody">
|
||||
<div className="comment-create__supportCommentBody">
|
||||
<UriIndicator uri={activeChannelClaim.canonical_url} link />
|
||||
<div>{commentValue}</div>
|
||||
</div>
|
||||
|
@ -470,23 +474,22 @@ export function CommentCreate(props: Props) {
|
|||
/>
|
||||
)}
|
||||
|
||||
<FormField
|
||||
<FormFieldAreaAdvanced
|
||||
autoFocus={isReply}
|
||||
charCount={charCount}
|
||||
className={isReply ? 'content_reply' : 'content_comment'}
|
||||
disabled={isFetchingChannels}
|
||||
label={
|
||||
<div className="commentCreate__labelWrapper">
|
||||
<span className="commentCreate__label">{(isReply ? __('Replying as') : __('Comment as')) + ' '}</span>
|
||||
<SelectChannel tiny />
|
||||
</div>
|
||||
header={
|
||||
<CommentCreateHeader
|
||||
isReply={isReply}
|
||||
advanced={advancedEditor}
|
||||
advancedHandler={() => setAdvancedEditor(!advancedEditor)}
|
||||
/>
|
||||
}
|
||||
name={isReply ? 'content_reply' : 'content_description'}
|
||||
quickActionLabel={isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')}
|
||||
ref={formFieldRef}
|
||||
onChange={handleCommentChange}
|
||||
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
||||
quickActionHandler={() => setAdvancedEditor(!advancedEditor)}
|
||||
onFocus={onTextareaFocus}
|
||||
onBlur={onTextareaBlur}
|
||||
placeholder={__('Say something about this...')}
|
||||
|
@ -654,7 +657,7 @@ export function CommentCreate(props: Props) {
|
|||
{/* Help Text */}
|
||||
{deletedComment && <div className="error__text">{__('This comment has been deleted.')}</div>}
|
||||
{!!minAmount && (
|
||||
<div className="help--notice commentCreate__minAmountNotice">
|
||||
<div className="help--notice comment-create__minAmountNotice">
|
||||
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
||||
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
||||
</I18nMessage>
|
||||
|
|
241
ui/component/common/form-components/form-field-area-advanced.jsx
Normal file
241
ui/component/common/form-components/form-field-area-advanced.jsx
Normal file
|
@ -0,0 +1,241 @@
|
|||
// @flow
|
||||
import 'easymde/dist/easymde.min.css';
|
||||
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
||||
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Button from 'component/button';
|
||||
import MarkdownPreview from 'component/common/markdown-preview';
|
||||
import React from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import SimpleMDE from 'react-simplemde-editor';
|
||||
import TextareaWithSuggestions from 'component/textareaWithSuggestions';
|
||||
import type { ElementRef, Node } from 'react';
|
||||
|
||||
type Props = {
|
||||
autoFocus?: boolean,
|
||||
blockWrap: boolean,
|
||||
charCount?: number,
|
||||
children?: React$Node,
|
||||
disabled?: boolean,
|
||||
helper?: string | React$Node,
|
||||
hideSuggestions?: boolean,
|
||||
isLivestream?: boolean,
|
||||
label?: string | Node,
|
||||
labelOnLeft: boolean,
|
||||
name: string,
|
||||
noEmojis?: boolean,
|
||||
placeholder?: string | number,
|
||||
quickActionLabel?: string,
|
||||
textAreaMaxLength?: number,
|
||||
type?: string,
|
||||
value?: string | number,
|
||||
onChange?: (any) => any,
|
||||
openEmoteMenu?: () => void,
|
||||
quickActionHandler?: (any) => any,
|
||||
render?: () => React$Node,
|
||||
header?: React$Node,
|
||||
};
|
||||
|
||||
export class FormFieldAreaAdvanced extends React.PureComponent<Props> {
|
||||
static defaultProps = { labelOnLeft: false, blockWrap: true };
|
||||
|
||||
input: { current: ElementRef<any> };
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.input = React.createRef();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { autoFocus } = this.props;
|
||||
const input = this.input.current;
|
||||
|
||||
if (input && autoFocus) input.focus();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
autoFocus,
|
||||
blockWrap,
|
||||
charCount,
|
||||
children,
|
||||
helper,
|
||||
hideSuggestions,
|
||||
isLivestream,
|
||||
label,
|
||||
header,
|
||||
labelOnLeft,
|
||||
name,
|
||||
noEmojis,
|
||||
quickActionLabel,
|
||||
textAreaMaxLength,
|
||||
type,
|
||||
openEmoteMenu,
|
||||
quickActionHandler,
|
||||
render,
|
||||
...inputProps
|
||||
} = this.props;
|
||||
|
||||
// Ideally, the character count should (and can) be appended to the
|
||||
// SimpleMDE's "options::status" bar. However, I couldn't figure out how
|
||||
// to pass the current value to it's callback, nor query the current
|
||||
// text length from the callback. So, we'll use our own widget.
|
||||
const hasCharCount = charCount !== undefined && charCount >= 0;
|
||||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
||||
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
||||
);
|
||||
|
||||
const quickAction =
|
||||
quickActionLabel && quickActionHandler ? (
|
||||
<div className="form-field__quick-action">
|
||||
<Button button="link" onClick={quickActionHandler} label={quickActionLabel} />
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const input = () => {
|
||||
switch (type) {
|
||||
case 'markdown':
|
||||
const handleEvents = { contextmenu: openEditorMenu };
|
||||
|
||||
const getInstance = (editor) => {
|
||||
// SimpleMDE max char check
|
||||
editor.codemirror.on('beforeChange', (instance, changes) => {
|
||||
if (textAreaMaxLength && changes.update) {
|
||||
var str = changes.text.join('\n');
|
||||
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
||||
|
||||
if (delta <= 0) return;
|
||||
|
||||
delta = instance.getValue().length + delta - textAreaMaxLength;
|
||||
if (delta > 0) {
|
||||
str = str.substring(0, str.length - delta);
|
||||
changes.update(changes.from, changes.to, str.split('\n'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// "Create Link (Ctrl-K)": highlight URL instead of label:
|
||||
editor.codemirror.on('changes', (instance, changes) => {
|
||||
try {
|
||||
// Grab the last change from the buffered list. I assume the
|
||||
// buffered one ('changes', instead of 'change') is more efficient,
|
||||
// and that "Create Link" will always end up last in the list.
|
||||
const lastChange = changes[changes.length - 1];
|
||||
if (lastChange.origin === '+input') {
|
||||
// https://github.com/Ionaru/easy-markdown-editor/blob/8fa54c496f98621d5f45f57577ce630bee8c41ee/src/js/easymde.js#L765
|
||||
const EASYMDE_URL_PLACEHOLDER = '(https://)';
|
||||
|
||||
// The URL placeholder is always placed last, so just look at the
|
||||
// last text in the array to also cover the multi-line case:
|
||||
const urlLineText = lastChange.text[lastChange.text.length - 1];
|
||||
|
||||
if (urlLineText.endsWith(EASYMDE_URL_PLACEHOLDER) && urlLineText !== '[]' + EASYMDE_URL_PLACEHOLDER) {
|
||||
const from = lastChange.from;
|
||||
const to = lastChange.to;
|
||||
const isSelectionMultiline = lastChange.text.length > 1;
|
||||
const baseIndex = isSelectionMultiline ? 0 : from.ch;
|
||||
|
||||
// Everything works fine for the [Ctrl-K] case, but for the
|
||||
// [Button] case, this handler happens before the original
|
||||
// code, thus our change got wiped out.
|
||||
// Add a small delay to handle that case.
|
||||
setTimeout(() => {
|
||||
instance.setSelection(
|
||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf('(') + 1 },
|
||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf(')') }
|
||||
);
|
||||
}, 25);
|
||||
}
|
||||
}
|
||||
} catch (e) {} // Do nothing (revert to original behavior)
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
||||
<fieldset-section>
|
||||
{!header && (
|
||||
<div className="form-field__two-column">
|
||||
<div>
|
||||
<label htmlFor={name}>{label}</label>
|
||||
</div>
|
||||
{quickAction}
|
||||
</div>
|
||||
)}
|
||||
{!!header && <div className="form-field__textarea-header">{header}</div>}
|
||||
<SimpleMDE
|
||||
{...inputProps}
|
||||
id={name}
|
||||
type="textarea"
|
||||
events={handleEvents}
|
||||
getMdeInstance={getInstance}
|
||||
options={{
|
||||
spellChecker: true,
|
||||
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
|
||||
previewRender(plainText) {
|
||||
const preview = <MarkdownPreview content={plainText} noDataStore />;
|
||||
return ReactDOMServer.renderToString(preview);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{countInfo}
|
||||
</fieldset-section>
|
||||
</div>
|
||||
);
|
||||
case 'textarea':
|
||||
return (
|
||||
<fieldset-section>
|
||||
{!header && (label || quickAction) && (
|
||||
<div className="form-field__two-column">
|
||||
<label htmlFor={name}>{label}</label>
|
||||
{quickAction}
|
||||
</div>
|
||||
)}
|
||||
{!!header && <div className="form-field__textarea-header">{header}</div>}
|
||||
{hideSuggestions ? (
|
||||
<textarea
|
||||
type={type}
|
||||
id={name}
|
||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||
ref={this.input}
|
||||
{...inputProps}
|
||||
/>
|
||||
) : (
|
||||
<TextareaWithSuggestions
|
||||
type={type}
|
||||
id={name}
|
||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||
inputRef={this.input}
|
||||
isLivestream={isLivestream}
|
||||
{...inputProps}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="form-field__textarea-info">
|
||||
{!noEmojis && openEmoteMenu && (
|
||||
<Button
|
||||
type="alt"
|
||||
className="button--file-action"
|
||||
title="Emotes"
|
||||
onClick={openEmoteMenu}
|
||||
icon={ICONS.EMOJI}
|
||||
iconSize={20}
|
||||
/>
|
||||
)}
|
||||
{countInfo}
|
||||
</div>
|
||||
</fieldset-section>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{type && input()}
|
||||
{helper && <div className="form-field__help">{helper}</div>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FormFieldAreaAdvanced;
|
|
@ -1,14 +1,7 @@
|
|||
// @flow
|
||||
import 'easymde/dist/easymde.min.css';
|
||||
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
||||
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Button from 'component/button';
|
||||
import MarkdownPreview from 'component/common/markdown-preview';
|
||||
import React from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import SimpleMDE from 'react-simplemde-editor';
|
||||
import TextareaWithSuggestions from 'component/textareaWithSuggestions';
|
||||
import type { ElementRef, Node } from 'react';
|
||||
|
||||
type Props = {
|
||||
|
@ -21,19 +14,15 @@ type Props = {
|
|||
disabled?: boolean,
|
||||
error?: string | boolean,
|
||||
helper?: string | React$Node,
|
||||
hideSuggestions?: boolean,
|
||||
inputButton?: React$Node,
|
||||
isLivestream?: boolean,
|
||||
label?: string | Node,
|
||||
labelOnLeft: boolean,
|
||||
max?: number,
|
||||
min?: number,
|
||||
name: string,
|
||||
noEmojis?: boolean,
|
||||
placeholder?: string | number,
|
||||
postfix?: string,
|
||||
prefix?: string,
|
||||
quickActionLabel?: string,
|
||||
range?: number,
|
||||
readOnly?: boolean,
|
||||
stretch?: boolean,
|
||||
|
@ -41,8 +30,6 @@ type Props = {
|
|||
type?: string,
|
||||
value?: string | number,
|
||||
onChange?: (any) => any,
|
||||
openEmoteMenu?: () => void,
|
||||
quickActionHandler?: (any) => any,
|
||||
render?: () => React$Node,
|
||||
};
|
||||
|
||||
|
@ -72,21 +59,15 @@ export class FormField extends React.PureComponent<Props> {
|
|||
children,
|
||||
error,
|
||||
helper,
|
||||
hideSuggestions,
|
||||
inputButton,
|
||||
isLivestream,
|
||||
label,
|
||||
labelOnLeft,
|
||||
name,
|
||||
noEmojis,
|
||||
postfix,
|
||||
prefix,
|
||||
quickActionLabel,
|
||||
stretch,
|
||||
textAreaMaxLength,
|
||||
type,
|
||||
openEmoteMenu,
|
||||
quickActionHandler,
|
||||
render,
|
||||
...inputProps
|
||||
} = this.props;
|
||||
|
@ -101,18 +82,10 @@ export class FormField extends React.PureComponent<Props> {
|
|||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
||||
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
||||
);
|
||||
|
||||
const Wrapper = blockWrap
|
||||
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
|
||||
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
|
||||
|
||||
const quickAction =
|
||||
quickActionLabel && quickActionHandler ? (
|
||||
<div className="form-field__quick-action">
|
||||
<Button button="link" onClick={quickActionHandler} label={quickActionLabel} />
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const inputSimple = (type: string) => (
|
||||
<>
|
||||
<input id={name} type={type} {...inputProps} />
|
||||
|
@ -143,133 +116,22 @@ export class FormField extends React.PureComponent<Props> {
|
|||
return inputSelect('');
|
||||
case 'select-tiny':
|
||||
return inputSelect('select--slim');
|
||||
case 'markdown':
|
||||
const handleEvents = { contextmenu: openEditorMenu };
|
||||
|
||||
const getInstance = (editor) => {
|
||||
// SimpleMDE max char check
|
||||
editor.codemirror.on('beforeChange', (instance, changes) => {
|
||||
if (textAreaMaxLength && changes.update) {
|
||||
var str = changes.text.join('\n');
|
||||
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
||||
|
||||
if (delta <= 0) return;
|
||||
|
||||
delta = instance.getValue().length + delta - textAreaMaxLength;
|
||||
if (delta > 0) {
|
||||
str = str.substring(0, str.length - delta);
|
||||
changes.update(changes.from, changes.to, str.split('\n'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// "Create Link (Ctrl-K)": highlight URL instead of label:
|
||||
editor.codemirror.on('changes', (instance, changes) => {
|
||||
try {
|
||||
// Grab the last change from the buffered list. I assume the
|
||||
// buffered one ('changes', instead of 'change') is more efficient,
|
||||
// and that "Create Link" will always end up last in the list.
|
||||
const lastChange = changes[changes.length - 1];
|
||||
if (lastChange.origin === '+input') {
|
||||
// https://github.com/Ionaru/easy-markdown-editor/blob/8fa54c496f98621d5f45f57577ce630bee8c41ee/src/js/easymde.js#L765
|
||||
const EASYMDE_URL_PLACEHOLDER = '(https://)';
|
||||
|
||||
// The URL placeholder is always placed last, so just look at the
|
||||
// last text in the array to also cover the multi-line case:
|
||||
const urlLineText = lastChange.text[lastChange.text.length - 1];
|
||||
|
||||
if (urlLineText.endsWith(EASYMDE_URL_PLACEHOLDER) && urlLineText !== '[]' + EASYMDE_URL_PLACEHOLDER) {
|
||||
const from = lastChange.from;
|
||||
const to = lastChange.to;
|
||||
const isSelectionMultiline = lastChange.text.length > 1;
|
||||
const baseIndex = isSelectionMultiline ? 0 : from.ch;
|
||||
|
||||
// Everything works fine for the [Ctrl-K] case, but for the
|
||||
// [Button] case, this handler happens before the original
|
||||
// code, thus our change got wiped out.
|
||||
// Add a small delay to handle that case.
|
||||
setTimeout(() => {
|
||||
instance.setSelection(
|
||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf('(') + 1 },
|
||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf(')') }
|
||||
);
|
||||
}, 25);
|
||||
}
|
||||
}
|
||||
} catch (e) {} // Do nothing (revert to original behavior)
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
||||
<fieldset-section>
|
||||
<div className="form-field__two-column">
|
||||
<div>
|
||||
<label htmlFor={name}>{label}</label>
|
||||
</div>
|
||||
{quickAction}
|
||||
</div>
|
||||
<SimpleMDE
|
||||
{...inputProps}
|
||||
id={name}
|
||||
type="textarea"
|
||||
events={handleEvents}
|
||||
getMdeInstance={getInstance}
|
||||
options={{
|
||||
spellChecker: true,
|
||||
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
|
||||
previewRender(plainText) {
|
||||
const preview = <MarkdownPreview content={plainText} noDataStore />;
|
||||
return ReactDOMServer.renderToString(preview);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{countInfo}
|
||||
</fieldset-section>
|
||||
</div>
|
||||
);
|
||||
case 'textarea':
|
||||
return (
|
||||
<fieldset-section>
|
||||
{(label || quickAction) && (
|
||||
{label && (
|
||||
<div className="form-field__two-column">
|
||||
<label htmlFor={name}>{label}</label>
|
||||
{quickAction}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hideSuggestions ? (
|
||||
<textarea
|
||||
type={type}
|
||||
id={name}
|
||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||
ref={this.input}
|
||||
{...inputProps}
|
||||
/>
|
||||
) : (
|
||||
<TextareaWithSuggestions
|
||||
type={type}
|
||||
id={name}
|
||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||
inputRef={this.input}
|
||||
isLivestream={isLivestream}
|
||||
{...inputProps}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="form-field__textarea-info">
|
||||
{!noEmojis && openEmoteMenu && (
|
||||
<Button
|
||||
type="alt"
|
||||
className="button--comment-icons"
|
||||
title="Emotes"
|
||||
onClick={openEmoteMenu}
|
||||
icon={ICONS.EMOJI}
|
||||
iconSize={20}
|
||||
/>
|
||||
)}
|
||||
{countInfo}
|
||||
</div>
|
||||
<textarea
|
||||
type={type}
|
||||
id={name}
|
||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||
ref={this.input}
|
||||
{...inputProps}
|
||||
/>
|
||||
<div className="form-field__textarea-info">{countInfo}</div>
|
||||
</fieldset-section>
|
||||
);
|
||||
default:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export { Form } from './form-components/form';
|
||||
export { FormField } from './form-components/form-field';
|
||||
export { FormFieldAreaAdvanced } from './form-components/form-field-area-advanced';
|
||||
export { FormFieldPrice } from './form-components/form-field-price';
|
||||
export { Submit } from './form-components/submit';
|
||||
|
|
|
@ -2054,4 +2054,15 @@ export const icons = {
|
|||
<path d="M12.5,23.24v-1A10.74,10.74,0,0,1,23.24,11.52" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.SIMPLE_EDITOR]: buildIcon(
|
||||
<g>
|
||||
<path d="M1 18V6c0-1 1-2 2-2h18c1 0 2 1 2 2v12c0 1-1 2-2 2H3c-1 0-2-1-2-2ZM5 7v4" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.ADVANCED_EDITOR]: buildIcon(
|
||||
<g>
|
||||
<path d="M1 20V4c0-1 1-2 2-2h18c1 0 2 1 2 2v16c0 1-1 2-2 2H3c-1 0-2-1-2-2ZM1 11h22" />
|
||||
<path d="M5 8V6h2v2H5ZM11 8V6h2v2h-2ZM17 8V6h2v2h-2ZM5 14v4" />
|
||||
</g>
|
||||
),
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
import React, { useEffect } from 'react';
|
||||
import { FormField } from 'component/common/form';
|
||||
import { FormFieldAreaAdvanced } from 'component/common/form';
|
||||
|
||||
type Props = {
|
||||
uri: ?string,
|
||||
|
@ -99,7 +99,7 @@ function PostEditor(props: Props) {
|
|||
]);
|
||||
|
||||
return (
|
||||
<FormField
|
||||
<FormFieldAreaAdvanced
|
||||
type={'markdown'}
|
||||
name="content_post"
|
||||
label={label}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
||||
import React from 'react';
|
||||
import { FormField } from 'component/common/form';
|
||||
import { FormFieldAreaAdvanced } from 'component/common/form';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import Card from 'component/common/card';
|
||||
|
||||
|
@ -27,7 +27,7 @@ function PublishDescription(props: Props) {
|
|||
return (
|
||||
<Card
|
||||
actions={
|
||||
<FormField
|
||||
<FormFieldAreaAdvanced
|
||||
type={advancedEditor ? 'markdown' : 'textarea'}
|
||||
name="content_description"
|
||||
label={__('Description')}
|
||||
|
|
|
@ -186,3 +186,5 @@ export const MYSTERIES = 'Mysteries';
|
|||
export const TECHNOLOGY = 'Technology';
|
||||
export const EMOJI = 'Emoji';
|
||||
export const STICKER = 'Sticker';
|
||||
export const SIMPLE_EDITOR = 'SimpleEditor';
|
||||
export const ADVANCED_EDITOR = 'AdvancedEditor';
|
||||
|
|
|
@ -62,7 +62,7 @@ class ReportPage extends React.Component {
|
|||
name="message"
|
||||
stretch
|
||||
value={this.state.message}
|
||||
onChange={event => {
|
||||
onChange={(event) => {
|
||||
this.onMessageChange(event);
|
||||
}}
|
||||
placeholder={__('Description of your issue or feature request')}
|
||||
|
@ -71,7 +71,7 @@ class ReportPage extends React.Component {
|
|||
<div className="section__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
onClick={event => {
|
||||
onClick={(event) => {
|
||||
this.submitMessage(event);
|
||||
}}
|
||||
className={`button-block button-primary ${this.state.submitting ? 'disabled' : ''}`}
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
margin: 0px var(--spacing-xxs);
|
||||
}
|
||||
|
||||
.button + .commentCreate {
|
||||
.button + .comment-create {
|
||||
margin-top: var(--spacing-xxs);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.commentCreate {
|
||||
.comment-create {
|
||||
font-size: var(--font-small);
|
||||
position: relative;
|
||||
|
||||
|
@ -135,12 +135,12 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.commentCreate--reply {
|
||||
.comment-create--reply {
|
||||
margin-top: var(--spacing-m);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.commentCreate--nestedReply {
|
||||
.comment-create--nestedReply {
|
||||
margin-top: var(--spacing-s);
|
||||
margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px);
|
||||
|
||||
|
@ -149,27 +149,40 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.commentCreate--bottom {
|
||||
.comment-create--bottom {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.comment-create__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
.comment-create__header-button {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.button--alt {
|
||||
padding: var(--spacing-xs);
|
||||
height: unset;
|
||||
margin-bottom: var(--spacing-xxs);
|
||||
background: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-create__label-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
max-width: 50%;
|
||||
|
||||
.comment-create__label {
|
||||
white-space: nowrap;
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
fieldset-section {
|
||||
max-width: 10rem;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
fieldset-section {
|
||||
font-size: var(--font-xxsmall);
|
||||
|
@ -179,14 +192,14 @@ $thumbnailWidthSmall: 1rem;
|
|||
font-size: var(--font-xxsmall);
|
||||
}
|
||||
|
||||
select {
|
||||
height: 1rem;
|
||||
margin: var(--spacing-xxs) 0px;
|
||||
}
|
||||
//select {
|
||||
// height: 1rem;
|
||||
// margin: var(--spacing-xxs) 0px;
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
.commentCreate__supportCommentPreview {
|
||||
.comment-create__supportCommentPreview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--color-border);
|
||||
|
@ -194,7 +207,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
padding: var(--spacing-s);
|
||||
margin: var(--spacing-s) 0;
|
||||
|
||||
.commentCreate__supportCommentPreviewAmount {
|
||||
.comment-create__supportCommentPreviewAmount {
|
||||
margin-right: var(--spacing-m);
|
||||
font-size: var(--font-large);
|
||||
}
|
||||
|
@ -223,8 +236,8 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.commentCreate__stickerPreview {
|
||||
@extend .commentCreate;
|
||||
.comment-create__stickerPreview {
|
||||
@extend .comment-create;
|
||||
display: flex;
|
||||
background-color: var(--color-header-background);
|
||||
border-radius: var(--border-radius);
|
||||
|
@ -234,12 +247,12 @@ $thumbnailWidthSmall: 1rem;
|
|||
width: 100%;
|
||||
height: 10rem;
|
||||
|
||||
.commentCreate__stickerPreviewInfo {
|
||||
.comment-create__stickerPreviewInfo {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.commentCreate__stickerPreviewImage {
|
||||
.comment-create__stickerPreviewImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-left: var(--spacing-m);
|
||||
|
|
|
@ -29,7 +29,12 @@ select,
|
|||
background-color: var(--color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: var(--height-input);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-input);
|
||||
background-color: var(--color-input-bg);
|
||||
}
|
||||
@media (min-width: $breakpoint-small) {
|
||||
textarea {
|
||||
height: var(--height-input);
|
||||
|
@ -532,6 +537,7 @@ fieldset-group {
|
|||
}
|
||||
|
||||
.form-field__quick-action {
|
||||
text-align: right;
|
||||
font-size: var(--font-xsmall);
|
||||
}
|
||||
|
||||
|
|
|
@ -32,13 +32,17 @@ $contentMaxWidth: 60rem;
|
|||
}
|
||||
}
|
||||
|
||||
.commentCreate {
|
||||
.comment-create {
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding-top: var(--spacing-s);
|
||||
|
||||
.commentCreate__label {
|
||||
.comment-create__label {
|
||||
color: var(--color-text);
|
||||
}
|
||||
.comment-create__header {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 1fr;
|
||||
}
|
||||
textarea,
|
||||
select,
|
||||
.button:not(.button--file-action) {
|
||||
|
@ -81,7 +85,7 @@ $contentMaxWidth: 60rem;
|
|||
}
|
||||
}
|
||||
|
||||
.commentCreate,
|
||||
.comment-create,
|
||||
.comment__content {
|
||||
margin: var(--spacing-m);
|
||||
margin-bottom: 0;
|
||||
|
|
|
@ -85,7 +85,7 @@
|
|||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.card__main-actions .commentCreate .MuiOutlinedInput-notchedOutline {
|
||||
.card__main-actions .comment-create .MuiOutlinedInput-notchedOutline {
|
||||
border: 1px solid var(--color-border) !important;
|
||||
border-radius: var(--border-radius) !important;
|
||||
}
|
||||
|
@ -104,7 +104,7 @@
|
|||
|
||||
textarea {
|
||||
border: none;
|
||||
margin: 9px 0px;
|
||||
padding: var(--spacing-xxs) var(--spacing-xxs);
|
||||
}
|
||||
|
||||
button {
|
||||
|
@ -320,7 +320,7 @@
|
|||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
.commentCreate {
|
||||
.comment-create {
|
||||
.section__actions {
|
||||
.button {
|
||||
background-color: var(--color-header-button);
|
||||
|
|
Loading…
Reference in a new issue