Thibaud Colas 2017-12-05 19:49:17 +02:00
rodzic 00009252ac
commit c163d93b72
18 zmienionych plików z 584 dodań i 0 usunięć

Wyświetl plik

@ -0,0 +1,29 @@
@import '../../../../node_modules/draftail/dist/draftail';
// Give each block element eg. paragraph some spacing so we don't end up
// with empty paragraphs in code when user double enters because they think
// there are no paragraphs.
div[data-block="true"] {
margin: 0.75rem 0;
}
/* Override Wagtail styles. */
.DraftEditor-editorContainer h2 {
text-transform: none;
}
/* Override Wagtail styles. */
.DraftEditor-editorContainer h3,
.DraftEditor-editorContainer h4,
.DraftEditor-editorContainer h5,
.DraftEditor-editorContainer h6 {
font-weight: bold;
}
/* Get rid of cray number 6 that appears above h2 elements in draft editor. */
.object.collapsible .DraftEditor-editorContainer h2:before,
.object.collapsible .DraftEditor-editorContainer h2 label:before {
// TODO Assess whether !important is necessary.
// stylelint-disable declaration-no-important
content: none !important;
}

Wyświetl plik

@ -0,0 +1,21 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Icon } from 'draftail';
const Document = ({ entityKey, contentState, children }) => {
const { title } = contentState.getEntity(entityKey).getData();
return (
<span data-tooltip={entityKey} className="RichEditor-link" title={title}>
<Icon name="icon-doc-full" />
{children}
</span>
);
};
Document.propTypes = {
entityKey: PropTypes.string.isRequired,
contentState: PropTypes.object.isRequired,
children: PropTypes.node.isRequired,
};
export default Document;

Wyświetl plik

@ -0,0 +1,25 @@
import React from 'react';
import { shallow } from 'enzyme';
import { convertFromHTML, ContentState } from 'draft-js';
import Document from './Document';
describe('Document', () => {
it('exists', () => {
expect(Document).toBeDefined();
});
it('renders', () => {
const contentBlocks = convertFromHTML('<h1>aaaaaaaaaa</h1>');
const contentState = ContentState.createFromBlockArray(contentBlocks);
const contentStateWithEntity = contentState.createEntity('DOCUMENT', 'MUTABLE', { title: 'Test title' });
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
expect(shallow((
<Document
entityKey={entityKey}
contentState={contentStateWithEntity}
>
<span>Test children</span>
</Document>
))).toMatchSnapshot();
});
});

Wyświetl plik

@ -0,0 +1,22 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Icon } from 'draftail';
const Link = ({ entityKey, contentState, children }) => {
const { url } = contentState.getEntity(entityKey).getData();
return (
<span data-tooltip={entityKey} className="RichEditor-link">
<Icon name={`icon-${url.indexOf('mailto:') !== -1 ? 'mail' : 'link'}`} />
{children}
</span>
);
};
Link.propTypes = {
entityKey: PropTypes.string.isRequired,
contentState: PropTypes.object.isRequired,
children: PropTypes.node.isRequired,
};
export default Link;

Wyświetl plik

@ -0,0 +1,40 @@
import React from 'react';
import { shallow } from 'enzyme';
import { convertFromHTML, ContentState } from 'draft-js';
import Link from './Link';
describe('Link', () => {
it('exists', () => {
expect(Link).toBeDefined();
});
it('renders', () => {
const contentBlocks = convertFromHTML('<h1>aaaaaaaaaa</h1>');
const contentState = ContentState.createFromBlockArray(contentBlocks);
const contentStateWithEntity = contentState.createEntity('LINK', 'MUTABLE', { url: 'http://example.com/' });
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
expect(shallow((
<Link
entityKey={entityKey}
contentState={contentStateWithEntity}
>
<span>Test children</span>
</Link>
))).toMatchSnapshot();
});
it('renders email', () => {
const contentBlocks = convertFromHTML('<h1>aaaaaaaaaa</h1>');
const contentState = ContentState.createFromBlockArray(contentBlocks);
const contentStateWithEntity = contentState.createEntity('LINK', 'MUTABLE', { url: 'mailto:test@example.com' });
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
expect(shallow((
<Link
entityKey={entityKey}
contentState={contentStateWithEntity}
>
<span>Test children</span>
</Link>
))).toMatchSnapshot();
});
});

Wyświetl plik

