Cleanup Form-Field

- avoid declaring components inside the body function of parent components https://dev.to/borasvm/react-create-component-inside-a-component-456b
This commit is contained in:
Rafael 2022-02-08 08:36:47 -03:00 committed by Thomas Zarebczan
parent ba5d96bb71
commit 1f367c641e
5 changed files with 324 additions and 195 deletions

View file

@ -0,0 +1,52 @@
// @flow
import * as React from 'react';
import Button from 'component/button';
type CountInfoProps = {
charCount?: number,
textAreaMaxLength?: number,
};
export const CountInfo = (countInfoProps: CountInfoProps) => {
const { charCount, textAreaMaxLength } = countInfoProps;
// 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;
return (
hasCharCount &&
textAreaMaxLength !== undefined && (
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
)
);
};
type QuickActionProps = {
label?: string,
quickActionHandler?: (any) => any,
};
export const QuickAction = (quickActionProps: QuickActionProps) => {
const { label, quickActionHandler } = quickActionProps;
return label && quickActionHandler ? (
<div className="form-field__quick-action">
<Button button="link" onClick={quickActionHandler} label={label} />
</div>
) : null;
};
type LabelProps = {
name: string,
label?: any,
errorMessage?: any,
};
export const Label = (labelProps: LabelProps) => {
const { name, label, errorMessage } = labelProps;
return <label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label>;
};

View file

