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:
@ -67,6 +67,16 @@ type KeyData = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (!this.props.readOnly) {
|
||||||
|
if (this.props.text) {
|
||||||
|
this.focusAtEnd();
|
||||||
|
} else {
|
||||||
|
this.focusAtStart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
componentDidUpdate(prevProps: Props) {
|
||||||
if (prevProps.readOnly && !this.props.readOnly) {
|
if (prevProps.readOnly && !this.props.readOnly) {
|
||||||
this.focusAtEnd();
|
this.focusAtEnd();
|
||||||
@ -121,6 +131,8 @@ type KeyData = {
|
|||||||
// Handling of keyboard shortcuts outside of editor focus
|
// Handling of keyboard shortcuts outside of editor focus
|
||||||
@keydown('meta+s')
|
@keydown('meta+s')
|
||||||
onSave(ev: SyntheticKeyboardEvent) {
|
onSave(ev: SyntheticKeyboardEvent) {
|
||||||
|
if (this.props.readOnly) return;
|
||||||
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this.props.onSave();
|
this.props.onSave();
|
||||||
@ -128,6 +140,8 @@ type KeyData = {
|
|||||||
|
|
||||||
@keydown('meta+enter')
|
@keydown('meta+enter')
|
||||||
onSaveAndExit(ev: SyntheticKeyboardEvent) {
|
onSaveAndExit(ev: SyntheticKeyboardEvent) {
|
||||||
|
if (this.props.readOnly) return;
|
||||||
|
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this.props.onSave({ redirect: false });
|
this.props.onSave({ redirect: false });
|
||||||
@ -135,6 +149,7 @@ type KeyData = {
|
|||||||
|
|
||||||
@keydown('esc')
|
@keydown('esc')
|
||||||
onCancel() {
|
onCancel() {
|
||||||
|
if (this.props.readOnly) return;
|
||||||
this.props.onCancel();
|
this.props.onCancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,7 +208,8 @@ type KeyData = {
|
|||||||
<Editor
|
<Editor
|
||||||
key={this.props.starred}
|
key={this.props.starred}
|
||||||
ref={ref => (this.editor = ref)}
|
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 })}
|
className={cx(styles.editor, { readOnly: this.props.readOnly })}
|
||||||
schema={this.schema}
|
schema={this.schema}
|
||||||
plugins={this.plugins}
|
plugins={this.plugins}
|
||||||
|
@ -30,6 +30,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1:first-of-type {
|
||||||
|
.placeholder {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p:first-of-type {
|
||||||
|
.placeholder {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ul,
|
ul,
|
||||||
ol {
|
ol {
|
||||||
margin: 1em .1em;
|
margin: 1em .1em;
|
||||||
@ -41,6 +53,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
li p {
|
li p {
|
||||||
display: inline;
|
display: inline;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -113,8 +129,10 @@
|
|||||||
.placeholder {
|
.placeholder {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
visibility: hidden;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
color: #ddd;
|
user-select: none;
|
||||||
|
color: #B1BECC;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (max-width: 2000px) and (min-width: 960px) {
|
@media all and (max-width: 2000px) and (min-width: 960px) {
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
import { Document } from 'slate';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import slug from 'slug';
|
import slug from 'slug';
|
||||||
import StarIcon from 'components/Icon/StarIcon';
|
import StarIcon from 'components/Icon/StarIcon';
|
||||||
@ -41,8 +42,8 @@ const StyledStar = styled(StarIcon)`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function Heading(
|
function Heading(props: Props, { starred }: Context) {
|
||||||
{
|
const {
|
||||||
parent,
|
parent,
|
||||||
placeholder,
|
placeholder,
|
||||||
node,
|
node,
|
||||||
@ -52,10 +53,9 @@ function Heading(
|
|||||||
readOnly,
|
readOnly,
|
||||||
children,
|
children,
|
||||||
component = 'h1',
|
component = 'h1',
|
||||||
}: Props,
|
} = props;
|
||||||
{ starred }: Context
|
const parentIsDocument = parent instanceof Document;
|
||||||
) {
|
const firstHeading = parentIsDocument && parent.nodes.first() === node;
|
||||||
const firstHeading = parent.nodes.first() === node;
|
|
||||||
const showPlaceholder = placeholder && firstHeading && !node.text;
|
const showPlaceholder = placeholder && firstHeading && !node.text;
|
||||||
const slugish = _.escape(`${component}-${slug(node.text)}`);
|
const slugish = _.escape(`${component}-${slug(node.text)}`);
|
||||||
const showStar = readOnly && !!onStar;
|
const showStar = readOnly && !!onStar;
|
||||||
@ -66,7 +66,7 @@ function Heading(
|
|||||||
<Component className={styles.title}>
|
<Component className={styles.title}>
|
||||||
{children}
|
{children}
|
||||||
{showPlaceholder &&
|
{showPlaceholder &&
|
||||||
<span className={styles.placeholder}>
|
<span className={styles.placeholder} contentEditable={false}>
|
||||||
{editor.props.placeholder}
|
{editor.props.placeholder}
|
||||||
</span>}
|
</span>}
|
||||||
{showHash &&
|
{showHash &&
|
||||||
|
29
frontend/components/Editor/components/Paragraph.js
Normal file
29
frontend/components/Editor/components/Paragraph.js
Normal file
@ -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);
|
return this.onDash(ev, state);
|
||||||
case '`':
|
case '`':
|
||||||
return this.onBacktick(ev, state);
|
return this.onBacktick(ev, state);
|
||||||
|
case 'tab':
|
||||||
|
return this.onTab(ev, state);
|
||||||
case 'space':
|
case 'space':
|
||||||
return this.onSpace(ev, state);
|
return this.onSpace(ev, state);
|
||||||
case 'backspace':
|
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,
|
* On return, if at the end of a node type that should not be extended,
|
||||||
* create a new paragraph below it.
|
* create a new paragraph below it.
|
||||||
|
@ -5,6 +5,7 @@ import Image from './components/Image';
|
|||||||
import Link from './components/Link';
|
import Link from './components/Link';
|
||||||
import ListItem from './components/ListItem';
|
import ListItem from './components/ListItem';
|
||||||
import Heading from './components/Heading';
|
import Heading from './components/Heading';
|
||||||
|
import Paragraph from './components/Paragraph';
|
||||||
import type { Props, Node, Transform } from './types';
|
import type { Props, Node, Transform } from './types';
|
||||||
import styles from './Editor.scss';
|
import styles from './Editor.scss';
|
||||||
|
|
||||||
@ -25,7 +26,7 @@ const createSchema = ({ onStar, onUnstar }: Options) => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
nodes: {
|
nodes: {
|
||||||
paragraph: (props: Props) => <p>{props.children}</p>,
|
paragraph: (props: Props) => <Paragraph {...props} />,
|
||||||
'block-quote': (props: Props) => (
|
'block-quote': (props: Props) => (
|
||||||
<blockquote>{props.children}</blockquote>
|
<blockquote>{props.children}</blockquote>
|
||||||
),
|
),
|
||||||
|
@ -167,7 +167,7 @@ type Props = {
|
|||||||
const isNew = this.props.newDocument;
|
const isNew = this.props.newDocument;
|
||||||
const isEditing = !!this.props.match.params.edit || isNew;
|
const isEditing = !!this.props.match.params.edit || isNew;
|
||||||
const isFetching = !this.document;
|
const isFetching = !this.document;
|
||||||
const titleText = get(this.document, 'title', 'Loading');
|
const titleText = get(this.document, 'title', '');
|
||||||
const document = this.document;
|
const document = this.document;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
Reference in New Issue
Block a user