Various editor fixes (#160)
* Tab should move to body from heading * Focus editor on mount, closes #156 * Closes #154 - Just went for the simple approach here, considered something more complex but this is clear * Added body placeholder
This commit is contained in:
parent
e0d5d1bbbe
commit
b6616cd05a
|
@ -67,6 +67,16 @@ type KeyData = {
|
|||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.readOnly) {
|
||||
if (this.props.text) {
|
||||
this.focusAtEnd();
|
||||
} else {
|
||||
this.focusAtStart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps.readOnly && !this.props.readOnly) {
|
||||
this.focusAtEnd();
|
||||
|
@ -121,6 +131,8 @@ type KeyData = {
|
|||
// Handling of keyboard shortcuts outside of editor focus
|
||||
@keydown('meta+s')
|
||||
onSave(ev: SyntheticKeyboardEvent) {
|
||||
if (this.props.readOnly) return;
|
||||
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.onSave();
|
||||
|
@ -128,6 +140,8 @@ type KeyData = {
|
|||
|
||||
@keydown('meta+enter')
|
||||
onSaveAndExit(ev: SyntheticKeyboardEvent) {
|
||||
if (this.props.readOnly) return;
|
||||
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.onSave({ redirect: false });
|
||||
|
@ -135,6 +149,7 @@ type KeyData = {
|
|||
|
||||
@keydown('esc')
|
||||
onCancel() {
|
||||
if (this.props.readOnly) return;
|
||||
this.props.onCancel();
|
||||
}
|
||||
|
||||
|
@ -193,7 +208,8 @@ type KeyData = {
|
|||
<Editor
|
||||
key={this.props.starred}
|
||||
ref={ref => (this.editor = ref)}
|
||||
placeholder="Start with a title..."
|
||||
placeholder="Start with a title…"
|
||||
bodyPlaceholder="Insert witty platitude here"
|
||||
className={cx(styles.editor, { readOnly: this.props.readOnly })}
|
||||
schema={this.schema}
|
||||
plugins={this.plugins}
|
||||
|
|
|
@ -30,6 +30,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
h1:first-of-type {
|
||||
.placeholder {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
p:first-of-type {
|
||||
.placeholder {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 1em .1em;
|
||||
|
@ -41,6 +53,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
p {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
li p {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
|
@ -113,8 +129,10 @@
|
|||
.placeholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
color: #ddd;
|
||||
user-select: none;
|
||||
color: #B1BECC;
|
||||
}
|
||||
|
||||
@media all and (max-width: 2000px) and (min-width: 960px) {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { Document } from 'slate';
|
||||
import _ from 'lodash';
|
||||
import slug from 'slug';
|
||||
import StarIcon from 'components/Icon/StarIcon';
|
||||
|
@ -41,8 +42,8 @@ const StyledStar = styled(StarIcon)`
|
|||
}
|
||||
`;
|
||||
|
||||
function Heading(
|
||||
{
|
||||
function Heading(props: Props, { starred }: Context) {
|
||||
const {
|
||||
parent,
|
||||
placeholder,
|
||||
node,
|
||||
|
@ -52,10 +53,9 @@ function Heading(
|
|||
readOnly,
|
||||
children,
|
||||
component = 'h1',
|
||||
}: Props,
|
||||
{ starred }: Context
|
||||
) {
|
||||
const firstHeading = parent.nodes.first() === node;
|
||||
} = props;
|
||||
const parentIsDocument = parent instanceof Document;
|
||||
const firstHeading = parentIsDocument && parent.nodes.first() === node;
|
||||
const showPlaceholder = placeholder && firstHeading && !node.text;
|
||||
const slugish = _.escape(`${component}-${slug(node.text)}`);
|
||||
const showStar = readOnly && !!onStar;
|
||||
|
@ -66,7 +66,7 @@ function Heading(
|
|||
<Component className={styles.title}>
|
||||
{children}
|
||||
{showPlaceholder &&
|
||||
<span className={styles.placeholder}>
|
||||
<span className={styles.placeholder} contentEditable={false}>
|
||||
{editor.props.placeholder}
|
||||
</span>}
|
||||
{showHash &&
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { Document } from 'slate';
|
||||
import type { Props } from '../types';
|
||||
import styles from '../Editor.scss';
|
||||
|
||||
export default function Link({
|
||||
attributes,
|
||||
editor,
|
||||
node,
|
||||
parent,
|
||||
children,
|
||||
}: Props) {
|
||||
const parentIsDocument = parent instanceof Document;
|
||||
const firstParagraph = parent && parent.nodes.get(1) === node;
|
||||
const lastParagraph = parent && parent.nodes.last() === node;
|
||||
const showPlaceholder =
|
||||
parentIsDocument && firstParagraph && lastParagraph && !node.text;
|
||||
|
||||
return (
|
||||
<p>
|
||||
{children}
|
||||
{showPlaceholder &&
|
||||
<span className={styles.placeholder} contentEditable={false}>
|
||||
{editor.props.bodyPlaceholder}
|
||||
</span>}
|
||||
</p>
|
||||
);
|
||||
}
|
|
@ -20,6 +20,8 @@ export default function MarkdownShortcuts() {
|
|||
return this.onDash(ev, state);
|
||||
case '`':
|
||||
return this.onBacktick(ev, state);
|
||||
case 'tab':
|
||||
return this.onTab(ev, state);
|
||||
case 'space':
|
||||
return this.onSpace(ev, state);
|
||||
case 'backspace':
|
||||
|
@ -184,6 +186,17 @@ export default function MarkdownShortcuts() {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* On tab, if at the end of the heading jump to the main body content
|
||||
* as if it is another input field (act the same as enter).
|
||||
*/
|
||||
onTab(ev: SyntheticEvent, state: Object) {
|
||||
if (state.startBlock.type === 'heading1') {
|
||||
ev.preventDefault();
|
||||
return state.transform().splitBlock().setBlock('paragraph').apply();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* On return, if at the end of a node type that should not be extended,
|
||||
* create a new paragraph below it.
|
||||
|
|
|
@ -5,6 +5,7 @@ import Image from './components/Image';
|
|||
import Link from './components/Link';
|
||||
import ListItem from './components/ListItem';
|
||||
import Heading from './components/Heading';
|
||||
import Paragraph from './components/Paragraph';
|
||||
import type { Props, Node, Transform } from './types';
|
||||
import styles from './Editor.scss';
|
||||
|
||||
|
@ -25,7 +26,7 @@ const createSchema = ({ onStar, onUnstar }: Options) => {
|
|||
},
|
||||
|
||||
nodes: {
|
||||
paragraph: (props: Props) => <p>{props.children}</p>,
|
||||
paragraph: (props: Props) => <Paragraph {...props} />,
|
||||
'block-quote': (props: Props) => (
|
||||
<blockquote>{props.children}</blockquote>
|
||||
),
|
||||
|
|
|
@ -167,7 +167,7 @@ type Props = {
|
|||
const isNew = this.props.newDocument;
|
||||
const isEditing = !!this.props.match.params.edit || isNew;
|
||||
const isFetching = !this.document;
|
||||
const titleText = get(this.document, 'title', 'Loading');
|
||||
const titleText = get(this.document, 'title', '');
|
||||
const document = this.document;
|
||||
|
||||
return (
|
||||
|
|
Reference in New Issue