2018-03-26 23:32:43 +02:00
|
|
|
// @flow
|
2020-02-21 23:05:10 +01:00
|
|
|
import type { ElementRef, Node } from 'react';
|
2019-05-08 03:42:56 +02:00
|
|
|
import React from 'react';
|
2018-06-10 23:57:46 +02:00
|
|
|
import ReactDOMServer from 'react-dom/server';
|
2019-05-08 03:42:56 +02:00
|
|
|
import SimpleMDE from 'react-simplemde-editor';
|
2020-04-29 22:50:06 +02:00
|
|
|
import MarkdownPreview from 'component/common/markdown-preview';
|
2019-06-09 22:31:33 +02:00
|
|
|
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
2020-06-12 17:18:04 +02:00
|
|
|
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
2019-06-09 22:31:33 +02:00
|
|
|
import 'easymde/dist/easymde.min.css';
|
2020-05-21 09:25:37 +02:00
|
|
|
import Button from 'component/button';
|
2021-04-23 21:59:48 +02:00
|
|
|
import emoji from 'emoji-dictionary';
|
|
|
|
|
|
|
|
const QUICK_EMOJIS = [
|
|
|
|
emoji.getUnicode('rocket'),
|
|
|
|
emoji.getUnicode('jeans'),
|
|
|
|
emoji.getUnicode('fire'),
|
|
|
|
emoji.getUnicode('heart'),
|
|
|
|
emoji.getUnicode('open_mouth'),
|
|
|
|
];
|
2019-04-03 07:56:58 +02:00
|
|
|
|
2018-03-26 23:32:43 +02:00
|
|
|
type Props = {
|
|
|
|
name: string,
|
2020-02-21 23:05:10 +01:00
|
|
|
label?: string | Node,
|
2019-04-24 16:02:08 +02:00
|
|
|
render?: () => React$Node,
|
2018-03-26 23:32:43 +02:00
|
|
|
prefix?: string,
|
|
|
|
postfix?: string,
|
|
|
|
error?: string | boolean,
|
2019-04-24 16:02:08 +02:00
|
|
|
helper?: string | React$Node,
|
2018-03-26 23:32:43 +02:00
|
|
|
type?: string,
|
2021-02-26 08:38:33 +01:00
|
|
|
onChange?: (any) => any,
|
2018-03-26 23:32:43 +02:00
|
|
|
defaultValue?: string | number,
|
|
|
|
placeholder?: string | number,
|
2019-04-24 16:02:08 +02:00
|
|
|
children?: React$Node,
|
2018-03-26 23:32:43 +02:00
|
|
|
stretch?: boolean,
|
2018-06-14 22:10:50 +02:00
|
|
|
affixClass?: string, // class applied to prefix/postfix label
|
2018-09-22 03:20:58 +02:00
|
|
|
autoFocus?: boolean,
|
2019-02-13 17:27:20 +01:00
|
|
|
labelOnLeft: boolean,
|
2019-04-24 16:02:08 +02:00
|
|
|
inputButton?: React$Node,
|
2019-02-18 18:24:56 +01:00
|
|
|
blockWrap: boolean,
|
2019-09-30 23:16:46 +02:00
|
|
|
charCount?: number,
|
|
|
|
textAreaMaxLength?: number,
|
2020-03-25 04:15:05 +01:00
|
|
|
range?: number,
|
|
|
|
min?: number,
|
|
|
|
max?: number,
|
2020-05-21 09:25:37 +02:00
|
|
|
quickActionLabel?: string,
|
2021-02-26 08:38:33 +01:00
|
|
|
quickActionHandler?: (any) => any,
|
2021-04-23 21:59:48 +02:00
|
|
|
disabled?: boolean,
|
|
|
|
onChange: (any) => void,
|
|
|
|
value?: string | number,
|
2021-05-12 16:19:45 +02:00
|
|
|
noEmojis?: boolean,
|
2018-03-26 23:32:43 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
export class FormField extends React.PureComponent<Props> {
|
2019-02-13 17:27:20 +01:00
|
|
|
static defaultProps = {
|
|
|
|
labelOnLeft: false,
|
2019-02-18 18:24:56 +01:00
|
|
|
blockWrap: true,
|
2019-02-13 17:27:20 +01:00
|
|
|
};
|
|
|
|
|
2019-04-24 16:02:08 +02:00
|
|
|
input: { current: ElementRef<any> };
|
2019-02-20 06:20:29 +01:00
|
|
|
|
2019-02-18 18:24:56 +01:00
|
|
|
constructor(props: Props) {
|
2018-09-22 03:20:58 +02:00
|
|
|
super(props);
|
|
|
|
this.input = React.createRef();
|
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount() {
|
|
|
|
const { autoFocus } = this.props;
|
|
|
|
const input = this.input.current;
|
|
|
|
|
|
|
|
if (input && autoFocus) {
|
|
|
|
input.focus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-26 23:32:43 +02:00
|
|
|
render() {
|
|
|
|
const {
|
|
|
|
render,
|
|
|
|
label,
|
|
|
|
prefix,
|
|
|
|
postfix,
|
|
|
|
error,
|
|
|
|
helper,
|
|
|
|
name,
|
|
|
|
type,
|
|
|
|
children,
|
|
|
|
stretch,
|
2018-06-14 22:10:50 +02:00
|
|
|
affixClass,
|
2018-09-22 03:20:58 +02:00
|
|
|
autoFocus,
|
2019-02-13 17:27:20 +01:00
|
|
|
inputButton,
|
|
|
|
labelOnLeft,
|
2019-02-18 18:24:56 +01:00
|
|
|
blockWrap,
|
2019-09-30 23:16:46 +02:00
|
|
|
charCount,
|
2021-03-20 00:48:47 +01:00
|
|
|
textAreaMaxLength,
|
2020-05-21 09:25:37 +02:00
|
|
|
quickActionLabel,
|
|
|
|
quickActionHandler,
|
2021-05-12 16:19:45 +02:00
|
|
|
noEmojis,
|
2018-03-26 23:32:43 +02:00
|
|
|
...inputProps
|
|
|
|
} = this.props;
|
2018-04-05 23:26:20 +02:00
|
|
|
const errorMessage = typeof error === 'object' ? error.message : error;
|
2019-02-18 18:24:56 +01:00
|
|
|
const Wrapper = blockWrap
|
2019-11-22 22:13:00 +01:00
|
|
|
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
|
|
|
|
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
|
2019-02-18 18:24:56 +01:00
|
|
|
|
2020-05-21 09:25:37 +02:00
|
|
|
const quickAction =
|
|
|
|
quickActionLabel && quickActionHandler ? (
|
|
|
|
<div className="form-field__quick-action">
|
|
|
|
<Button button="link" onClick={quickActionHandler} label={quickActionLabel} />
|
|
|
|
</div>
|
|
|
|
) : null;
|
|
|
|
|
2018-03-26 23:32:43 +02:00
|
|
|
let input;
|
|
|
|
if (type) {
|
2019-02-13 17:27:20 +01:00
|
|
|
if (type === 'radio') {
|
|
|
|
input = (
|
2019-02-18 18:24:56 +01:00
|
|
|
<Wrapper>
|
2019-11-22 22:13:00 +01:00
|
|
|
<input id={name} type="radio" {...inputProps} />
|
|
|
|
<label htmlFor={name}>{label}</label>
|
2019-02-18 18:24:56 +01:00
|
|
|
</Wrapper>
|
2019-02-13 17:27:20 +01:00
|
|
|
);
|
|
|
|
} else if (type === 'checkbox') {
|
|
|
|
input = (
|
2019-11-22 22:13:00 +01:00
|
|
|
<div className="checkbox">
|
|
|
|
<input id={name} type="checkbox" {...inputProps} />
|
|
|
|
<label htmlFor={name}>{label}</label>
|
|
|
|
</div>
|
2019-02-13 17:27:20 +01:00
|
|
|
);
|
2020-03-25 04:15:05 +01:00
|
|
|
} else if (type === 'range') {
|
|
|
|
input = (
|
|
|
|
<div>
|
|
|
|
<input id={name} type="range" {...inputProps} />
|
|
|
|
<label htmlFor={name}>{label}</label>
|
|
|
|
</div>
|
|
|
|
);
|
2019-02-13 17:27:20 +01:00
|
|
|
} else if (type === 'select') {
|
2018-03-26 23:32:43 +02:00
|
|
|
input = (
|
2019-02-13 17:27:20 +01:00
|
|
|
<fieldset-section>
|
2020-03-13 20:54:31 +01:00
|
|
|
{(label || errorMessage) && (
|
2020-04-13 21:16:07 +02:00
|
|
|
<label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label>
|
2020-03-13 20:54:31 +01:00
|
|
|
)}
|
2019-02-13 17:27:20 +01:00
|
|
|
<select id={name} {...inputProps}>
|
2018-12-19 06:44:53 +01:00
|
|
|
{children}
|
|
|
|
</select>
|
2019-02-13 17:27:20 +01:00
|
|
|
</fieldset-section>
|
2020-08-24 19:35:21 +02:00
|
|
|
);
|
|
|
|
} else if (type === 'select-tiny') {
|
|
|
|
input = (
|
|
|
|
<fieldset-section class="select--slim">
|
|
|
|
{(label || errorMessage) && (
|
|
|
|
<label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label>
|
|
|
|
)}
|
|
|
|
<select id={name} {...inputProps}>
|
|
|
|
{children}
|
|
|
|
</select>
|
|
|
|
</fieldset-section>
|
2018-03-26 23:32:43 +02:00
|
|
|
);
|
|
|
|
} else if (type === 'markdown') {
|
2018-06-25 22:42:49 +02:00
|
|
|
const handleEvents = {
|
2018-07-29 01:48:54 +02:00
|
|
|
contextmenu: openEditorMenu,
|
2018-06-25 22:42:49 +02:00
|
|
|
};
|
2018-07-29 01:48:54 +02:00
|
|
|
|
2021-02-26 08:38:33 +01:00
|
|
|
const getInstance = (editor) => {
|
2020-06-12 16:52:24 +02:00
|
|
|
// 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.substr(0, str.length - delta);
|
|
|
|
changes.update(changes.from, changes.to, str.split('\n'));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2021-02-26 08:38:33 +01:00
|
|
|
|
|
|
|
// "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 (err) {
|
|
|
|
// Do nothing (revert to original behavior)
|
|
|
|
}
|
|
|
|
});
|
2020-06-12 16:52:24 +02:00
|
|
|
};
|
|
|
|
|
2020-06-13 05:58:53 +02:00
|
|
|
// 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;
|
2021-03-20 00:48:47 +01:00
|
|
|
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
2020-06-13 05:58:53 +02:00
|
|
|
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
|
|
|
);
|
|
|
|
|
2018-03-26 23:32:43 +02:00
|
|
|
input = (
|
2019-03-21 16:22:23 +01:00
|
|
|
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
2019-02-13 17:27:20 +01:00
|
|
|
<fieldset-section>
|
2020-05-21 09:25:37 +02:00
|
|
|
<div className="form-field__two-column">
|
|
|
|
<div>
|
|
|
|
<label htmlFor={name}>{label}</label>
|
|
|
|
</div>
|
|
|
|
{quickAction}
|
|
|
|
</div>
|
2019-05-08 03:42:56 +02:00
|
|
|
<SimpleMDE
|
|
|
|
{...inputProps}
|
|
|
|
id={name}
|
|
|
|
type="textarea"
|
|
|
|
events={handleEvents}
|
2020-06-12 16:52:24 +02:00
|
|
|
getMdeInstance={getInstance}
|
2019-05-08 03:42:56 +02:00
|
|
|
options={{
|
2020-01-24 21:32:22 +01:00
|
|
|
spellChecker: true,
|
2019-05-08 03:42:56 +02:00
|
|
|
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
|
|
|
|
previewRender(plainText) {
|
2020-09-29 05:46:18 +02:00
|
|
|
const preview = <MarkdownPreview content={plainText} noDataStore />;
|
2019-05-08 03:42:56 +02:00
|
|
|
return ReactDOMServer.renderToString(preview);
|
|
|
|
},
|
|
|
|
}}
|
|
|
|
/>
|
2020-06-13 05:58:53 +02:00
|
|
|
{countInfo}
|
2019-02-13 17:27:20 +01:00
|
|
|
</fieldset-section>
|
2018-03-26 23:32:43 +02:00
|
|
|
</div>
|
|
|
|
);
|
2018-04-06 07:15:29 +02:00
|
|
|
} else if (type === 'textarea') {
|
2019-09-30 23:16:46 +02:00
|
|
|
const hasCharCount = charCount !== undefined && charCount >= 0;
|
2021-03-20 00:48:47 +01:00
|
|
|
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
2019-10-01 00:08:16 +02:00
|
|
|
<span className="comment__char-count">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
2019-09-30 23:16:46 +02:00
|
|
|
);
|
2019-02-13 17:27:20 +01:00
|
|
|
input = (
|
|
|
|
<fieldset-section>
|
2020-08-31 18:28:28 +02:00
|
|
|
{(label || quickAction) && (
|
|
|
|
<div className="form-field__two-column">
|
|
|
|
<div>
|
|
|
|
<label htmlFor={name}>{label}</label>
|
|
|
|
</div>
|
|
|
|
{quickAction}
|
2020-05-21 09:25:37 +02:00
|
|
|
</div>
|
2020-08-31 18:28:28 +02:00
|
|
|
)}
|
2021-03-20 00:48:47 +01:00
|
|
|
<textarea
|
|
|
|
type={type}
|
|
|
|
id={name}
|
|
|
|
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
|
|
|
ref={this.input}
|
|
|
|
{...inputProps}
|
|
|
|
/>
|
2021-04-23 21:59:48 +02:00
|
|
|
<div className="form-field__textarea-info">
|
2021-05-12 16:19:45 +02:00
|
|
|
{!noEmojis && <div className="form-field__quick-emojis">
|
2021-04-23 21:59:48 +02:00
|
|
|
{QUICK_EMOJIS.map((emoji) => (
|
|
|
|
<Button
|
|
|
|
key={emoji}
|
|
|
|
disabled={inputProps.disabled}
|
|
|
|
type="button"
|
|
|
|
className="button--emoji"
|
|
|
|
label={emoji}
|
|
|
|
onClick={() => {
|
|
|
|
inputProps.onChange({
|
|
|
|
target: { value: inputProps.value ? `${inputProps.value} ${emoji}` : emoji },
|
|
|
|
});
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
))}
|
2021-05-12 16:19:45 +02:00
|
|
|
</div>}
|
2021-04-23 21:59:48 +02:00
|
|
|
{countInfo}
|
|
|
|
</div>
|
2019-02-13 17:27:20 +01:00
|
|
|
</fieldset-section>
|
|
|
|
);
|
2018-03-26 23:32:43 +02:00
|
|
|
} else {
|
2019-02-13 17:27:20 +01:00
|
|
|
const inputElement = <input type={type} id={name} {...inputProps} ref={this.input} />;
|
|
|
|
const inner = inputButton ? (
|
|
|
|
<input-submit>
|
|
|
|
{inputElement}
|
|
|
|
{inputButton}
|
|
|
|
</input-submit>
|
|
|
|
) : (
|
|
|
|
inputElement
|
|
|
|
);
|
|
|
|
|
|
|
|
input = (
|
|
|
|
<React.Fragment>
|
|
|
|
<fieldset-section>
|
2020-02-06 19:49:05 +01:00
|
|
|
{(label || errorMessage) && (
|
2019-11-22 22:13:00 +01:00
|
|
|
<label htmlFor={name}>
|
2020-04-13 21:16:07 +02:00
|
|
|
{errorMessage ? <span className="error__text">{errorMessage}</span> : label}
|
2019-11-22 22:13:00 +01:00
|
|
|
</label>
|
|
|
|
)}
|
2019-09-26 18:07:11 +02:00
|
|
|
{prefix && <label htmlFor={name}>{prefix}</label>}
|
2019-02-13 17:27:20 +01:00
|
|
|
{inner}
|
|
|
|
</fieldset-section>
|
|
|
|
</React.Fragment>
|
|
|
|
);
|
2018-03-26 23:32:43 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
2019-02-13 17:27:20 +01:00
|
|
|
<React.Fragment>
|
|
|
|
{input}
|
|
|
|
|
2019-03-21 16:22:23 +01:00
|
|
|
{helper && <div className="form-field__help">{helper}</div>}
|
2019-02-13 17:27:20 +01:00
|
|
|
</React.Fragment>
|
2018-03-26 23:32:43 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default FormField;
|