@ -1,16 +1,18 @@
// @flow // @flow
import 'easymde/dist/easymde.min.css'; import 'easymde/dist/easymde.min.css';
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field'; import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
import { openEditorMenu, stopContextMenu } from 'util/context-menu'; import { openEditorMenu, stopContextMenu } from 'util/context-menu';
import { lazyImport } from 'util/lazyImport'; import { lazyImport } from 'util/lazyImport';
import Button from 'component/button';
import MarkdownPreview from 'component/common/markdown-preview'; import MarkdownPreview from 'component/common/markdown-preview';
import React from 'react'; import React from 'react';
import ReactDOMServer from 'react-dom/server'; import ReactDOMServer from 'react-dom/server';
import SimpleMDE from 'react-simplemde-editor'; import SimpleMDE from 'react-simplemde-editor';
import type { ElementRef, Node } from 'react'; import type { ElementRef } from 'react';
import Drawer from '@mui/material/Drawer'; import { InputSimple, BlockWrapWrapper } from './input-simple';
import CommentSelectors from 'component/commentCreate/comment-selectors'; import { InputSelect } from './input-select';
import { CountInfo, QuickAction, Label } from './common';
import { TextareaWrapper } from './slim-input-field';
// prettier-ignore // prettier-ignore
const TextareaWithSuggestions = lazyImport(() => import('component/textareaWithSuggestions' /* webpackChunkName: "suggestions" */)); const TextareaWithSuggestions = lazyImport(() => import('component/textareaWithSuggestions' /* webpackChunkName: "suggestions" */));
@ -29,7 +31,7 @@ type Props = {
hideSuggestions?: boolean, hideSuggestions?: boolean,
inputButton?: React$Node, inputButton?: React$Node,
isLivestream?: boolean, isLivestream?: boolean,
label?: string | Node, label?: any,
labelOnLeft: boolean, labelOnLeft: boolean,
max?: number, max?: number,
min?: number, min?: number,
@ -124,122 +126,119 @@ export class FormField extends React.PureComponent<Props, State> {
const errorMessage = typeof error === 'object' ? error.message : error; const errorMessage = typeof error === 'object' ? error.message : error;
// Ideally, the character count should (and can) be appended to the const wrapperProps = { type, helper };
// SimpleMDE's "options::status" bar. However, I couldn't figure out how const labelProps = { name, label };
// to pass the current value to it's callback, nor query the current const countInfoProps = { charCount, textAreaMaxLength };
// text length from the callback. So, we'll use our own widget. const quickActionProps = { label: quickActionLabel, quickActionHandler };
const hasCharCount = charCount !== undefined && charCount >= 0; const inputSimpleProps = { name, label, ...inputProps };
const countInfo = hasCharCount && textAreaMaxLength !== undefined && ( const inputSelectProps = { name, error, label, children, ...inputProps };
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
);
const Wrapper = blockWrap switch (type) {
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section> case 'radio':
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>; return (
<FormFieldWrapper {...wrapperProps}>
<BlockWrapWrapper blockWrap={blockWrap}>
<InputSimple {...inputSimpleProps} type="radio" />
</BlockWrapWrapper>
</FormFieldWrapper>
);
case 'checkbox':
return (
<FormFieldWrapper {...wrapperProps}>
<div className="checkbox">
<InputSimple {...inputSimpleProps} type="checkbox" />
</div>
</FormFieldWrapper>
);
case 'range':
return (
<FormFieldWrapper {...wrapperProps}>
<div className="range">
<InputSimple {...inputSimpleProps} type="range" />
</div>
</FormFieldWrapper>
);
case 'select':
return (
<FormFieldWrapper {...wrapperProps}>
<InputSelect {...inputSelectProps} />
</FormFieldWrapper>
);
case 'select-tiny':
return (
<FormFieldWrapper {...wrapperProps}>
<InputSelect {...inputSelectProps} className="select--slim" />
</FormFieldWrapper>
);
case 'markdown':
const handleEvents = { contextmenu: openEditorMenu };
const quickAction = const getInstance = (editor) => {
quickActionLabel && quickActionHandler ? ( // SimpleMDE max char check
<div className="form-field__quick-action"> editor.codemirror.on('beforeChange', (instance, changes) => {
<Button button="link" onClick={quickActionHandler} label={quickActionLabel} /> if (textAreaMaxLength && changes.update) {
</div> var str = changes.text.join('\n');
) : null; var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
const inputSimple = (type: string) => ( if (delta <= 0) return;
<>
<input id={name} type={type} {...inputProps} />
<label htmlFor={name}>{label}</label>
</>
);
const inputSelect = (selectClass: string) => ( delta = instance.getValue().length + delta - textAreaMaxLength;
<fieldset-section class={selectClass}> if (delta > 0) {
{(label || errorMessage) && ( str = str.substr(0, str.length - delta);
<label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label> changes.update(changes.from, changes.to, str.split('\n'));
)} }
<select id={name} {...inputProps}> }
{children} });
</select>
</fieldset-section>
);
const input = () => { // "Create Link (Ctrl-K)": highlight URL instead of label:
switch (type) { editor.codemirror.on('changes', (instance, changes) => {
case 'radio': try {
return <Wrapper>{inputSimple('radio')}</Wrapper>; // Grab the last change from the buffered list. I assume the
case 'checkbox': // buffered one ('changes', instead of 'change') is more efficient,
return <div className="checkbox">{inputSimple('checkbox')}</div>; // and that "Create Link" will always end up last in the list.
case 'range': const lastChange = changes[changes.length - 1];
return <div>{inputSimple('range')}</div>; if (lastChange.origin === '+input') {
case 'select': // https://github.com/Ionaru/easy-markdown-editor/blob/8fa54c496f98621d5f45f57577ce630bee8c41ee/src/js/easymde.js#L765
return inputSelect(''); const EASYMDE_URL_PLACEHOLDER = '(https://)';
case 'select-tiny':
return inputSelect('select--slim');
case 'markdown':
const handleEvents = { contextmenu: openEditorMenu };
const getInstance = (editor) => { // The URL placeholder is always placed last, so just look at the
// SimpleMDE max char check // last text in the array to also cover the multi-line case:
editor.codemirror.on('beforeChange', (instance, changes) => { const urlLineText = lastChange.text[lastChange.text.length - 1];
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; 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;
delta = instance.getValue().length + delta - textAreaMaxLength; // Everything works fine for the [Ctrl-K] case, but for the
if (delta > 0) { // [Button] case, this handler happens before the original
str = str.substr(0, str.length - delta); // code, thus our change got wiped out.
changes.update(changes.from, changes.to, str.split('\n')); // 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)
});
};
// "Create Link (Ctrl-K)": highlight URL instead of label: return (
editor.codemirror.on('changes', (instance, changes) => { <FormFieldWrapper {...wrapperProps}>
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}> <div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
<fieldset-section> <fieldset-section>
<div className="form-field__two-column"> <div className="form-field__two-column">
<div> <div>
<label htmlFor={name}>{label}</label> <Label {...labelProps} />
</div> </div>
{quickAction}
<QuickAction {...quickActionProps} />
</div> </div>
<SimpleMDE <SimpleMDE
{...inputProps} {...inputProps}
id={name} id={name}
@ -255,17 +254,20 @@ export class FormField extends React.PureComponent<Props, State> {
}, },
}} }}
/> />
{countInfo}
<CountInfo {...countInfoProps} />
</fieldset-section> </fieldset-section>
</div> </div>
); </FormFieldWrapper>
case 'textarea': );
const closeSelector = case 'textarea':
setShowSelectors && showSelectors const closeSelector =
? () => setShowSelectors({ tab: showSelectors.tab || undefined, open: false }) setShowSelectors && showSelectors
: () => {}; ? () => setShowSelectors({ tab: showSelectors.tab || undefined, open: false })
: () => {};
return ( return (
<FormFieldWrapper {...wrapperProps}>
<fieldset-section> <fieldset-section>
<TextareaWrapper <TextareaWrapper
isDrawerOpen={Boolean(this.state.drawerOpen)} isDrawerOpen={Boolean(this.state.drawerOpen)}
@ -277,11 +279,13 @@ export class FormField extends React.PureComponent<Props, State> {
slimInputButtonRef={slimInputButtonRef} slimInputButtonRef={slimInputButtonRef}
tipModalOpen={tipModalOpen} tipModalOpen={tipModalOpen}
> >
{(!slimInput || this.state.drawerOpen) && (label || quickAction) && ( {(!slimInput || this.state.drawerOpen) && label && (
<div className="form-field__two-column"> <div className="form-field__two-column">
<label htmlFor={name}>{label}</label> <Label {...labelProps} />
{quickAction}
{countInfo} <QuickAction {...quickActionProps} />
<CountInfo {...countInfoProps} />
</div> </div>
)} )}
@ -328,94 +332,48 @@ export class FormField extends React.PureComponent<Props, State> {
)} )}
</TextareaWrapper> </TextareaWrapper>
</fieldset-section> </fieldset-section>
); </FormFieldWrapper>
default: );
const inputElement = <input type={type} id={name} {...inputProps} ref={this.input} />; default:
const inner = inputButton ? ( const inputElementProps = { type, name, ref: this.input, ...inputProps };
<input-submit>
{inputElement}
{inputButton}
</input-submit>
) : (
inputElement
);
return ( return (
<FormFieldWrapper {...wrapperProps}>
<fieldset-section> <fieldset-section>
{(label || errorMessage) && ( {(label || errorMessage) && <Label {...labelProps} errorMessage={errorMessage} />}
<label htmlFor={name}>
{errorMessage ? <span className="error__text">{errorMessage}</span> : label}
</label>
)}
{prefix && <label htmlFor={name}>{prefix}</label>}
{inner}
</fieldset-section>
);
}
};
return ( {prefix && <label htmlFor={name}>{prefix}</label>}
<>
{type && input()} {inputButton ? (
{helper && <div className="form-field__help">{helper}</div>} <input-submit>
</> <input {...inputElementProps} />
); {inputButton}
</input-submit>
) : (
<input {...inputElementProps} />
)}
</fieldset-section>
</FormFieldWrapper>
);
}
} }
} }
export default FormField; export default FormField;
type TextareaWrapperProps = { type WrapperProps = {
slimInput?: boolean, type?: string,
slimInputButtonRef?: any, children?: any,
children: Node, helper?: any,
isDrawerOpen: boolean,
showSelectors?: boolean,
commentSelectorsProps?: any,
tipModalOpen?: boolean,
toggleDrawer: () => void,
closeSelector?: () => void,
}; };
function TextareaWrapper(wrapperProps: TextareaWrapperProps) { const FormFieldWrapper = (wrapperProps: WrapperProps) => {
const { const { type, children, helper } = wrapperProps;
children,
slimInput,
slimInputButtonRef,
isDrawerOpen,
commentSelectorsProps,
showSelectors,
tipModalOpen,
toggleDrawer,
closeSelector,
} = wrapperProps;
function handleCloseAll() { return (
toggleDrawer(); <>
if (closeSelector) closeSelector(); {type && children}
} {helper && <div className="form-field__help">{helper}</div>}
</>
return slimInput ? (
!isDrawerOpen ? (
<div ref={slimInputButtonRef} role="button" onClick={toggleDrawer}>
{children}
</div>
) : (
<Drawer
className="comment-create--drawer"
anchor="bottom"
open
onClose={handleCloseAll}
// The Modal tries to enforce focus when open and doesn't allow clicking or changing any
// other input boxes, so in this case it is disabled when trying to type in a custom tip
ModalProps={{ disableEnforceFocus: tipModalOpen }}
>
{children}
{showSelectors && <CommentSelectors closeSelector={closeSelector} {...commentSelectorsProps} />}
</Drawer>
)
) : (
<>{children}</>
); );
} };