@ -0,0 +1,18 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Document renders 1`] = `
<span
className="RichEditor-link"
data-tooltip="1"
title="Test title"
>
<Icon
className=""
name="icon-doc-full"
title={null}
/>
<span>
Test children
</span>
</span>
`;

Wyświetl plik

@ -0,0 +1,33 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Link renders 1`] = `
<span
className="RichEditor-link"
data-tooltip="1"
>
<Icon
className=""
name="icon-link"
title={null}
/>
<span>
Test children
</span>
</span>
`;
exports[`Link renders email 1`] = `
<span
className="RichEditor-link"
data-tooltip="2"
>
<Icon
className=""
name="icon-mail"
title={null}
/>
<span>
Test children
</span>
</span>
`;

Wyświetl plik

@ -0,0 +1,10 @@
import Link from './Link';
import Document from './Document';
/**
* Mapping object from name to component.
*/
export default {
Link,
Document,
};

Wyświetl plik

@ -0,0 +1,7 @@
import entities from './index';
describe('entities', () => {
it('exists', () => {
expect(entities).toBeInstanceOf(Object);
});
});

Wyświetl plik

@ -0,0 +1,56 @@
import React from 'react';
import ReactDOM from 'react-dom';
import DraftailEditor from 'draftail';
import decorators from './decorators';
import sources from './sources';
import registry from './registry';
export const initEditor = (fieldName, options = {}) => {
const field = document.querySelector(`[name="${fieldName}"]`);
const editorWrapper = document.createElement('div');
field.parentNode.appendChild(editorWrapper);
const serialiseInputValue = (rawContentState) => {
// TODO Remove default {} when finishing https://github.com/springload/wagtaildraftail/issues/32.
field.value = JSON.stringify(rawContentState || {});
};
if (options.entityTypes) {
// eslint-disable-next-line no-param-reassign
options.entityTypes = options.entityTypes.map(entity => Object.assign(entity, {
source: registry.getSource(entity.source),
strategy: registry.getStrategy(entity.type) || null,
decorator: registry.getDecorator(entity.decorator),
}));
}
const fieldValue = JSON.parse(field.value);
// TODO Remove default null when finishing https://github.com/springload/wagtaildraftail/issues/32.
const rawContentState = fieldValue && Object.keys(fieldValue).length === 0 ? null : fieldValue;
const editor = (
<DraftailEditor
rawContentState={rawContentState}
onSave={serialiseInputValue}
placeholder="Write here…"
{...options}
/>
);
ReactDOM.render(editor, editorWrapper);
};
// Register default Decorators and Sources
registry.registerDecorators(decorators);
registry.registerSources(sources);
const draftail = Object.assign({
initEditor: initEditor,
// Expose basic React methods for basic needs
// TODO Expose React as global as part of Wagtail vendor file instead of doing this.
// createClass: React.createClass,
// createElement: React.createElement,
}, registry);
export default draftail;

Wyświetl plik

@ -0,0 +1,32 @@
const registry = {
decorators: {},
sources: {},
strategies: {},
};
const registerDecorators = (decorators) => {
Object.assign(registry.decorators, decorators);
};
const getDecorator = name => registry.decorators[name];
const registerSources = (sources) => {
Object.assign(registry.sources, sources);
};
const getSource = name => registry.sources[name];
const registerStrategies = (strategies) => {
Object.assign(registry.strategies, strategies);
};
const getStrategy = name => registry.strategies[name];
export default {
registerDecorators,
getDecorator,
registerSources,
getSource,
registerStrategies,
getStrategy,
};

Wyświetl plik

@ -0,0 +1,40 @@
import ModalSource from './ModalSource';
const $ = global.jQuery;
class DocumentSource extends ModalSource {
constructor(props) {
super(props);
this.parseData = this.parseData.bind(this);
}
parseData(documentData) {
this.onConfirm(documentData);
}
componentDidMount() {
const { entity } = this.props;
const documentChooser = global.chooserUrls.documentChooser;
const url = documentChooser;
$(document.body).on('hidden.bs.modal', this.onClose);
// TODO: wagtail should support passing params to this endpoint.
if (entity) {
// const entityData = entity.getData();
// console.log(entityData);
// if (entityData.title) {
// url = url + `?q=${entityData.title}`
// }
}
global.ModalWorkflow({
url,
responses: {
documentChosen: this.parseData,
},
});
}
}
export default DocumentSource;

Wyświetl plik

