diff --git a/packages/grafana-ui/src/slate-plugins/braces.test.tsx b/packages/grafana-ui/src/slate-plugins/braces.test.tsx index a56c34d0485..ecd55b7f503 100644 --- a/packages/grafana-ui/src/slate-plugins/braces.test.tsx +++ b/packages/grafana-ui/src/slate-plugins/braces.test.tsx @@ -18,7 +18,7 @@ describe('braces', () => { const value = Plain.deserialize(''); const editor = shallow(); const event = new window.KeyboardEvent('keydown', { key: '(' }); - handler(event as Event, editor.instance() as any, nextMock); + expect(handler(event as Event, editor.instance() as any, nextMock)).toBeTruthy(); expect(Plain.serialize(editor.instance().value)).toEqual('()'); }); @@ -37,4 +37,24 @@ describe('braces', () => { const handled = handler(event as Event, editor.instance().moveForward(5) as any, nextMock); expect(handled).toBeFalsy(); }); + + it('overrides an automatically inserted brace', () => { + const value = Plain.deserialize(''); + const editor = shallow(); + const opening = new window.KeyboardEvent('keydown', { key: '(' }); + expect(handler(opening as Event, editor.instance() as any, nextMock)).toBeTruthy(); + const closing = new window.KeyboardEvent('keydown', { key: ')' }); + expect(handler(closing as Event, editor.instance() as any, nextMock)).toBeTruthy(); + expect(Plain.serialize(editor.instance().value)).toEqual('()'); + }); + + it.skip('does not override manually inserted braces', () => { + const value = Plain.deserialize(''); + const editor = shallow(); + const event1 = new window.KeyboardEvent('keydown', { key: ')' }); + expect(handler(event1 as Event, editor.instance() as any, nextMock)).toBeFalsy(); + const event2 = new window.KeyboardEvent('keydown', { key: ')' }); + expect(handler(event2 as Event, editor.instance().moveBackward(1) as any, nextMock)).toBeFalsy(); + expect(Plain.serialize(editor.instance().value)).toEqual('))'); + }); }); diff --git a/packages/grafana-ui/src/slate-plugins/braces.ts b/packages/grafana-ui/src/slate-plugins/braces.ts index 2ae602b962d..7f8f550aa17 100644 --- a/packages/grafana-ui/src/slate-plugins/braces.ts +++ b/packages/grafana-ui/src/slate-plugins/braces.ts @@ -1,5 +1,5 @@ import { Plugin } from '@grafana/slate-react'; -import { Editor as CoreEditor } from 'slate'; +import { Editor as CoreEditor, Annotation } from 'slate'; const BRACES: any = { '[': ']', @@ -7,6 +7,8 @@ const BRACES: any = { '(': ')', }; +const MATCH_MARK = 'brace_match'; + export function BracesPlugin(): Plugin { return { onKeyDown(event: Event, editor: CoreEditor, next: Function) { @@ -17,7 +19,6 @@ export function BracesPlugin(): Plugin { case '(': case '{': case '[': { - keyEvent.preventDefault(); const { start: { offset: startOffset, key: startKey }, end: { offset: endOffset, key: endKey }, @@ -27,21 +28,67 @@ export function BracesPlugin(): Plugin { // If text is selected, wrap selected text in parens if (value.selection.isExpanded) { + keyEvent.preventDefault(); editor .insertTextByKey(startKey, startOffset, keyEvent.key) .insertTextByKey(endKey, endOffset + 1, BRACES[keyEvent.key]) .moveEndBackward(1); + return true; } else if ( + // Insert matching brace when there is no input after caret focusOffset === text.length || text[focusOffset] === ' ' || Object.values(BRACES).includes(text[focusOffset]) ) { - editor.insertText(`${keyEvent.key}${BRACES[keyEvent.key]}`).moveBackward(1); - } else { - editor.insertText(keyEvent.key); - } + keyEvent.preventDefault(); + const complement = BRACES[keyEvent.key]; + const matchAnnotation = { + key: `${MATCH_MARK}-${Date.now()}`, + type: `${MATCH_MARK}-${complement}`, + anchor: { + key: startKey, + offset: startOffset, + object: 'point', + }, + focus: { + key: endKey, + offset: endOffset + 1, + object: 'point', + }, + object: 'annotation', + } as Annotation; + editor + .insertText(keyEvent.key) + .insertText(complement) + .addAnnotation(matchAnnotation) + .moveBackward(1); - return true; + return true; + } + break; + } + + case ')': + case '}': + case ']': { + const text = value.anchorText.text; + const offset = value.selection.anchor.offset; + const nextChar = text[offset]; + // Handle closing brace when it's already the next character + const complement = keyEvent.key; + const annotationType = `${MATCH_MARK}-${complement}`; + const annotation = value.annotations.find( + a => a?.type === annotationType && a.anchor.key === value.anchorText.key + ); + if (annotation && nextChar === complement && !value.selection.isExpanded) { + keyEvent.preventDefault(); + editor + .moveFocusForward(1) + .removeAnnotation(annotation) + .moveAnchorForward(1); + return true; + } + break; } case 'Backspace': {