diff --git a/packages/volto-slate/news/8402.bugfix b/packages/volto-slate/news/8402.bugfix new file mode 100644 index 00000000000..533356d9db0 --- /dev/null +++ b/packages/volto-slate/news/8402.bugfix @@ -0,0 +1 @@ +Fixed cursor position lost when clicking on a Slate block in edit, causing the cursor to jump to the start of the text. @wesleybl diff --git a/packages/volto-slate/src/editor/SlateEditor.jsx b/packages/volto-slate/src/editor/SlateEditor.jsx index 5bd6f001dac..43f214bc7bd 100644 --- a/packages/volto-slate/src/editor/SlateEditor.jsx +++ b/packages/volto-slate/src/editor/SlateEditor.jsx @@ -54,6 +54,7 @@ class SlateEditor extends Component { this.createEditor = this.createEditor.bind(this); this.multiDecorator = this.multiDecorator.bind(this); this.handleChange = this.handleChange.bind(this); + this.handlePointerDown = this.handlePointerDown.bind(this); this.getSavedSelection = this.getSavedSelection.bind(this); this.setSavedSelection = this.setSavedSelection.bind(this); this.scheduleFocus = this.scheduleFocus.bind(this); @@ -73,6 +74,31 @@ class SlateEditor extends Component { this.editor = null; this.selectionTimeout = null; + this.pendingPointerSelection = null; + } + + handlePointerDown(event) { + if (this.props.selected) return; + + const nativeEvent = event.nativeEvent || event; + const domPoint = document.caretPositionFromPoint?.( + nativeEvent.clientX, + nativeEvent.clientY, + ); + if (!domPoint) return; + + try { + const point = ReactEditor.toSlatePoint( + this.state.editor, + [domPoint.offsetNode, domPoint.offset], + { exactMatch: false, suppressThrow: true }, + ); + if (point) { + this.pendingPointerSelection = { anchor: point, focus: point }; + } + } catch { + this.pendingPointerSelection = null; + } } getSavedSelection() { @@ -185,7 +211,10 @@ class SlateEditor extends Component { if (!prevProps.selected && this.props.selected) { // if the SlateEditor becomes selected from unselected - if (window.getSelection().type === 'None') { + if (this.pendingPointerSelection) { + Transforms.select(this.state.editor, this.pendingPointerSelection); + this.pendingPointerSelection = null; + } else if (window.getSelection().type === 'None') { // TODO: why is this condition checked? Transforms.select( this.state.editor, @@ -326,6 +355,7 @@ class SlateEditor extends Component { return null; }} onClick={this.props.onClick} + onPointerDown={this.handlePointerDown} onSelect={(e) => { if (!selected && this.props.onFocus) { // we can't overwrite the onFocus of Editable, as the onFocus diff --git a/packages/volto/cypress/tests/core/blocks/blocks-slate-cursor.js b/packages/volto/cypress/tests/core/blocks/blocks-slate-cursor.js new file mode 100644 index 00000000000..aa37579238f --- /dev/null +++ b/packages/volto/cypress/tests/core/blocks/blocks-slate-cursor.js @@ -0,0 +1,94 @@ +describe('Slate cursor position Tests', () => { + const EDITABLE = '.content-area .slate-editor [contenteditable=true]'; + const BLOCK = '.block.slate'; + const TEXT = 'The quick brown fox jumps over the lazy dog'; + + beforeEach(() => { + cy.intercept('GET', `/**/*?expand*`).as('content'); + cy.intercept('GET', '/**/Document').as('schema'); + // given a logged in editor and a page in edit mode + cy.autologin(); + cy.createContent({ + contentType: 'Document', + contentId: 'my-page', + contentTitle: 'My Page', + bodyModifier: (body) => ({ + ...body, + blocks: { + 'block-1': { + '@type': 'slate', + value: [{ type: 'p', children: [{ text: 'ab' }] }], + }, + 'block-2': { + '@type': 'slate', + value: [{ type: 'p', children: [{ text: 'abc' }] }], + }, + 'block-3': { + '@type': 'slate', + value: [{ type: 'p', children: [{ text: 'ab' }] }], + }, + 'block-4': { + '@type': 'slate', + value: [{ type: 'p', children: [{ text: TEXT }] }], + }, + }, + blocks_layout: { + items: ['block-1', 'block-2', 'block-3', 'block-4'], + }, + }), + }); + cy.visit('/'); + cy.wait('@content'); + + cy.navigate('/my-page/edit'); + cy.wait('@schema'); + }); + + const getEndOfTextCoordinates = (editable) => { + const walker = document.createTreeWalker(editable, NodeFilter.SHOW_TEXT); + let textNode; + while (walker.nextNode()) { + if (walker.currentNode.textContent === TEXT) { + textNode = walker.currentNode; + break; + } + } + expect( + Boolean(textNode), + 'text node of the last block to be found', + ).to.equal(true); + const range = document.createRange(); + range.setStart(textNode, textNode.textContent.length - 1); + range.setEnd(textNode, textNode.textContent.length); + const rect = range.getClientRects()[0]; + const editableRect = editable.getBoundingClientRect(); + return { + x: rect.right - editableRect.left - 1, + y: rect.top - editableRect.top + rect.height / 2, + }; + }; + + it('keeps the cursor at the click position when clicking at the end of a slate block', () => { + // when I click at the very end of the text + cy.get(EDITABLE) + .last() + .scrollIntoView({ block: 'center' }) + .then(($editable) => { + const { x, y } = getEndOfTextCoordinates($editable[0]); + cy.wrap($editable).click(x, y); + }); + + // then the block should be selected and the cursor should stay at the end + cy.get(BLOCK).last().should('have.class', 'selected'); + + cy.window().then((win) => { + const range = win.getSelection().getRangeAt(0); + const anchor = range.startContainer; + const offset = + anchor.nodeType === Node.TEXT_NODE ? range.startOffset : -1; + expect(offset, `cursor offset ${offset} to stay at the end`).to.equal( + TEXT.length, + ); + }); + }); +}); diff --git a/packages/volto/news/8402.bugfix b/packages/volto/news/8402.bugfix new file mode 100644 index 00000000000..533356d9db0 --- /dev/null +++ b/packages/volto/news/8402.bugfix @@ -0,0 +1 @@ +Fixed cursor position lost when clicking on a Slate block in edit, causing the cursor to jump to the start of the text. @wesleybl