Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[prototype] Resizable editor #319

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/react-codemirror-playground/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,8 @@ export function App() {
<button
key={demoName}
className={`hover:bg-blue-600 text-white font-bold py-1 px-3 rounded
${
selectedDemoName === demoName ? 'bg-blue-600' : 'bg-blue-400'
}`}
${selectedDemoName === demoName ? 'bg-blue-600' : 'bg-blue-400'
}`}
onClick={() => {
setSelectedDemoName(demoName);
setValue(demos[demoName]);
Expand All @@ -114,6 +113,7 @@ export function App() {
schema={schema}
featureFlags={{ signatureInfoOnAutoCompletions: true }}
ariaLabel="Cypher Editor"
resizeable={true}
/>

{commandRanCount > 0 && (
Expand Down
51 changes: 31 additions & 20 deletions packages/react-codemirror/src/CypherEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,16 @@ export interface CypherEditorProps {
* @default false
*/
moveFocusOnTab?: boolean;

/**
* Whether the editor should be resizable.
*
* true will initialize the editor with the resizable editor extension,
* enabling the editor to be resized dynamically.
*
* @default false
*/
resizeable?: boolean;
}

const executeKeybinding = (
Expand Down Expand Up @@ -321,15 +331,16 @@ export class CypherEditor extends Component<
lineNumbers: true,
newLineOnEnter: false,
moveFocusOnTab: false,
resizeable: false,
};

private debouncedOnChange = this.props.onChange
? debounce(
((value, viewUpdate) => {
this.props.onChange(value, viewUpdate);
}) satisfies CypherEditorProps['onChange'],
DEBOUNCE_TIME,
)
((value, viewUpdate) => {
this.props.onChange(value, viewUpdate);
}) satisfies CypherEditorProps['onChange'],
DEBOUNCE_TIME,
)
: undefined;

componentDidMount(): void {
Expand Down Expand Up @@ -369,18 +380,18 @@ export class CypherEditor extends Component<

const changeListener = this.debouncedOnChange
? [
EditorView.updateListener.of((upt: ViewUpdate) => {
const wasUserEdit = !upt.transactions.some((tr) =>
tr.annotation(ExternalEdit),
);

if (upt.docChanged && wasUserEdit) {
const doc = upt.state.doc;
const value = doc.toString();
this.debouncedOnChange(value, upt);
}
}),
]
EditorView.updateListener.of((upt: ViewUpdate) => {
const wasUserEdit = !upt.transactions.some((tr) =>
tr.annotation(ExternalEdit),
);

if (upt.docChanged && wasUserEdit) {
const doc = upt.state.doc;
const value = doc.toString();
this.debouncedOnChange(value, upt);
}
}),
]
: [];

this.editorState.current = EditorState.create({
Expand Down Expand Up @@ -416,8 +427,8 @@ export class CypherEditor extends Component<
),
this.props.ariaLabel
? EditorView.contentAttributes.of({
'aria-label': this.props.ariaLabel,
})
'aria-label': this.props.ariaLabel,
})
: [],
],
doc: this.props.value,
Expand Down Expand Up @@ -465,7 +476,7 @@ export class CypherEditor extends Component<
const didChangeTheme =
prevProps.theme !== this.props.theme ||
prevProps.overrideThemeBackgroundColor !==
this.props.overrideThemeBackgroundColor;
this.props.overrideThemeBackgroundColor;

if (didChangeTheme) {
this.editorView.current.dispatch({
Expand Down
7 changes: 7 additions & 0 deletions packages/react-codemirror/src/neo4jSetup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -63,10 +64,12 @@ const insertTab: StateCommand = (cmd) => {

type SetupProps = {
moveFocusOnTab?: boolean;
resizeable?: boolean;
};

export const basicNeo4jSetup = ({
moveFocusOnTab = false,
resizeable = false
}: SetupProps): Extension[] => {
const keymaps: KeyBinding[] = [
closeBracketsKeymap,
Expand Down Expand Up @@ -154,6 +157,10 @@ export const basicNeo4jSetup = ({
]),
);

if (resizeable) {
extensions.push(resizableEditor())
}

return extensions;
};

Expand Down
87 changes: 87 additions & 0 deletions packages/react-codemirror/src/resizableEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Extension } from '@codemirror/state';
import { EditorView, ViewPlugin } from '@codemirror/view';

const MIN_HEIGHT = 27.4;
const DRAG_HANDLE_SIZE = 14;

function resizableEditor(): Extension {
const resizeHandlePlugin = ViewPlugin.fromClass(
class {
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('button');
this.resizeHandle.innerHTML =
'<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8.66667 6H9.33333C9.70152 6 10 6.29848 10 6.66667V7.33333C10 7.70152 9.70152 8 9.33333 8H8.66667C8.29848 8 8 7.70152 8 7.33333V6.66667C8 6.29848 8.29848 6 8.66667 6Z" fill="currentColor"></path><path d="M14.6667 6H15.3333C15.7015 6 16 6.29848 16 6.66667V7.33333C16 7.70152 15.7015 8 15.3333 8H14.6667C14.2985 8 14 7.70152 14 7.33333V6.66667C14 6.29848 14.2985 6 14.6667 6Z" fill="currentColor"></path><path d="M15.3333 16H14.6667C14.2985 16 14 16.2985 14 16.6667V17.3333C14 17.7015 14.2985 18 14.6667 18H15.3333C15.7015 18 16 17.7015 16 17.3333V16.6667C16 16.2985 15.7015 16 15.3333 16Z" fill="currentColor"></path><path d="M8.66667 11H9.33333C9.70152 11 10 11.2985 10 11.6667V12.3333C10 12.7015 9.70152 13 9.33333 13H8.66667C8.29848 13 8 12.7015 8 12.3333V11.6667C8 11.2985 8.29848 11 8.66667 11Z" fill="currentColor"></path><path d="M15.3333 11H14.6667C14.2985 11 14 11.2985 14 11.6667V12.3333C14 12.7015 14.2985 13 14.6667 13H15.3333C15.7015 13 16 12.7015 16 12.3333V11.6667C16 11.2985 15.7015 11 15.3333 11Z" fill="currentColor"></path><path d="M8.66667 16H9.33333C9.70152 16 10 16.2985 10 16.6667V17.3333C10 17.7015 9.70152 18 9.33333 18H8.66667C8.29848 18 8 17.7015 8 17.3333V16.6667C8 16.2985 8.29848 16 8.66667 16Z" fill="currentColor"></path></svg>';
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';
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';
this.resizeHandle.setAttribute('aria-valuenow', newEditorHeight.toString());
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 [
resizeHandlePlugin,
];
}

export default resizableEditor;
Loading