mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-05-11 16:53:10 +00:00
Refactor Draftail sources to single component
This commit is contained in:
parent
19a6189d57
commit
f8b99045a7
11 changed files with 207 additions and 307 deletions
|
|
@ -11,10 +11,7 @@ import Document from './decorators/Document';
|
|||
import ImageBlock from './blocks/ImageBlock';
|
||||
import EmbedBlock from './blocks/EmbedBlock';
|
||||
|
||||
import LinkSource from './sources/LinkSource';
|
||||
import DocumentSource from './sources/DocumentSource';
|
||||
import ImageSource from './sources/ImageSource';
|
||||
import EmbedSource from './sources/EmbedSource';
|
||||
import ModalWorkflowSource from './sources/ModalWorkflowSource';
|
||||
|
||||
import registry from './registry';
|
||||
|
||||
|
|
@ -90,10 +87,7 @@ export const initEditor = (fieldName, options = {}) => {
|
|||
};
|
||||
|
||||
registry.registerSources({
|
||||
LinkSource,
|
||||
DocumentSource,
|
||||
ImageSource,
|
||||
EmbedSource,
|
||||
ModalWorkflowSource,
|
||||
});
|
||||
registry.registerDecorators({
|
||||
Link,
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
import ModalSource from './ModalSource';
|
||||
|
||||
import { STRINGS } from '../../../config/wagtailConfig';
|
||||
|
||||
const $ = global.jQuery;
|
||||
|
||||
class DocumentSource extends ModalSource {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.parseData = this.parseData.bind(this);
|
||||
}
|
||||
|
||||
parseData(data) {
|
||||
this.onConfirm({
|
||||
id: data.id,
|
||||
url: data.url,
|
||||
}, data.title);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { onClose } = this.props;
|
||||
const documentChooser = global.chooserUrls.documentChooser;
|
||||
const url = documentChooser;
|
||||
|
||||
$(document.body).on('hidden.bs.modal', this.onClose);
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
window.ModalWorkflow({
|
||||
url,
|
||||
responses: {
|
||||
documentChosen: this.parseData,
|
||||
},
|
||||
onError: () => {
|
||||
// eslint-disable-next-line no-alert
|
||||
window.alert(STRINGS.SERVER_ERROR);
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default DocumentSource;
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import ModalSource from './ModalSource';
|
||||
|
||||
import { STRINGS } from '../../../config/wagtailConfig';
|
||||
|
||||
const $ = global.jQuery;
|
||||
|
||||
class EmbedSource extends ModalSource {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.parseData = this.parseData.bind(this);
|
||||
}
|
||||
|
||||
parseData(html, embed) {
|
||||
this.onConfirmAtomicBlock({
|
||||
embedType: embed.embedType,
|
||||
url: embed.url,
|
||||
providerName: embed.providerName,
|
||||
authorName: embed.authorName,
|
||||
thumbnail: embed.thumbnail,
|
||||
title: embed.title,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { onClose } = this.props;
|
||||
|
||||
$(document.body).on('hidden.bs.modal', this.onClose);
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
window.ModalWorkflow({
|
||||
url: global.chooserUrls.embedsChooser,
|
||||
responses: {
|
||||
embedChosen: this.parseData,
|
||||
},
|
||||
onError: () => {
|
||||
// eslint-disable-next-line no-alert
|
||||
window.alert(STRINGS.SERVER_ERROR);
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default EmbedSource;
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import ModalSource from './ModalSource';
|
||||
|
||||
import { STRINGS } from '../../../config/wagtailConfig';
|
||||
|
||||
const $ = global.jQuery;
|
||||
|
||||
class ImageSource extends ModalSource {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.parseData = this.parseData.bind(this);
|
||||
}
|
||||
|
||||
parseData(imageData) {
|
||||
this.onConfirmAtomicBlock({
|
||||
id: imageData.id,
|
||||
src: imageData.preview.url,
|
||||
alt: imageData.alt,
|
||||
format: imageData.format,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { onClose } = this.props;
|
||||
|
||||
const imageChooser = global.chooserUrls.imageChooser;
|
||||
$(document.body).on('hidden.bs.modal', this.onClose);
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
window.ModalWorkflow({
|
||||
url: imageChooser + '?select_format=true',
|
||||
responses: {
|
||||
imageChosen: this.parseData,
|
||||
},
|
||||
onError: () => {
|
||||
// eslint-disable-next-line no-alert
|
||||
window.alert(STRINGS.SERVER_ERROR);
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default ImageSource;
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
import ModalSource from './ModalSource';
|
||||
|
||||
import { STRINGS } from '../../../config/wagtailConfig';
|
||||
|
||||
const $ = global.jQuery;
|
||||
|
||||
// Plaster over Wagtail internals.
|
||||
const buildInitialUrl = (entity, openAtParentId, canChooseRoot, pageTypes) => {
|
||||
// We can't destructure from the window object yet
|
||||
const pageChooser = global.chooserUrls.pageChooser;
|
||||
const emailLinkChooser = global.chooserUrls.emailLinkChooser;
|
||||
const externalLinkChooser = global.chooserUrls.externalLinkChooser;
|
||||
let url = pageChooser;
|
||||
|
||||
if (openAtParentId) {
|
||||
url = `${url}${openAtParentId}/`;
|
||||
}
|
||||
|
||||
const urlParams = {
|
||||
page_type: pageTypes.join(','),
|
||||
allow_external_link: true,
|
||||
allow_email_link: true,
|
||||
can_choose_root: canChooseRoot ? 'true' : 'false',
|
||||
// This does not initialise the modal with the currently selected text.
|
||||
// This will need to be implemented in the future.
|
||||
// See https://github.com/jpuri/draftjs-utils/blob/e81c0ae19c3b0fdef7e0c1b70d924398956be126/js/block.js#L106.
|
||||
link_text: '',
|
||||
};
|
||||
|
||||
if (entity) {
|
||||
const data = entity.getData();
|
||||
|
||||
// urlParams.link_text = data.title;
|
||||
|
||||
if (data.id) {
|
||||
url = ` ${pageChooser}${data.parentId}/`;
|
||||
} else if (data.url.startsWith('mailto:')) {
|
||||
url = emailLinkChooser;
|
||||
urlParams.link_url = data.url.replace('mailto:', '');
|
||||
} else {
|
||||
url = externalLinkChooser;
|
||||
urlParams.link_url = data.url;
|
||||
}
|
||||
}
|
||||
|
||||
return { url, urlParams };
|
||||
};
|
||||
|
||||
class LinkSource extends ModalSource {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.parseData = this.parseData.bind(this);
|
||||
}
|
||||
|
||||
parseData(data) {
|
||||
const parsedData = {
|
||||
url: data.url,
|
||||
};
|
||||
|
||||
if (data.id) {
|
||||
parsedData.id = data.id;
|
||||
parsedData.parentId = data.parentId;
|
||||
}
|
||||
|
||||
this.onConfirm(parsedData, data.title, data.prefer_this_title_as_link_text);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { entity, onClose } = this.props;
|
||||
const openAtParentId = false;
|
||||
const canChooseRoot = false;
|
||||
const pageTypes = ['wagtailcore.page'];
|
||||
const { url, urlParams } = buildInitialUrl(entity, openAtParentId, canChooseRoot, pageTypes);
|
||||
|
||||
$(document.body).on('hidden.bs.modal', this.onClose);
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
window.ModalWorkflow({
|
||||
url,
|
||||
urlParams,
|
||||
responses: {
|
||||
pageChosen: this.parseData,
|
||||
},
|
||||
onError: () => {
|
||||
// eslint-disable-next-line no-alert
|
||||
window.alert(STRINGS.SERVER_ERROR);
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default LinkSource;
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { AtomicBlockUtils, RichUtils, Modifier, EditorState } from 'draft-js';
|
||||
|
||||
const $ = global.jQuery;
|
||||
|
||||
class ModalSource extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.onConfirm = this.onConfirm.bind(this);
|
||||
this.onConfirmAtomicBlock = this.onConfirmAtomicBlock.bind(this);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
$(document.body).off('hidden.bs.modal', this.onClose);
|
||||
}
|
||||
|
||||
onConfirm(data, text = null, overrideText = false) {
|
||||
const { editorState, entityType, onComplete } = this.props;
|
||||
const contentState = editorState.getCurrentContent();
|
||||
const contentStateWithEntity = contentState.createEntity(entityType.type, 'MUTABLE', data);
|
||||
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
|
||||
const selection = editorState.getSelection();
|
||||
const shouldOverrideText = overrideText || selection.isCollapsed();
|
||||
let nextState;
|
||||
|
||||
if (shouldOverrideText) {
|
||||
const newContent = Modifier.replaceText(editorState.getCurrentContent(), selection, text, null, entityKey);
|
||||
nextState = EditorState.push(editorState, newContent, 'insert-characters');
|
||||
} else {
|
||||
nextState = RichUtils.toggleLink(editorState, selection, entityKey);
|
||||
}
|
||||
|
||||
onComplete(nextState);
|
||||
}
|
||||
|
||||
onConfirmAtomicBlock(data) {
|
||||
const { editorState, entityType, onComplete } = this.props;
|
||||
const contentState = editorState.getCurrentContent();
|
||||
const contentStateWithEntity = contentState.createEntity(entityType.type, 'IMMUTABLE', data);
|
||||
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
|
||||
const nextState = AtomicBlockUtils.insertAtomicBlock(editorState, entityKey, ' ');
|
||||
|
||||
onComplete(nextState);
|
||||
}
|
||||
|
||||
onClose(e) {
|
||||
const { onClose } = this.props;
|
||||
e.preventDefault();
|
||||
|
||||
onClose();
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
ModalSource.propTypes = {
|
||||
editorState: PropTypes.object.isRequired,
|
||||
entityType: PropTypes.object.isRequired,
|
||||
// eslint-disable-next-line
|
||||
entity: PropTypes.object,
|
||||
onComplete: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
ModalSource.defaultProps = {
|
||||
entity: null,
|
||||
};
|
||||
|
||||
export default ModalSource;
|
||||
201
client/src/components/Draftail/sources/ModalWorkflowSource.js
Normal file
201
client/src/components/Draftail/sources/ModalWorkflowSource.js
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { Component } from 'react';
|
||||
import { AtomicBlockUtils, Modifier, RichUtils, EditorState } from 'draft-js';
|
||||
import { ENTITY_TYPE } from 'draftail';
|
||||
|
||||
import { STRINGS } from '../../../config/wagtailConfig';
|
||||
|
||||
const $ = global.jQuery;
|
||||
|
||||
const EMBED = 'EMBED';
|
||||
const DOCUMENT = 'DOCUMENT';
|
||||
|
||||
const MUTABILITY = {};
|
||||
MUTABILITY[ENTITY_TYPE.LINK] = 'MUTABLE';
|
||||
MUTABILITY[DOCUMENT] = 'MUTABLE';
|
||||
MUTABILITY[ENTITY_TYPE.IMAGE] = 'IMMUTABLE';
|
||||
MUTABILITY[EMBED] = 'IMMUTABLE';
|
||||
|
||||
const getChooserConfig = (entityType, entity) => {
|
||||
const chooserURL = {};
|
||||
chooserURL[ENTITY_TYPE.IMAGE] = `${global.chooserUrls.imageChooser}?select_format=true`;
|
||||
chooserURL[EMBED] = global.chooserUrls.embedsChooser;
|
||||
chooserURL[ENTITY_TYPE.LINK] = global.chooserUrls.pageChooser;
|
||||
chooserURL[DOCUMENT] = global.chooserUrls.documentChooser;
|
||||
|
||||
let url = chooserURL[entityType.type];
|
||||
let urlParams = {};
|
||||
|
||||
if (entityType.type === ENTITY_TYPE.LINK) {
|
||||
urlParams = {
|
||||
page_type: 'wagtailcore.page',
|
||||
allow_external_link: true,
|
||||
allow_email_link: true,
|
||||
can_choose_root: 'false',
|
||||
// This does not initialise the modal with the currently selected text.
|
||||
// This will need to be implemented in the future.
|
||||
// See https://github.com/jpuri/draftjs-utils/blob/e81c0ae19c3b0fdef7e0c1b70d924398956be126/js/block.js#L106.
|
||||
link_text: '',
|
||||
};
|
||||
|
||||
if (entity) {
|
||||
const data = entity.getData();
|
||||
|
||||
if (data.id) {
|
||||
url = `${global.chooserUrls.pageChooser}${data.parentId}/`;
|
||||
} else if (data.url.startsWith('mailto:')) {
|
||||
url = global.chooserUrls.emailLinkChooser;
|
||||
urlParams.link_url = data.url.replace('mailto:', '');
|
||||
} else {
|
||||
url = global.chooserUrls.externalLinkChooser;
|
||||
urlParams.link_url = data.url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
urlParams,
|
||||
};
|
||||
};
|
||||
|
||||
const filterEntityData = (entityType, data) => {
|
||||
switch (entityType.type) {
|
||||
case ENTITY_TYPE.IMAGE:
|
||||
return {
|
||||
id: data.id,
|
||||
src: data.preview.url,
|
||||
alt: data.alt,
|
||||
format: data.format,
|
||||
};
|
||||
case EMBED:
|
||||
return {
|
||||
embedType: data.embedType,
|
||||
url: data.url,
|
||||
providerName: data.providerName,
|
||||
authorName: data.authorName,
|
||||
thumbnail: data.thumbnail,
|
||||
title: data.title,
|
||||
};
|
||||
case ENTITY_TYPE.LINK:
|
||||
if (data.id) {
|
||||
return {
|
||||
url: data.url,
|
||||
id: data.id,
|
||||
parentId: data.parentId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
url: data.url,
|
||||
};
|
||||
case DOCUMENT:
|
||||
return {
|
||||
url: data.url,
|
||||
id: data.id,
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Interfaces with Wagtail's ModalWorkflow to open the chooser,
|
||||
* and create new content in Draft.js based on the data.
|
||||
*/
|
||||
class ModalWorkflowSource extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChosen = this.onChosen.bind(this);
|
||||
this.onClose = this.onClose.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { onClose, entityType, entity } = this.props;
|
||||
const { url, urlParams } = getChooserConfig(entityType, entity);
|
||||
|
||||
$(document.body).on('hidden.bs.modal', this.onClose);
|
||||
|
||||
// eslint-disable-next-line new-cap
|
||||
global.ModalWorkflow({
|
||||
url,
|
||||
urlParams,
|
||||
responses: {
|
||||
imageChosen: this.onChosen,
|
||||
// Discard the first parameter (HTML) to only transmit the data.
|
||||
embedChosen: (_, data) => this.onChosen(data),
|
||||
documentChosen: this.onChosen,
|
||||
pageChosen: this.onChosen,
|
||||
},
|
||||
onError: () => {
|
||||
// eslint-disable-next-line no-alert
|
||||
window.alert(STRINGS.SERVER_ERROR);
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
$(document.body).off('hidden.bs.modal', this.onClose);
|
||||
}
|
||||
|
||||
onChosen(data) {
|
||||
const { editorState, entityType, onComplete } = this.props;
|
||||
const content = editorState.getCurrentContent();
|
||||
const selection = editorState.getSelection();
|
||||
|
||||
const entityData = filterEntityData(entityType, data);
|
||||
const mutability = MUTABILITY[entityType.type];
|
||||
const contentWithEntity = content.createEntity(entityType.type, mutability, entityData);
|
||||
const entityKey = contentWithEntity.getLastCreatedEntityKey();
|
||||
|
||||
let nextState;
|
||||
|
||||
if (entityType.block) {
|
||||
// Only supports adding entities at the moment, not editing existing ones.
|
||||
// See https://github.com/springload/draftail/blob/cdc8988fe2e3ac32374317f535a5338ab97e8637/examples/sources/ImageSource.js#L44-L62.
|
||||
// See https://github.com/springload/draftail/blob/cdc8988fe2e3ac32374317f535a5338ab97e8637/examples/sources/EmbedSource.js#L64-L91
|
||||
nextState = AtomicBlockUtils.insertAtomicBlock(editorState, entityKey, ' ');
|
||||
} else {
|
||||
// Replace text if the chooser demands it, or if there is no selected text in the first place.
|
||||
const shouldReplaceText = data.prefer_this_title_as_link_text || selection.isCollapsed();
|
||||
|
||||
if (shouldReplaceText) {
|
||||
// If there is a title attribute, use it. Otherwise we inject the URL.
|
||||
const newText = data.title || data.url;
|
||||
const newContent = Modifier.replaceText(content, selection, newText, null, entityKey);
|
||||
nextState = EditorState.push(editorState, newContent, 'insert-characters');
|
||||
} else {
|
||||
nextState = RichUtils.toggleLink(editorState, selection, entityKey);
|
||||
}
|
||||
}
|
||||
|
||||
onComplete(nextState);
|
||||
}
|
||||
|
||||
onClose(e) {
|
||||
const { onClose } = this.props;
|
||||
e.preventDefault();
|
||||
|
||||
onClose();
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
ModalWorkflowSource.propTypes = {
|
||||
editorState: PropTypes.object.isRequired,
|
||||
entityType: PropTypes.object.isRequired,
|
||||
entity: PropTypes.object,
|
||||
onComplete: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
ModalWorkflowSource.defaultProps = {
|
||||
entity: null,
|
||||
};
|
||||
|
||||
export default ModalWorkflowSource;
|
||||
|
|
@ -432,7 +432,7 @@ def register_core_features(features):
|
|||
'type': ENTITY_TYPES.LINK,
|
||||
'icon': 'link',
|
||||
'description': str(_('Link')),
|
||||
'source': 'LinkSource',
|
||||
'source': 'ModalWorkflowSource',
|
||||
'decorator': 'Link',
|
||||
# We want to enforce constraints on which links can be pasted into rich text.
|
||||
# Keep only the attributes Wagtail needs.
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ def register_document_feature(features):
|
|||
'type': ENTITY_TYPES.DOCUMENT,
|
||||
'icon': 'doc-full',
|
||||
'description': str(_('Document')),
|
||||
'source': 'DocumentSource',
|
||||
'source': 'ModalWorkflowSource',
|
||||
'decorator': 'Document',
|
||||
})
|
||||
)
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ def register_embed_feature(features):
|
|||
'type': ENTITY_TYPES.EMBED,
|
||||
'icon': 'media',
|
||||
'description': str(_('Embed')),
|
||||
'source': 'EmbedSource',
|
||||
'source': 'ModalWorkflowSource',
|
||||
'block': 'EmbedBlock',
|
||||
})
|
||||
)
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ def register_image_feature(features):
|
|||
'type': ENTITY_TYPES.IMAGE,
|
||||
'icon': 'image',
|
||||
'description': str(_('Image')),
|
||||
'source': 'ImageSource',
|
||||
'source': 'ModalWorkflowSource',
|
||||
'block': 'ImageBlock',
|
||||
# We do not want users to be able to copy-paste hotlinked images into rich text.
|
||||
# Keep only the attributes Wagtail needs.
|
||||
|
|
|
|||
Loading…
Reference in a new issue