From 3546178171b1e0cf2052321af5e4baa6a9c590a5 Mon Sep 17 00:00:00 2001 From: David Russell Date: Mon, 30 Dec 2024 12:53:58 +0000 Subject: [PATCH 1/4] create a resizable editor extension --- packages/react-codemirror/src/neo4jSetup.tsx | 3 + .../react-codemirror/src/resizableEditor.tsx | 135 ++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 packages/react-codemirror/src/resizableEditor.tsx diff --git a/packages/react-codemirror/src/neo4jSetup.tsx b/packages/react-codemirror/src/neo4jSetup.tsx index d7750d194..eb32f0ed1 100644 --- a/packages/react-codemirror/src/neo4jSetup.tsx +++ b/packages/react-codemirror/src/neo4jSetup.tsx @@ -40,6 +40,7 @@ import { import { lintKeymap } from '@codemirror/lint'; import { getIconForType } from './icons'; +import resizableEditor from './resizableEditor'; const insertTab: StateCommand = (cmd) => { // if there is a selection we should indent the selected text, but if not insert @@ -154,6 +155,8 @@ export const basicNeo4jSetup = ({ ]), ); + extensions.push(resizableEditor()) + return extensions; }; diff --git a/packages/react-codemirror/src/resizableEditor.tsx b/packages/react-codemirror/src/resizableEditor.tsx new file mode 100644 index 000000000..93eb82e41 --- /dev/null +++ b/packages/react-codemirror/src/resizableEditor.tsx @@ -0,0 +1,135 @@ +import { Extension } from '@codemirror/state'; +import { EditorView, ViewPlugin } from '@codemirror/view'; + +const MIN_HEIGHT = 27.4; +const LINE_HEIGHT = 20; +const DRAG_HANDLE_SIZE = 14; +const MIN_LINES = 1; + +function resizableEditor(): Extension { + const resizeHandlePlugin = ViewPlugin.fromClass( + class { + private resizeHandle: HTMLDivElement; + + constructor(view: EditorView) { + const scroller = view.scrollDOM; + const handleOffset = view.dom.clientWidth / 2 - DRAG_HANDLE_SIZE; + + // Create a resize handle using the raw NDL icon svg + this.resizeHandle = document.createElement('div'); + this.resizeHandle.innerHTML = + ''; + this.resizeHandle.className = 'cm-resize-handle'; + this.resizeHandle.style.position = 'absolute'; + this.resizeHandle.style.bottom = '0'; + this.resizeHandle.style.cursor = 'row-resize'; + this.resizeHandle.style.transform = `rotate(90deg) translate(16px, -${handleOffset}px)`; + view.dom.appendChild(this.resizeHandle); + + let dragging = false; + + this.resizeHandle.addEventListener('mousedown', (event: MouseEvent) => { + dragging = true; + // Capture the height of the editor and the mouse position when the drag starts + const startHeight = view.dom.offsetHeight; + const startClientY = event.clientY; + + if (scroller) { + // Hides the horizontal scrollbar while resizing, prevents flickering + scroller.style.overflowX = 'hidden'; + } + + event.preventDefault(); + + const onMouseMove = (e: MouseEvent) => { + if (!dragging) return; + + // Calculate the new editor height based on its starting height, + // plus the delta between the current mouse position and the position when the drag started + const verticalDragDelta = e.clientY - startClientY; + const newEditorHeight = Math.max(MIN_HEIGHT, startHeight + verticalDragDelta); + view.dom.style.height = newEditorHeight + 'px'; + + // Calculate the number of visible lines in the editor based on the current contents + const currentContent = view.state.doc.toString(); + const currentLines = currentContent.split('\n'); + + // Find the last non-empty line in the editor + let lastNonEmptyLine = currentLines.length - 1; + while ( + lastNonEmptyLine >= 0 && + currentLines[lastNonEmptyLine].trim() === '' + ) { + lastNonEmptyLine--; + } + + // Calculate the target number of new lines to create in/remove from the resized editor + const minLines = Math.max(lastNonEmptyLine, MIN_LINES); + const visibleLines = Math.floor(newEditorHeight / LINE_HEIGHT); + const targetLines = Math.max(visibleLines, minLines); + + // If we're expanding the editor, continue to add empty lines + // If we're shrinking the editor, remove empty lines until we reach the last non-empty line + if (currentLines.length < targetLines) { + while (currentLines.length < targetLines) { + currentLines.push(''); + } + } else if (currentLines.length > targetLines) { + while ( + currentLines.length > targetLines && + currentLines[currentLines.length - 1].trim() === '' + ) { + currentLines.pop(); + } + } + + // Only update the editor content if it has actually changed + const newContent = currentLines.join('\n'); + if (newContent !== currentContent) { + view.dispatch({ + changes: { + from: 0, + to: view.state.doc.length, + insert: newContent, + }, + }); + } + + view.requestMeasure(); + }; + + const onMouseUp = () => { + dragging = false; + + if (scroller) { + // Reset the horizontal scrollbar to its default state + scroller.style.overflowX = ''; + } + + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', onMouseUp); + }; + + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + }); + } + + destroy() { + this.resizeHandle.remove(); + } + }, + ); + + return [ + EditorView.theme({ + '&': { + position: 'relative', + resize: 'none', + }, + }), + resizeHandlePlugin, + ]; +} + +export default resizableEditor; From 831a52133ee77b53960c92a149f83d10ba7de5b8 Mon Sep 17 00:00:00 2001 From: David Russell Date: Mon, 30 Dec 2024 14:49:07 +0000 Subject: [PATCH 2/4] remove unnecessary init code --- packages/react-codemirror/src/resizableEditor.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/react-codemirror/src/resizableEditor.tsx b/packages/react-codemirror/src/resizableEditor.tsx index 93eb82e41..ba8b78428 100644 --- a/packages/react-codemirror/src/resizableEditor.tsx +++ b/packages/react-codemirror/src/resizableEditor.tsx @@ -122,12 +122,6 @@ function resizableEditor(): Extension { ); return [ - EditorView.theme({ - '&': { - position: 'relative', - resize: 'none', - }, - }), resizeHandlePlugin, ]; } From 2a7d533a5c8b8b478dae6247dc5cf98a178b2c56 Mon Sep 17 00:00:00 2001 From: David Russell Date: Tue, 31 Dec 2024 10:21:40 +0000 Subject: [PATCH 3/4] simplify resizing to remove line generation, and add aria attributes --- .../react-codemirror/src/resizableEditor.tsx | 58 +++---------------- 1 file changed, 8 insertions(+), 50 deletions(-) diff --git a/packages/react-codemirror/src/resizableEditor.tsx b/packages/react-codemirror/src/resizableEditor.tsx index ba8b78428..d353381d9 100644 --- a/packages/react-codemirror/src/resizableEditor.tsx +++ b/packages/react-codemirror/src/resizableEditor.tsx @@ -2,24 +2,27 @@ import { Extension } from '@codemirror/state'; import { EditorView, ViewPlugin } from '@codemirror/view'; const MIN_HEIGHT = 27.4; -const LINE_HEIGHT = 20; const DRAG_HANDLE_SIZE = 14; -const MIN_LINES = 1; function resizableEditor(): Extension { const resizeHandlePlugin = ViewPlugin.fromClass( class { - private resizeHandle: HTMLDivElement; + private resizeHandle: HTMLButtonElement; constructor(view: EditorView) { const scroller = view.scrollDOM; const handleOffset = view.dom.clientWidth / 2 - DRAG_HANDLE_SIZE; // Create a resize handle using the raw NDL icon svg - this.resizeHandle = document.createElement('div'); + this.resizeHandle = document.createElement('button'); this.resizeHandle.innerHTML = ''; this.resizeHandle.className = 'cm-resize-handle'; + this.resizeHandle.setAttribute('aria-label', 'Resize editor height'); + this.resizeHandle.setAttribute('role', 'separator'); + this.resizeHandle.setAttribute('aria-orientation', 'horizontal'); + this.resizeHandle.setAttribute('aria-valuemin', MIN_HEIGHT.toString()); + this.resizeHandle.setAttribute('aria-valuenow', view.dom.offsetHeight.toString()); this.resizeHandle.style.position = 'absolute'; this.resizeHandle.style.bottom = '0'; this.resizeHandle.style.cursor = 'row-resize'; @@ -49,52 +52,7 @@ function resizableEditor(): Extension { const verticalDragDelta = e.clientY - startClientY; const newEditorHeight = Math.max(MIN_HEIGHT, startHeight + verticalDragDelta); view.dom.style.height = newEditorHeight + 'px'; - - // Calculate the number of visible lines in the editor based on the current contents - const currentContent = view.state.doc.toString(); - const currentLines = currentContent.split('\n'); - - // Find the last non-empty line in the editor - let lastNonEmptyLine = currentLines.length - 1; - while ( - lastNonEmptyLine >= 0 && - currentLines[lastNonEmptyLine].trim() === '' - ) { - lastNonEmptyLine--; - } - - // Calculate the target number of new lines to create in/remove from the resized editor - const minLines = Math.max(lastNonEmptyLine, MIN_LINES); - const visibleLines = Math.floor(newEditorHeight / LINE_HEIGHT); - const targetLines = Math.max(visibleLines, minLines); - - // If we're expanding the editor, continue to add empty lines - // If we're shrinking the editor, remove empty lines until we reach the last non-empty line - if (currentLines.length < targetLines) { - while (currentLines.length < targetLines) { - currentLines.push(''); - } - } else if (currentLines.length > targetLines) { - while ( - currentLines.length > targetLines && - currentLines[currentLines.length - 1].trim() === '' - ) { - currentLines.pop(); - } - } - - // Only update the editor content if it has actually changed - const newContent = currentLines.join('\n'); - if (newContent !== currentContent) { - view.dispatch({ - changes: { - from: 0, - to: view.state.doc.length, - insert: newContent, - }, - }); - } - + this.resizeHandle.setAttribute('aria-valuenow', newEditorHeight.toString()); view.requestMeasure(); }; From 4f9986f78149104a9c8f52102fa2ff698f5b6919 Mon Sep 17 00:00:00 2001 From: David Russell Date: Tue, 31 Dec 2024 10:37:56 +0000 Subject: [PATCH 4/4] update api of cypher editor to expose resizeable prop --- .../react-codemirror-playground/src/App.tsx | 6 +-- .../react-codemirror/src/CypherEditor.tsx | 51 +++++++++++-------- packages/react-codemirror/src/neo4jSetup.tsx | 6 ++- 3 files changed, 39 insertions(+), 24 deletions(-) diff --git a/packages/react-codemirror-playground/src/App.tsx b/packages/react-codemirror-playground/src/App.tsx index f39abbdd1..175a1b204 100644 --- a/packages/react-codemirror-playground/src/App.tsx +++ b/packages/react-codemirror-playground/src/App.tsx @@ -88,9 +88,8 @@ export function App() {