Selaa lähdekoodia

Adding external link with selected text now includes text in link chooser. Fix #4328 (#4366)

Tony Yates 7 vuotta sitten
vanhempi
commit
cab90e5d1b

+ 1 - 0
CHANGELOG.txt

@@ -23,6 +23,7 @@ Changelog
  * Fix: Correct dropdown arrow styling in Firefox, IE11 (Janneke Janssen, Alexs Mathilda)
  * Fix: Password reset no indicates specific validation errors on certain password restrictions (Lucas Moeskops)
  * Fix: Confirmation page on page deletion now respects custom `get_admin_display_title` methods (Kim Chee Leong)
+ * Fix: Adding external link with selected text now includes text in link chooser (Tony Yates, Thibaud Colas, Alexs Mathilda)
 
 
 2.0.1 (xx.xx.xxxx) - IN DEVELOPMENT

+ 2 - 0
CONTRIBUTORS.rst

@@ -284,6 +284,8 @@ Contributors
 * Kevin Chung
 * Kim Chee Leong
 * Dan Swain
+* Alexs Mathilda
+* Tony Yates
 
 Translators
 ===========

+ 44 - 0
client/src/components/Draftail/DraftUtils.js

@@ -0,0 +1,44 @@
+
+  /**
+  * Returns collection of currently selected blocks.
+  * See https://github.com/jpuri/draftjs-utils/blob/e81c0ae19c3b0fdef7e0c1b70d924398956be126/js/block.js#L19.
+  */
+  const getSelectedBlocksList = (editorState) => {
+    const selectionState = editorState.getSelection();
+    const content = editorState.getCurrentContent();
+    const startKey = selectionState.getStartKey();
+    const endKey = selectionState.getEndKey();
+    const blockMap = content.getBlockMap();
+    const blocks =  blockMap
+      .toSeq()
+      .skipUntil((_, k) => k === startKey)
+      .takeUntil((_, k) => k === endKey)
+      .concat([[endKey, blockMap.get(endKey)]]);
+    return blocks.toList();
+  };
+
+  /**
+  * Returns the currently selected text in the editor.
+  * See https://github.com/jpuri/draftjs-utils/blob/e81c0ae19c3b0fdef7e0c1b70d924398956be126/js/block.js#L106.
+  */
+  export const getSelectionText = (editorState) => {
+    const selection = editorState.getSelection();
+    let start = selection.getAnchorOffset();
+    let end = selection.getFocusOffset();
+    const selectedBlocks = getSelectedBlocksList(editorState);
+
+    if (selection.getIsBackward()) {
+      const temp = start;
+      start = end;
+      end = temp;
+    }
+
+    let selectedText = '';
+    for (let i = 0; i < selectedBlocks.size; i += 1) {
+      const blockStart = i === 0 ? start : 0;
+      const blockEnd = i === (selectedBlocks.size - 1) ? end : selectedBlocks.get(i).getText().length;
+      selectedText += selectedBlocks.get(i).getText().slice(blockStart, blockEnd);
+    }
+
+    return selectedText;
+  };

+ 121 - 0
client/src/components/Draftail/DraftUtils.test.js