View file

@ -0,0 +1,25 @@
// @flow
import * as React from 'react';
import { Label } from './common';
type InputSelectProps = {
name: string,
className?: string,
label?: any,
errorMessage?: any,
children?: any,
};
export const InputSelect = (inputSelectProps: InputSelectProps) => {
const { name, className, errorMessage, label, children, ...inputProps } = inputSelectProps;
return (
<fieldset-section class={className || ''}>
{(label || errorMessage) && <Label name={name} label={label} errorMessage={errorMessage} />}
<select id={name} {...inputProps}>
{children}
</select>
</fieldset-section>
);
};

View file

@ -0,0 +1,35 @@
// @flow
import * as React from 'react';
import { Label } from './common';
type InputSimpleProps = {
name: string,
type: string,
label?: any,
};
export const InputSimple = (inputSimpleProps: InputSimpleProps) => {
const { name, type, label, ...inputProps } = inputSimpleProps;
return (
<>
<input id={name} type={type} {...inputProps} />
<Label name={name} label={label} />
</>
);
};
type BlockWrapProps = {
blockWrap: boolean,
children?: any,
};
export const BlockWrapWrapper = (blockWrapProps: BlockWrapProps) => {
const { blockWrap, children } = blockWrapProps;
return blockWrap ? (
<fieldset-section class="radio">{children}</fieldset-section>
) : (
<span className="radio">{children}</span>
);
};

