Browse Source

Refactor Draftail sources to single component

Thibaud Colas 7 years ago
parent
commit
f8b99045a7

+ 2 - 8
client/src/components/Draftail/index.js

@@ -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,

+ 0 - 42
client/src/components/Draftail/sources/DocumentSource.js

@@ -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;

+ 0 - 44
client/src/components/Draftail/sources/EmbedSource.js

@@ -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;

+ 0 - 43
client/src/components/Draftail/sources/ImageSource.js

@@ -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;

+ 0 - 93
client/src/components/Draftail/sources/LinkSource.js

@@ -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;

+ 0 - 73
client/src/components/Draftail/sources/ModalSource.js

@@ -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 - 0
client/src/components/Draftail/sources/ModalWorkflowSource.js

@@ -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;

+ 1 - 1
wagtail/admin/wagtail_hooks.py

@@ -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.

+ 1 - 1
wagtail/documents/wagtail_hooks.py

@@ -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',
         })
     )

+ 1 - 1
wagtail/embeds/wagtail_hooks.py

@@ -57,7 +57,7 @@ def register_embed_feature(features):
             'type': ENTITY_TYPES.EMBED,
             'icon': 'media',
             'description': str(_('Embed')),
-            'source': 'EmbedSource',
+            'source': 'ModalWorkflowSource',
             'block': 'EmbedBlock',
         })
     )

+ 1 - 1
wagtail/images/wagtail_hooks.py

@@ -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.