@@ -0,0 +1,121 @@
+import {
+    EditorState,
+    convertFromRaw,
+} from 'draft-js';
+
+import { getSelectionText } from './DraftUtils';
+
+describe('DraftUtils', () => {
+  describe('#getSelectionText', () => {
+    it('works', () => {
+      const content = convertFromRaw({
+        entityMap: {},
+        blocks: [
+          {
+            key: 'a',
+            text: 'test1234',
+          },
+        ],
+      });
+      let editorState = EditorState.createWithContent(content);
+
+      let selection = editorState.getSelection();
+      selection = selection.merge({
+        anchorOffset: 0,
+        focusOffset: 4,
+      });
+
+      editorState = EditorState.acceptSelection(editorState, selection);
+
+      expect(getSelectionText(editorState)).toBe('test');
+    });
+
+    it('empty', () => {
+      expect(getSelectionText(EditorState.createEmpty())).toBe('');
+    });
+
+    it('backwards', () => {
+      const content = convertFromRaw({
+        entityMap: {},
+        blocks: [
+          {
+            key: 'a',
+            text: 'test1234',
+          },
+        ],
+      });
+      let editorState = EditorState.createWithContent(content);
+
+      let selection = editorState.getSelection();
+      selection = selection.merge({
+        anchorOffset: 8,
+        focusOffset: 4,
+        isBackward: true,
+      });
+
+      editorState = EditorState.acceptSelection(editorState, selection);
+
+      expect(getSelectionText(editorState)).toBe('1234');
+    });
+
+    it('multiblock', () => {
+      const content = convertFromRaw({
+        entityMap: {},
+        blocks: [
+          {
+            key: 'a',
+            text: 'test1234',
+          },
+          {
+            key: 'b',
+            text: 'multiblock',
+          }
+        ],
+      });
+      let editorState = EditorState.createWithContent(content);
+
+      let selection = editorState.getSelection();
+      selection = selection.merge({
+        anchorKey: 'a',
+        focusKey: 'b',
+        anchorOffset: 4,
+        focusOffset: 5,
+        isBackward: false,
+      });
+
+      editorState = EditorState.acceptSelection(editorState, selection);
+
+      expect(getSelectionText(editorState)).toBe('1234multi');
+    });
+
+    it('multiblock-backwards', () => {
+      const content = convertFromRaw({
+        entityMap: {},
+        blocks: [
+          {
+            key: 'a',
+            text: 'test1234',
+          },
+          {
+            key: 'b',
+            text: 'multiblock',
+          }
+        ],
+      });
+      let editorState = EditorState.createWithContent(content);
+
+      let selection = editorState.getSelection();
+      selection = selection.merge({
+        focusKey: 'a',
+        anchorKey: 'b',
+        anchorOffset: 5,
+        focusOffset: 4,
+        isBackward: true,
+      });
+
+      editorState = EditorState.acceptSelection(editorState, selection);
+
+      expect(getSelectionText(editorState)).toBe('1234multi');
+    });
+  });
+});

+ 6 - 7
client/src/components/Draftail/sources/ModalWorkflowSource.js

@@ -4,6 +4,7 @@ import { AtomicBlockUtils, Modifier, RichUtils, EditorState } from 'draft-js';
 import { ENTITY_TYPE } from 'draftail';
 
 import { STRINGS } from '../../../config/wagtailConfig';
+import { getSelectionText } from '../DraftUtils';
 
 const $ = global.jQuery;
 
@@ -16,7 +17,7 @@ MUTABILITY[DOCUMENT] = 'MUTABLE';
 MUTABILITY[ENTITY_TYPE.IMAGE] = 'IMMUTABLE';
 MUTABILITY[EMBED] = 'IMMUTABLE';
 
