From 7634c4d58b33230323539226fb8f2eee7f8bc5df Mon Sep 17 00:00:00 2001 From: Vigneshkumar Chinnachamy M Date: Sat, 8 Jun 2019 15:46:00 +0530 Subject: [PATCH] fix parsing links from pasted text --- .../EditLinkPopoverWrapper.jsx | 69 ++++++------ .../RichTextArea/LinkPlugin/Link/Link.jsx | 15 ++- .../RichTextArea/LinkPlugin/LinkPlugin.js | 6 +- .../LinkPlugin/utils/handlePastedText.js | 104 ++++++++++++++++++ 4 files changed, 154 insertions(+), 40 deletions(-) create mode 100644 src/components/RichTextArea/LinkPlugin/utils/handlePastedText.js diff --git a/src/components/RichTextArea/LinkPlugin/EditLinkPopoverWrapper/EditLinkPopoverWrapper.jsx b/src/components/RichTextArea/LinkPlugin/EditLinkPopoverWrapper/EditLinkPopoverWrapper.jsx index 1d5fa3398..a87908eaf 100644 --- a/src/components/RichTextArea/LinkPlugin/EditLinkPopoverWrapper/EditLinkPopoverWrapper.jsx +++ b/src/components/RichTextArea/LinkPlugin/EditLinkPopoverWrapper/EditLinkPopoverWrapper.jsx @@ -96,8 +96,8 @@ export default class EditLinkPopoverWrapper extends React.Component { autoPopover: false, popoverOnTop: false }) - } else if (evt.key === 'Backspace') { - // Backspace should close the popover as it may delete the created link entity + } else if (evt.key === 'Backspace' || evt.key === 'Delete') { + // Backspace or Delete should close the popover as it may delete the created link entity this.setState({ autoPopover: false, popoverOnTop: false @@ -140,14 +140,23 @@ export default class EditLinkPopoverWrapper extends React.Component { */ onEditorStateChange(editorState, prevLastCreatedEntityKey) { const selectionState = editorState.getSelection() + const contentState = editorState.getCurrentContent() - // If cursor moved - if ( - this.isSelectionChanged(selectionState) && - selectionState.getHasFocus() - ) { - const contentState = editorState.getCurrentContent() + const lastCreatedEntityKey = contentState.getLastCreatedEntityKey() + const lastCreatedEntity = getEntityForKey( + contentState, + lastCreatedEntityKey + ) + const lastCreatedEntityType = lastCreatedEntity && lastCreatedEntity.getType() + const newEntityCreated = + lastCreatedEntityKey && prevLastCreatedEntityKey !== lastCreatedEntityKey + const newLinkCreated = newEntityCreated && lastCreatedEntityType === 'LINK' + const cursorMoved = + this.isSelectionChanged(selectionState) && selectionState.getHasFocus() + + // If cursor moved or new link created + if (cursorMoved || newLinkCreated) { const startBlock = getSelectionBlock(selectionState, contentState) const endBlock = getSelectionBlock(selectionState, contentState, true) @@ -161,36 +170,11 @@ export default class EditLinkPopoverWrapper extends React.Component { selectionState.isCollapsed() ? selectionEnd : selectionEnd - 1 ) - // If the cursor is on a link or whole/part of the link is selected - if ( - startEntity && - endEntity === startEntity && - startEntity.getType() === 'LINK' - ) { - this.editExistingLink( - startBlock.getEntityAt(selectionStart), - contentState - ) - } else if (selectionState.isCollapsed()) { - const lastCreatedEntityKey = contentState.getLastCreatedEntityKey() - const lastCreatedEntity = getEntityForKey( - contentState, - lastCreatedEntityKey - ) - + // If a new link entity created, auto show the popover + if (selectionState.isCollapsed() && newLinkCreated) { const entityData = lastCreatedEntity && lastCreatedEntity.getData() - const entityType = lastCreatedEntity && lastCreatedEntity.getType() - const newEntityCreated = - lastCreatedEntityKey && - prevLastCreatedEntityKey !== lastCreatedEntityKey - - // If a new link entity created, auto show the popover - if ( - newEntityCreated && - entityType === 'LINK' && - entityData && - entityData.url - ) { + + if (entityData && entityData.url) { this.openAutoPopover(lastCreatedEntityKey, contentState) // If enter is pressed, the cursor would be in the start of the line. So, text till cursor will be empty ('') @@ -203,6 +187,17 @@ export default class EditLinkPopoverWrapper extends React.Component { } else { this.hideEdit() } + } + // If the cursor is on a link or whole/part of the link is selected + else if ( + startEntity && + endEntity === startEntity && + startEntity.getType() === 'LINK' + ) { + this.editExistingLink( + startBlock.getEntityAt(selectionStart), + contentState + ) } else { this.hideEdit() } diff --git a/src/components/RichTextArea/LinkPlugin/Link/Link.jsx b/src/components/RichTextArea/LinkPlugin/Link/Link.jsx index 0a7d1aec5..a2f521c32 100644 --- a/src/components/RichTextArea/LinkPlugin/Link/Link.jsx +++ b/src/components/RichTextArea/LinkPlugin/Link/Link.jsx @@ -24,9 +24,22 @@ export default class Link extends Component { this.setElementGetter(contentState, entityKey) } + componentWillUnmount() { + const { contentState, entityKey } = this.props + this.removeElementGetter(contentState, entityKey) + } + setElementGetter(contentState, entityKey) { contentState.mergeEntityData(entityKey, { - el: () => this.element + el: () => { + return this.element + } + }) + } + + removeElementGetter(contentState, entityKey) { + contentState.mergeEntityData(entityKey, { + el: null }) } diff --git a/src/components/RichTextArea/LinkPlugin/LinkPlugin.js b/src/components/RichTextArea/LinkPlugin/LinkPlugin.js index 7c29ff3dd..c9f7280ea 100644 --- a/src/components/RichTextArea/LinkPlugin/LinkPlugin.js +++ b/src/components/RichTextArea/LinkPlugin/LinkPlugin.js @@ -2,6 +2,7 @@ import Link from './Link/Link' import linkStrategy from './linkStrategy' import createLinkEntity from './utils/createLink' +import handlePastedText from './utils/handlePastedText' /** * Creates link plugin @@ -17,9 +18,10 @@ export default function createLinkPlugin() { decorators: [ { strategy: linkStrategy, - component: Link, + component: Link } ], - onChange: createLinkEntity + onChange: createLinkEntity, + handlePastedText } } diff --git a/src/components/RichTextArea/LinkPlugin/utils/handlePastedText.js b/src/components/RichTextArea/LinkPlugin/utils/handlePastedText.js new file mode 100644 index 000000000..93c862e25 --- /dev/null +++ b/src/components/RichTextArea/LinkPlugin/utils/handlePastedText.js @@ -0,0 +1,104 @@ +import { EditorState, Modifier, convertFromHTML } from 'draft-js' + +import newLinkifyIt from 'linkify-it' +import tlds from 'tlds' + +const linkifyIt = newLinkifyIt() +linkifyIt.tlds(tlds) + +/** + * Handle pasted text + * @param {string} text - pasted text + * @param {string} html - pasted html + * @param {Object} lastEditorState - current editor state + * @param {Object} extras - Extra object passed by draft-js-link-editor. + * @param {function} extras.setEditorState - function to set new editor state. + * @param {function} extras.getEditorState - function to get current editor state. + */ +export default function handlePastedText( + text, + html, + lastEditorState, + { setEditorState, getEditorState } +) { + if (!text) { + return false + } + + // parse all the links in the pasted text + const links = linkifyIt.match(text) + if (!links) { + return false + } + + // Let the editor render the pasted text before we detect the links + setTimeout(() => { + const lastSelectionState = lastEditorState.getSelection() + const startKey = lastSelectionState.getStartKey() + let startOffset = lastSelectionState.getStartOffset() + + const newBlocks = html && convertFromHTML(html).contentBlocks + const pastedLines = text.split('\n').filter(line => line) + + // When we paste formatted text, draftjs pastes it automatically in a new line (i.e., new block) + const extraBlockInserted = + newBlocks && newBlocks.length > pastedLines.length + + const currentEditorState = getEditorState() + const initialSelectionState = currentEditorState.getSelection() + let currentContentState = currentEditorState.getCurrentContent() + let currentSelectionState = initialSelectionState + + let currentBlock = extraBlockInserted + ? currentContentState.getBlockAfter(startKey) + : currentContentState.getBlockForKey(startKey) + let currentBlockKey = currentBlock.getKey() + let currentLine + + for (let i = 0, numLines = pastedLines.length; i < numLines; i++) { + currentLine = pastedLines[i] + + const linksInLine = linkifyIt.match(currentLine) + if (linksInLine) { + linksInLine.forEach(({ index, lastIndex, url }) => { + // Create the link entity + currentContentState = currentContentState.createEntity( + 'LINK', + 'MUTABLE', + { + url, + text: null + } + ) + const entityKey = currentContentState.getLastCreatedEntityKey() + + // Apply the link entity + currentSelectionState = currentSelectionState.merge({ + anchorOffset: index + startOffset, + focusOffset: lastIndex + startOffset, + anchorKey: currentBlockKey, + focusKey: currentBlockKey, + isBackward: false + }) + currentContentState = Modifier.applyEntity( + currentContentState, + currentSelectionState, + entityKey + ) + }) + } + + currentBlock = currentContentState.getBlockAfter(currentBlockKey) + currentBlockKey = currentBlock && currentBlock.getKey() + startOffset = 0 + } + + // Move cursor to the end of the pasted text + const newEditorState = EditorState.forceSelection( + EditorState.push(currentEditorState, currentContentState, 'apply-entity'), + initialSelectionState + ) + + setEditorState(newEditorState) + }) +}