@ -0,0 +1,36 @@
import ModalSource from './ModalSource';
const $ = global.jQuery;
class EmbedSource extends ModalSource {
constructor(props) {
super(props);
this.parseData = this.parseData.bind(this);
}
parseData(html) {
const embed = $.parseHTML(html)[0];
this.onConfirmAtomicBlock({
embedType: embed.getAttribute('data-embedtype'),
url: embed.getAttribute('data-url'),
providerName: embed.getAttribute('data-provider-name'),
authorName: embed.getAttribute('data-author-name'),
thumbnail: embed.getAttribute('data-thumbnail-url'),
title: embed.getAttribute('data-title'),
});
}
componentDidMount() {
$(document.body).on('hidden.bs.modal', this.onClose);
global.ModalWorkflow({
url: global.chooserUrls.embedsChooser,
responses: {
embedChosen: this.parseData,
},
});
}
}
export default EmbedSource;

Wyświetl plik

@ -0,0 +1,30 @@
import ModalSource from './ModalSource';
const $ = global.jQuery;
class ImageSource extends ModalSource {
constructor(props) {
super(props);
this.parseData = this.parseData.bind(this);
}
parseData(imageData) {
this.onConfirmAtomicBlock(Object.assign({}, imageData, {
src: imageData.preview.url,
}));
}
componentDidMount() {
const imageChooser = global.chooserUrls.imageChooser;
$(document.body).on('hidden.bs.modal', this.onClose);
global.ModalWorkflow({
url: imageChooser,
responses: {
imageChosen: this.parseData,
},
});
}
}
export default ImageSource;

Wyświetl plik

@ -0,0 +1,100 @@
import ModalSource from './ModalSource';
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',
link_text: '',
};
if (entity) {
let data = entity.getData();
if (typeof data === 'string') {
data = { url: data, linkType: 'external', title: '' };
}
urlParams.link_text = data.title;
switch (data.linkType) {
case 'page':
url = ` ${pageChooser}${data.parentId}/`;
break;
case 'email':
url = emailLinkChooser;
urlParams.link_url = data.url.replace('mailto:', '');
break;
default:
url = externalLinkChooser;
urlParams.link_url = data.url;
break;
}
}
return { url, urlParams };
};
class LinkSource extends ModalSource {
constructor(props) {
super(props);
this.parseData = this.parseData.bind(this);
}
// Plaster over more Wagtail internals.
parseData(pageData) {
const data = Object.assign({}, pageData);
if (data.id) {
data.linkType = 'page';
} else if (data.url.indexOf('mailto:') === 0) {
data.linkType = 'email';
} else {
data.linkType = 'external';
}
// We do not want each link to have the page's title as an attr.
// nor links to have the link URL as a title.
if (data.linkType === 'page' || data.url.replace('mailto:', '') === data.title) {
delete data.title;
}
this.onConfirm(data);
}
componentDidMount() {
const { entity } = 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);
global.ModalWorkflow({
url,
urlParams,
responses: {
pageChosen: this.parseData,
},
});
}
}
export default LinkSource;

Wyświetl plik

@ -0,0 +1,64 @@
import PropTypes from 'prop-types';
import React from 'react';
import { AtomicBlockUtils, RichUtils } 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) {
const { editorState, options, onUpdate } = this.props;
const contentState = editorState.getCurrentContent();
const contentStateWithEntity = contentState.createEntity(options.type, 'MUTABLE', data);
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
const nextState = RichUtils.toggleLink(editorState, editorState.getSelection(), entityKey);
onUpdate(nextState);
}
onConfirmAtomicBlock(data) {
const { editorState, options, onUpdate } = this.props;
const contentState = editorState.getCurrentContent();
const contentStateWithEntity = contentState.createEntity(options.type, 'IMMUTABLE', data);
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
const nextState = AtomicBlockUtils.insertAtomicBlock(editorState, entityKey, ' ');
onUpdate(nextState);
}
onClose(e) {
const { onClose } = this.props;
e.preventDefault();
onClose();
}
render() {
return null;
}
}
ModalSource.propTypes = {
editorState: PropTypes.object.isRequired,
options: PropTypes.object.isRequired,
// eslint-disable-next-line
entity: PropTypes.object,
onUpdate: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
ModalSource.defaultProps = {
entity: null,
};
export default ModalSource;

Wyświetl plik

@ -0,0 +1,14 @@
import LinkSource from './LinkSource';
import ImageSource from './ImageSource';
import DocumentSource from './DocumentSource';
import EmbedSource from './EmbedSource';
/**
* Mapping object from name to component.
*/
export default {
LinkSource,
ImageSource,
DocumentSource,
EmbedSource,
};

Wyświetl plik

@ -0,0 +1,7 @@
import sources from './index';
describe('sources', () => {
it('exists', () => {
expect(sources).toBeInstanceOf(Object);
});
});