UNPKG

joy-query-box

Version:

A React component for building and parsing SQL-like queries

146 lines (124 loc) 4.76 kB
import { useRef, useEffect, useMemo } from 'react'; import { QuerySuggestion } from '../QueryBox.types'; import { SimpleQueryMode } from '../../../utils/editor/simpleQueryMode'; import ace from 'ace-builds'; import 'ace-builds/src-noconflict/theme-tomorrow'; import 'ace-builds/src-noconflict/ext-language_tools'; import 'ace-builds/src-noconflict/mode-text'; declare global { interface Window { ace: typeof ace; } } // Make ace available globally window.ace = ace; interface EditorSetupProps { words?: QuerySuggestion[]; onQueryChange: (value: string) => void; initialValue?: string; } interface EditorInstance { editor: any; setEditorValue: (value: string) => void; } const operatorSuggestions = [ { caption: 'between', value: 'between', meta: 'Range check operator', score: 1000 }, { caption: 'is', value: 'is', meta: 'Boolean check operator', score: 1000 }, { caption: 'in', value: 'in', meta: 'Set membership operator', score: 1000 }, { caption: 'like', value: 'like', meta: 'Pattern match operator', score: 1000 }, { caption: 'contains', value: 'contains', meta: 'Text search operator', score: 1000 }, { caption: 'startwith', value: 'startwith', meta: 'Text prefix operator', score: 1000 } ]; export const useEditorSetup = ({ words, onQueryChange, initialValue }: EditorSetupProps): EditorInstance => { const aceEditor = useRef<HTMLDivElement>(null); const editorInstance = useRef<any>(null); const suggestions = useMemo(() => [ ...(words || []).map(({ word, desc }) => ({ caption: word, value: word, meta: desc, score: 100 })), ...operatorSuggestions ], [words]); const customCompleter = useMemo(() => ({ getCompletions: (_editor: any, session: any, pos: any, prefix: string, callback: Function) => { // Get the current line's text const line = session.getLine(pos.row); const cursorPosition = pos.column; // Check if we're after a field name by looking for spaces before the cursor const beforeCursor = line.substring(0, cursorPosition); const isAfterField = /\w+\s+$/.test(beforeCursor); // If we're after a field name, prioritize operators const filteredSuggestions = isAfterField ? suggestions.filter(s => operatorSuggestions.some(op => op.caption === s.caption)) : suggestions; callback(null, filteredSuggestions); } }), [suggestions]); useEffect(() => { if (!aceEditor.current) return; // Initialize editor editorInstance.current = ace.edit(aceEditor.current); editorInstance.current.$blockScrolling = true; const session = editorInstance.current.getSession(); // Create and set the mode const Mode = ace.require('ace/mode/text').Mode; const customMode = new Mode(); customMode.HighlightRules = SimpleQueryMode; session.setMode(customMode); session.setUseWrapMode(true); // Configure editor const langTools = ace.require('ace/ext/language_tools'); langTools.addCompleter(customCompleter); editorInstance.current.setOptions({ maxLines: 1, minLines: 1, autoScrollEditorIntoView: true, enableBasicAutocompletion: true, enableLiveAutocompletion: true, enableSnippets: true, highlightActiveLine: false, printMargin: false, showGutter: false, theme: 'ace/theme/tomorrow', fontSize: 13 }); // Add command for query submission editorInstance.current.commands.addCommand({ name: 'submit-query', bindKey: { mac: 'Enter', win: 'Enter' }, exec: (editor: any) => onQueryChange(editor.getSession().getValue()) }); // Set initial value if (initialValue) { session.setValue(initialValue); onQueryChange(initialValue); } // Cleanup return () => { if (editorInstance.current) { editorInstance.current.destroy(); editorInstance.current = null; } }; }, [customCompleter, onQueryChange, initialValue]); const setEditorValue = (value: string) => { if (editorInstance.current) { const session = editorInstance.current.getSession(); session.setValue(value); onQueryChange(value); } }; return { editor: aceEditor, setEditorValue }; };