View file

@ -0,0 +1,59 @@
// @flow
import React from 'react';
import Drawer from '@mui/material/Drawer';
import CommentSelectors from 'component/commentCreate/comment-selectors';
type TextareaWrapperProps = {
slimInput?: boolean,
slimInputButtonRef?: any,
children: any,
isDrawerOpen: boolean,
showSelectors?: boolean,
commentSelectorsProps?: any,
tipModalOpen?: boolean,
toggleDrawer: () => void,
closeSelector?: () => void,
};
export const TextareaWrapper = (wrapperProps: TextareaWrapperProps) => {
const {
children,
slimInput,
slimInputButtonRef,
isDrawerOpen,
commentSelectorsProps,
showSelectors,
tipModalOpen,
toggleDrawer,
closeSelector,
} = wrapperProps;
function handleCloseAll() {
toggleDrawer();
if (closeSelector) closeSelector();
}
return slimInput ? (
!isDrawerOpen ? (
<div ref={slimInputButtonRef} role="button" onClick={toggleDrawer}>
{children}
</div>
) : (
<Drawer
className="comment-create--drawer"
anchor="bottom"
open
onClose={handleCloseAll}
// The Modal tries to enforce focus when open and doesn't allow clicking or changing any
// other input boxes, so in this case it is disabled when trying to type in a custom tip
ModalProps={{ disableEnforceFocus: tipModalOpen }}
>
{children}
{showSelectors && <CommentSelectors closeSelector={closeSelector} {...commentSelectorsProps} />}
</Drawer>
)
) : (
children
);
};