diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 507a94a42b..05ced0ec23 100644 --- a/CHANGELOG.txt +++ b/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 diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst index a993b54dc0..0a4edbaa10 100644 --- a/CONTRIBUTORS.rst +++ b/CONTRIBUTORS.rst @@ -284,6 +284,8 @@ Contributors * Kevin Chung * Kim Chee Leong * Dan Swain +* Alexs Mathilda +* Tony Yates Translators =========== diff --git a/client/src/components/Draftail/DraftUtils.js b/client/src/components/Draftail/DraftUtils.js new file mode 100644 index 0000000000..8df8555fc8 --- /dev/null +++ b/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; + }; diff --git a/client/src/components/Draftail/DraftUtils.test.js b/client/src/components/Draftail/DraftUtils.test.js new file mode 100644 index 0000000000..a8e22cb3cf --- /dev/null +++ b/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'); + }); + }); +}); diff --git a/client/src/components/Draftail/sources/ModalWorkflowSource.js b/client/src/components/Draftail/sources/ModalWorkflowSource.js index ab888c8825..fba1ed38be 100644 --- a/client/src/components/Draftail/sources/ModalWorkflowSource.js +++ b/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); diff --git a/client/src/components/Draftail/sources/ModalWorkflowSource.test.js b/client/src/components/Draftail/sources/ModalWorkflowSource.test.js index fa9f980faa..564981504e 100644 --- a/client/src/components/Draftail/sources/ModalWorkflowSource.test.js +++ b/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(( {}} @@ -171,7 +173,7 @@ describe('ModalWorkflowSource', () => { const wrapper = shallow(( {}} @@ -192,7 +194,7 @@ describe('ModalWorkflowSource', () => { it('#componentWillUnmount', () => { const wrapper = shallow(( {}} @@ -335,7 +337,7 @@ describe('ModalWorkflowSource', () => { const onClose = jest.fn(); const wrapper = shallow(( {}} diff --git a/docs/releases/2.1.rst b/docs/releases/2.1.rst index 6081ad54c0..664eb914af 100644 --- a/docs/releases/2.1.rst +++ b/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