diff --git a/app/javascript/article-form/components/ImageUploader.jsx b/app/javascript/article-form/components/ImageUploader.jsx index b3e1741a7..e1f215348 100644 --- a/app/javascript/article-form/components/ImageUploader.jsx +++ b/app/javascript/article-form/components/ImageUploader.jsx @@ -182,6 +182,7 @@ const V2EditorImageUpload = ({ {...buttonProps} icon={ImageIcon} onClick={(e) => { + buttonProps.onClick?.(e); useNativeUpload ? initNativeImagePicker(e) : document.getElementById('image-upload-field').click(); diff --git a/app/javascript/crayons/MarkdownToolbar/MarkdownToolbar.jsx b/app/javascript/crayons/MarkdownToolbar/MarkdownToolbar.jsx index 11f99d636..77209140f 100644 --- a/app/javascript/crayons/MarkdownToolbar/MarkdownToolbar.jsx +++ b/app/javascript/crayons/MarkdownToolbar/MarkdownToolbar.jsx @@ -59,6 +59,7 @@ const getPreviousMatchingSibling = (element, selector) => { export const MarkdownToolbar = ({ textAreaId }) => { const [textArea, setTextArea] = useState(null); const [overflowMenuOpen, setOverflowMenuOpen] = useState(false); + const [storedCursorPosition, setStoredCursorPosition] = useState({}); const smallScreen = useMediaQuery(`(max-width: ${BREAKPOINTS.Medium - 1}px)`); const markdownSyntaxFormatters = { @@ -190,9 +191,11 @@ export const MarkdownToolbar = ({ textAreaId }) => { }; const handleImageUploadStarted = () => { - const { textBeforeSelection, textAfterSelection, selectionEnd } = + const { textBeforeSelection, textAfterSelection } = getSelectionData(textArea); + const { selectionEnd } = storedCursorPosition; + const textWithPlaceholder = `${textBeforeSelection}\n${UPLOADING_IMAGE_PLACEHOLDER}${textAfterSelection}`; textArea.value = textWithPlaceholder; // Make sure Editor text area updates via linkstate @@ -203,27 +206,46 @@ export const MarkdownToolbar = ({ textAreaId }) => { // Set cursor to the end of the placeholder const newCursorPosition = selectionEnd + UPLOADING_IMAGE_PLACEHOLDER.length + 1; + textArea.setSelectionRange(newCursorPosition, newCursorPosition); }; - const handleImageUploadSuccess = (imageMarkdown) => { + const handleImageUploadEnd = (imageMarkdown = '') => { + const { + selectionStart, + selectionEnd, + value: currentTextAreaValue, + } = textArea; + + const indexOfPlaceholder = currentTextAreaValue.indexOf( + UPLOADING_IMAGE_PLACEHOLDER, + ); + + // User has deleted placeholder, nothing to do + if (indexOfPlaceholder === -1) return; + const newTextValue = textArea.value.replace( UPLOADING_IMAGE_PLACEHOLDER, imageMarkdown, ); - textArea.value = newTextValue; - // Make sure Editor text area updates via linkstate - textArea.dispatchEvent(new Event('input')); - }; - const handleImageUploadError = () => { - const newTextValue = textArea.value.replace( - UPLOADING_IMAGE_PLACEHOLDER, - '', - ); textArea.value = newTextValue; // Make sure Editor text area updates via linkstate textArea.dispatchEvent(new Event('input')); + + // The change to image markdown length does not affect cursor position + if (indexOfPlaceholder > selectionStart) { + textArea.setSelectionRange(selectionStart, selectionEnd); + return; + } + + const differenceInLength = + imageMarkdown.length - UPLOADING_IMAGE_PLACEHOLDER.length; + + textArea.setSelectionRange( + selectionStart + differenceInLength, + selectionEnd + differenceInLength, + ); }; const getSecondaryFormatterButtons = (isOverflow) => @@ -308,10 +330,14 @@ export const MarkdownToolbar = ({ textAreaId }) => { handleToolbarButtonKeyPress(e, 'toolbar-btn'), + onClick: () => { + const { selectionStart, selectionEnd } = textArea; + setStoredCursorPosition({ selectionStart, selectionEnd }); + }, tooltip: smallScreen ? null : ( ), diff --git a/cypress/integration/seededFlows/publishingFlows/uploadImage.spec.js b/cypress/integration/seededFlows/publishingFlows/uploadImage.spec.js index 7beb67c9f..98c81c339 100644 --- a/cypress/integration/seededFlows/publishingFlows/uploadImage.spec.js +++ b/cypress/integration/seededFlows/publishingFlows/uploadImage.spec.js @@ -98,6 +98,28 @@ describe('Upload image', () => { .should('have.text', 'Error message'); }); + it('maintains writing position when image is uploaded', () => { + cy.intercept('/image_uploads', { delay: 2000 }); + + cy.findByLabelText('Post Content').as('editorBody'); + // Type some text and place the cursor between one & two + cy.get('@editorBody') + .clear() + .type('one two{leftarrow}{leftarrow}{leftarrow}'); + + cy.findByLabelText(/Upload image/, { selector: 'button' }).click(); + cy.findByLabelText(/Upload image/, { selector: 'input' }).attachFile( + '/images/admin-image.png', + ); + + // Check that when we continue typing after selecting an image, my cursor is after the placeholder text, which was inserted at my cursor position + cy.get('@editorBody').should('have.focus').type(' more text '); + cy.get('@editorBody').should( + 'have.value', + 'one \n![Uploading image](...) more text two', + ); + }); + it('Uploads a cover image in the editor', () => { cy.findByRole('form', { name: 'Edit post' }).within(() => { cy.findByLabelText(/Add a cover image/).attachFile(