-export const getChooserConfig = (entityType, entity) => {
+export const getChooserConfig = (entityType, entity, selectedText) => {
   const chooserURL = {};
   chooserURL[ENTITY_TYPE.IMAGE] = `${global.chooserUrls.imageChooser}?select_format=true`;
   chooserURL[EMBED] = global.chooserUrls.embedsChooser;
@@ -32,10 +33,7 @@ export const getChooserConfig = (entityType, entity) => {
       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: '',
+      link_text: selectedText,
     };
 
     if (entity) {
@@ -113,8 +111,9 @@ class ModalWorkflowSource extends Component {
   }
 
   componentDidMount() {
-    const { onClose, entityType, entity } = this.props;
-    const { url, urlParams } = getChooserConfig(entityType, entity);
+    const { onClose, entityType, entity, editorState } = this.props;
+    const selectedText = getSelectionText(editorState);
+    const { url, urlParams } = getChooserConfig(entityType, entity, selectedText);
 
     $(document.body).on('hidden.bs.modal', this.onClose);
 

+ 13 - 11
client/src/components/Draftail/sources/ModalWorkflowSource.test.js

@@ -2,6 +2,7 @@ import React from 'react';
 import { shallow } from 'enzyme';
 
 import ModalWorkflowSource, { getChooserConfig, filterEntityData } from './ModalWorkflowSource';
+import * as DraftUtils from '../DraftUtils';
 import { EditorState, convertFromRaw, AtomicBlockUtils, RichUtils, Modifier } from 'draft-js';
 
 global.ModalWorkflow = () => {};
@@ -9,6 +10,7 @@ global.ModalWorkflow = () => {};
 describe('ModalWorkflowSource', () => {
   beforeEach(() => {
     jest.spyOn(global, 'ModalWorkflow');
+    jest.spyOn(DraftUtils, 'getSelectionText').mockImplementation(() => '');
   });
 
   afterEach(() => {
@@ -29,21 +31,21 @@ describe('ModalWorkflowSource', () => {
 
   describe('#getChooserConfig', () => {
     it('IMAGE', () => {
-      expect(getChooserConfig({ type: 'IMAGE' })).toEqual({
+      expect(getChooserConfig({ type: 'IMAGE' }, null, '')).toEqual({
         url: '/admin/images/chooser/?select_format=true',
         urlParams: {},
       });
     });
 
     it('EMBED', () => {
-      expect(getChooserConfig({ type: 'EMBED' })).toEqual({
+      expect(getChooserConfig({ type: 'EMBED' }, null, '')).toEqual({
         url: '/admin/embeds/chooser/',
         urlParams: {},
       });
     });
 
     it('DOCUMENT', () => {
-      expect(getChooserConfig({ type: 'DOCUMENT' })).toEqual({
+      expect(getChooserConfig({ type: 'DOCUMENT' }, null, '')).toEqual({
         url: '/admin/documents/chooser/',
         urlParams: {},
       });
@@ -51,25 +53,25 @@ describe('ModalWorkflowSource', () => {
 
     describe('LINK', () => {
       it('no entity', () => {
-        expect(getChooserConfig({ type: 'LINK' })).toMatchSnapshot();
+        expect(getChooserConfig({ type: 'LINK' }, null, '')).toMatchSnapshot();
       });
 
       it('page', () => {
         expect(getChooserConfig({ type: 'LINK' }, {
           getData: () => ({ id: 1, parentId: 0 })
-        })).toMatchSnapshot();
+        }, '')).toMatchSnapshot();
       });
 
       it('mail', () => {
         expect(getChooserConfig({ type: 'LINK' }, {
           getData: () => ({ url: 'mailto:test@example.com' })
-        })).toMatchSnapshot();
+        }, '')).toMatchSnapshot();
       });
 
       it('external', () => {
         expect(getChooserConfig({ type: 'LINK' }, {
           getData: () => ({ url: 'https://www.example.com/' })
-        })).toMatchSnapshot();
+        }, '')).toMatchSnapshot();
       });
     });
   });
@@ -146,7 +148,7 @@ describe('ModalWorkflowSource', () => {
   it('#componentDidMount', () => {
     const wrapper = shallow((
       <ModalWorkflowSource
-        editorState={{}}
+        editorState={EditorState.createEmpty()}
         entityType={{}}
         entity={{}}
         onComplete={() => {}}
@@ -171,7 +173,7 @@ describe('ModalWorkflowSource', () => {
 
     const wrapper = shallow((
       <ModalWorkflowSource
-        editorState={{}}
+        editorState={EditorState.createEmpty()}
         entityType={{}}
         entity={{}}
         onComplete={() => {}}
@@ -192,7 +194,7 @@ describe('ModalWorkflowSource', () => {
   it('#componentWillUnmount', () => {
     const wrapper = shallow((
       <ModalWorkflowSource
-        editorState={{}}
+        editorState={EditorState.createEmpty()}
         entityType={{}}
         entity={{}}
         onComplete={() => {}}
@@ -335,7 +337,7 @@ describe('ModalWorkflowSource', () => {
     const onClose = jest.fn();
     const wrapper = shallow((
       <ModalWorkflowSource
-        editorState={{}}
+        editorState={EditorState.createEmpty()}
         entityType={{}}
         entity={{}}
         onComplete={() => {}}

+ 1 - 0
docs/releases/2.1.rst

@@ -37,6 +37,7 @@ Bug fixes
  * Correct dropdown arrow styling in Firefox, IE11 (Janneke Janssen, Alexs Mathilda)
  * Password reset no indicates specific validation errors on certain password restrictions (Lucas Moeskops)
  * Confirmation page on page deletion now respects custom ``get_admin_display_title`` methods (Kim Chee Leong)
+ * Adding external link with selected text now includes text in link chooser (Tony Yates, Thibaud Colas, Alexs Mathilda)
 
 
 Upgrade considerations