feat: Configure included editor components per field, add optional minimal height (#3299)
This commit is contained in:
parent
93bd0529f1
commit
b7b4bcb609
@ -33,7 +33,7 @@ collections: # A list of collections the CMS should be able to edit
|
|||||||
required: false
|
required: false
|
||||||
tagname: ''
|
tagname: ''
|
||||||
|
|
||||||
- { label: 'Body', name: 'body', widget: 'markdown', hint: 'Main content goes here.' }
|
- { editorComponents: ['youtube'], label: 'Body', name: 'body', widget: 'markdown', hint: 'Main content goes here.' }
|
||||||
meta:
|
meta:
|
||||||
- { label: 'SEO Description', name: 'description', widget: 'text' }
|
- { label: 'SEO Description', name: 'description', widget: 'text' }
|
||||||
|
|
||||||
|
@ -13,19 +13,17 @@ import { markdownToHtml } from '../serializers';
|
|||||||
import { editorStyleVars, EditorControlBar } from '../styles';
|
import { editorStyleVars, EditorControlBar } from '../styles';
|
||||||
import Toolbar from './Toolbar';
|
import Toolbar from './Toolbar';
|
||||||
|
|
||||||
const styleStrings = {
|
const rawEditorStyles = ({ minimal }) => `
|
||||||
slateRaw: `
|
position: relative;
|
||||||
position: relative;
|
overflow: hidden;
|
||||||
overflow: hidden;
|
overflow-x: auto;
|
||||||
overflow-x: auto;
|
min-height: ${minimal ? 'auto' : lengths.richTextEditorMinHeight};
|
||||||
min-height: ${lengths.richTextEditorMinHeight};
|
font-family: ${fonts.mono};
|
||||||
font-family: ${fonts.mono};
|
border-top-left-radius: 0;
|
||||||
border-top-left-radius: 0;
|
border-top-right-radius: 0;
|
||||||
border-top-right-radius: 0;
|
border-top: 0;
|
||||||
border-top: 0;
|
margin-top: -${editorStyleVars.stickyDistanceBottom};
|
||||||
margin-top: -${editorStyleVars.stickyDistanceBottom};
|
`;
|
||||||
`,
|
|
||||||
};
|
|
||||||
|
|
||||||
const RawEditorContainer = styled.div`
|
const RawEditorContainer = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -118,7 +116,7 @@ export default class RawEditor extends React.Component {
|
|||||||
className={cx(
|
className={cx(
|
||||||
className,
|
className,
|
||||||
css`
|
css`
|
||||||
${styleStrings.slateRaw}
|
${rawEditorStyles({ minimal: field.get('minimal') })}
|
||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
value={this.state.value}
|
value={this.state.value}
|
||||||
|
@ -70,6 +70,7 @@ const headingOptions = {
|
|||||||
export default class Toolbar extends React.Component {
|
export default class Toolbar extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
buttons: ImmutablePropTypes.list,
|
buttons: ImmutablePropTypes.list,
|
||||||
|
editorComponents: ImmutablePropTypes.list,
|
||||||
onToggleMode: PropTypes.func.isRequired,
|
onToggleMode: PropTypes.func.isRequired,
|
||||||
rawMode: PropTypes.bool,
|
rawMode: PropTypes.bool,
|
||||||
plugins: ImmutablePropTypes.map,
|
plugins: ImmutablePropTypes.map,
|
||||||
@ -86,9 +87,9 @@ export default class Toolbar extends React.Component {
|
|||||||
t: PropTypes.func.isRequired,
|
t: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
isHidden = button => {
|
isVisible = button => {
|
||||||
const { buttons } = this.props;
|
const { buttons } = this.props;
|
||||||
return List.isList(buttons) ? !buttons.includes(button) : false;
|
return !List.isList(buttons) || buttons.includes(button);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleBlockClick = (event, type) => {
|
handleBlockClick = (event, type) => {
|
||||||
@ -114,52 +115,59 @@ export default class Toolbar extends React.Component {
|
|||||||
hasMark = () => {},
|
hasMark = () => {},
|
||||||
hasInline = () => {},
|
hasInline = () => {},
|
||||||
hasBlock = () => {},
|
hasBlock = () => {},
|
||||||
|
editorComponents,
|
||||||
t,
|
t,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
const isVisible = this.isVisible;
|
||||||
|
const showEditorComponents = !editorComponents || editorComponents.size >= 1;
|
||||||
|
const showPlugin = ({ id }) => (editorComponents ? editorComponents.includes(id) : true);
|
||||||
|
const pluginsList = plugins ? plugins.toList().filter(showPlugin) : List();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolbarContainer>
|
<ToolbarContainer>
|
||||||
<div>
|
<div>
|
||||||
<ToolbarButton
|
{isVisible('bold') && (
|
||||||
type="bold"
|
<ToolbarButton
|
||||||
label="Bold"
|
type="bold"
|
||||||
icon="bold"
|
label="Bold"
|
||||||
onClick={this.handleMarkClick}
|
icon="bold"
|
||||||
isActive={hasMark('bold')}
|
onClick={this.handleMarkClick}
|
||||||
isHidden={this.isHidden('bold')}
|
isActive={hasMark('bold')}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<ToolbarButton
|
)}
|
||||||
type="italic"
|
{isVisible('italic') && (
|
||||||
label="Italic"
|
<ToolbarButton
|
||||||
icon="italic"
|
type="italic"
|
||||||
onClick={this.handleMarkClick}
|
label="Italic"
|
||||||
isActive={hasMark('italic')}
|
icon="italic"
|
||||||
isHidden={this.isHidden('italic')}
|
onClick={this.handleMarkClick}
|
||||||
disabled={disabled}
|
isActive={hasMark('italic')}
|
||||||
/>
|
disabled={disabled}
|
||||||
<ToolbarButton
|
/>
|
||||||
type="code"
|
)}
|
||||||
label="Code"
|
{isVisible('code') && (
|
||||||
icon="code"
|
<ToolbarButton
|
||||||
onClick={this.handleMarkClick}
|
type="code"
|
||||||
isActive={hasMark('code')}
|
label="Code"
|
||||||
isHidden={this.isHidden('code')}
|
icon="code"
|
||||||
disabled={disabled}
|
onClick={this.handleMarkClick}
|
||||||
/>
|
isActive={hasMark('code')}
|
||||||
<ToolbarButton
|
disabled={disabled}
|
||||||
type="link"
|
/>
|
||||||
label="Link"
|
)}
|
||||||
icon="link"
|
{isVisible('link') && (
|
||||||
onClick={onLinkClick}
|
<ToolbarButton
|
||||||
isActive={hasInline('link')}
|
type="link"
|
||||||
isHidden={this.isHidden('link')}
|
label="Link"
|
||||||
disabled={disabled}
|
icon="link"
|
||||||
/>
|
onClick={onLinkClick}
|
||||||
|
isActive={hasInline('link')}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{/* Show dropdown if at least one heading is not hidden */}
|
{/* Show dropdown if at least one heading is not hidden */}
|
||||||
{Object.keys(headingOptions).some(optionKey => {
|
{Object.keys(headingOptions).some(isVisible) && (
|
||||||
return !this.isHidden(optionKey);
|
|
||||||
}) && (
|
|
||||||
<ToolbarDropdownWrapper>
|
<ToolbarDropdownWrapper>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
dropdownTopOverlap="36px"
|
dropdownTopOverlap="36px"
|
||||||
@ -170,12 +178,7 @@ export default class Toolbar extends React.Component {
|
|||||||
label="Headings"
|
label="Headings"
|
||||||
icon="hOptions"
|
icon="hOptions"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
isActive={
|
isActive={!disabled && Object.keys(headingOptions).some(hasBlock)}
|
||||||
!disabled &&
|
|
||||||
Object.keys(headingOptions).some(optionKey => {
|
|
||||||
return hasBlock(optionKey);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</DropdownButton>
|
</DropdownButton>
|
||||||
)}
|
)}
|
||||||
@ -183,7 +186,7 @@ export default class Toolbar extends React.Component {
|
|||||||
{!disabled &&
|
{!disabled &&
|
||||||
Object.keys(headingOptions).map(
|
Object.keys(headingOptions).map(
|
||||||
(optionKey, idx) =>
|
(optionKey, idx) =>
|
||||||
!this.isHidden(optionKey) && (
|
isVisible(optionKey) && (
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key={idx}
|
key={idx}
|
||||||
label={headingOptions[optionKey]}
|
label={headingOptions[optionKey]}
|
||||||
@ -195,56 +198,58 @@ export default class Toolbar extends React.Component {
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
</ToolbarDropdownWrapper>
|
</ToolbarDropdownWrapper>
|
||||||
)}
|
)}
|
||||||
<ToolbarButton
|
{isVisible('quote') && (
|
||||||
type="quote"
|
<ToolbarButton
|
||||||
label="Quote"
|
type="quote"
|
||||||
icon="quote"
|
label="Quote"
|
||||||
onClick={this.handleBlockClick}
|
icon="quote"
|
||||||
isActive={hasBlock('quote')}
|
onClick={this.handleBlockClick}
|
||||||
isHidden={this.isHidden('quote')}
|
isActive={hasBlock('quote')}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<ToolbarButton
|
)}
|
||||||
type="bulleted-list"
|
{isVisible('bulleted-list') && (
|
||||||
label="Bulleted List"
|
<ToolbarButton
|
||||||
icon="list-bulleted"
|
type="bulleted-list"
|
||||||
onClick={this.handleBlockClick}
|
label="Bulleted List"
|
||||||
isActive={hasBlock('bulleted-list')}
|
icon="list-bulleted"
|
||||||
isHidden={this.isHidden('bulleted-list')}
|
onClick={this.handleBlockClick}
|
||||||
disabled={disabled}
|
isActive={hasBlock('bulleted-list')}
|
||||||
/>
|
disabled={disabled}
|
||||||
<ToolbarButton
|
/>
|
||||||
type="numbered-list"
|
)}
|
||||||
label="Numbered List"
|
{isVisible('numbered-list') && (
|
||||||
icon="list-numbered"
|
<ToolbarButton
|
||||||
onClick={this.handleBlockClick}
|
type="numbered-list"
|
||||||
isActive={hasBlock('numbered-list')}
|
label="Numbered List"
|
||||||
isHidden={this.isHidden('numbered-list')}
|
icon="list-numbered"
|
||||||
disabled={disabled}
|
onClick={this.handleBlockClick}
|
||||||
/>
|
isActive={hasBlock('numbered-list')}
|
||||||
<ToolbarDropdownWrapper>
|
disabled={disabled}
|
||||||
<Dropdown
|
/>
|
||||||
dropdownTopOverlap="36px"
|
)}
|
||||||
dropdownWidth="110px"
|
{showEditorComponents && (
|
||||||
renderButton={() => (
|
<ToolbarDropdownWrapper>
|
||||||
<DropdownButton>
|
<Dropdown
|
||||||
<ToolbarButton
|
dropdownTopOverlap="36px"
|
||||||
label="Add Component"
|
dropdownWidth="110px"
|
||||||
icon="add-with"
|
renderButton={() => (
|
||||||
onClick={this.handleComponentsMenuToggle}
|
<DropdownButton>
|
||||||
disabled={disabled}
|
<ToolbarButton
|
||||||
/>
|
label="Add Component"
|
||||||
</DropdownButton>
|
icon="add-with"
|
||||||
)}
|
onClick={this.handleComponentsMenuToggle}
|
||||||
>
|
disabled={disabled}
|
||||||
{plugins &&
|
/>
|
||||||
plugins
|
</DropdownButton>
|
||||||
.toList()
|
)}
|
||||||
.map((plugin, idx) => (
|
>
|
||||||
<DropdownItem key={idx} label={plugin.label} onClick={() => onSubmit(plugin)} />
|
{pluginsList.map((plugin, idx) => (
|
||||||
))}
|
<DropdownItem key={idx} label={plugin.label} onClick={() => onSubmit(plugin)} />
|
||||||
</Dropdown>
|
))}
|
||||||
</ToolbarDropdownWrapper>
|
</Dropdown>
|
||||||
|
</ToolbarDropdownWrapper>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ToolbarToggle>
|
<ToolbarToggle>
|
||||||
<ToolbarToggleLabel isActive={!rawMode} offPosition>
|
<ToolbarToggleLabel isActive={!rawMode} offPosition>
|
||||||
|
@ -23,22 +23,16 @@ const StyledToolbarButton = styled.button`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ToolbarButton = ({ type, label, icon, onClick, isActive, isHidden, disabled }) => {
|
const ToolbarButton = ({ type, label, icon, onClick, isActive, disabled }) => (
|
||||||
if (isHidden) {
|
<StyledToolbarButton
|
||||||
return null;
|
isActive={isActive}
|
||||||
}
|
onClick={e => onClick && onClick(e, type)}
|
||||||
|
title={label}
|
||||||
return (
|
disabled={disabled}
|
||||||
<StyledToolbarButton
|
>
|
||||||
isActive={isActive}
|
{icon ? <Icon type={icon} /> : label}
|
||||||
onClick={e => onClick && onClick(e, type)}
|
</StyledToolbarButton>
|
||||||
title={label}
|
);
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{icon ? <Icon type={icon} /> : label}
|
|
||||||
</StyledToolbarButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ToolbarButton.propTypes = {
|
ToolbarButton.propTypes = {
|
||||||
type: PropTypes.string,
|
type: PropTypes.string,
|
||||||
@ -46,7 +40,6 @@ ToolbarButton.propTypes = {
|
|||||||
icon: PropTypes.string,
|
icon: PropTypes.string,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
isActive: PropTypes.bool,
|
isActive: PropTypes.bool,
|
||||||
isHidden: PropTypes.bool,
|
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -15,12 +15,12 @@ import { renderBlock, renderInline, renderMark } from './renderers';
|
|||||||
import plugins from './plugins/visual';
|
import plugins from './plugins/visual';
|
||||||
import schema from './schema';
|
import schema from './schema';
|
||||||
|
|
||||||
const visualEditorStyles = `
|
const visualEditorStyles = ({ minimal }) => `
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
font-family: ${fonts.primary};
|
font-family: ${fonts.primary};
|
||||||
min-height: ${lengths.richTextEditorMinHeight};
|
min-height: ${minimal ? 'auto' : lengths.richTextEditorMinHeight};
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
border-top: 0;
|
border-top: 0;
|
||||||
@ -195,6 +195,7 @@ export default class Editor extends React.Component {
|
|||||||
onAddAsset={onAddAsset}
|
onAddAsset={onAddAsset}
|
||||||
getAsset={getAsset}
|
getAsset={getAsset}
|
||||||
buttons={field.get('buttons')}
|
buttons={field.get('buttons')}
|
||||||
|
editorComponents={field.get('editorComponents')}
|
||||||
hasMark={this.hasMark}
|
hasMark={this.hasMark}
|
||||||
hasInline={this.hasInline}
|
hasInline={this.hasInline}
|
||||||
hasBlock={this.hasBlock}
|
hasBlock={this.hasBlock}
|
||||||
@ -207,7 +208,7 @@ export default class Editor extends React.Component {
|
|||||||
className={cx(
|
className={cx(
|
||||||
className,
|
className,
|
||||||
css`
|
css`
|
||||||
${visualEditorStyles}
|
${visualEditorStyles({ minimal: field.get('minimal') })}
|
||||||
`,
|
`,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -12,7 +12,9 @@ _Please note:_ If you want to use your markdown editor to fill a markdown file c
|
|||||||
- **Data type:** markdown
|
- **Data type:** markdown
|
||||||
- **Options:**
|
- **Options:**
|
||||||
- `default`: accepts markdown content
|
- `default`: accepts markdown content
|
||||||
- `buttons`: an array of strings representing the formatting buttons to display, all buttons shown by default. Buttons include: `bold`, `italic`, `code`, `link`, `heading-one`, `heading-two`, `heading-three`, `heading-four`, `heading-five`, `heading-six`, `quote`, `code-block`, `bulleted-list`, and `numbered-list`.
|
- `minimal`: accepts a boolean value, `false` by default. Sets the widget height to minimum possible.
|
||||||
|
- `buttons`: an array of strings representing the formatting buttons to display (all shown by default). Buttons include: `bold`, `italic`, `code`, `link`, `heading-one`, `heading-two`, `heading-three`, `heading-four`, `heading-five`, `heading-six`, `quote`, `bulleted-list`, and `numbered-list`.
|
||||||
|
- `editorComponents`: an array of strings representing the names of editor components to display (all shown by default). The `image` and `code-block` editor components are included with Netlify CMS by default, but others may be [created and registered](/docs/custom-widgets/#registereditorcomponent).
|
||||||
- **Example:**
|
- **Example:**
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
Loading…
x
Reference in New Issue
Block a user