joy-query-box
Version:
A React component for building and parsing SQL-like queries
150 lines (131 loc) • 4.47 kB
JSX
import React, { useRef, useEffect, useMemo, useCallback } from 'react';
import PropTypes from 'prop-types';
import * as PEG from 'pegjs';
import grammar from './gramma.pegjs';
import 'brace';
import 'brace/theme/tomorrow';
import "brace/snippets/text";
import 'brace/ext/language_tools';
import SimpleQueryMode from './simpleQuery.mode';
import debounce from 'lodash/debounce';
const baseSuggestion = [
];
const pegparser = PEG.generate(grammar);
const QueryBox = ({ id, className, onSearch, queryText, words }) => {
const aceEditor = useRef(null);
const editorInstance = useRef(null);
const previousQueryText = useRef(queryText);
const editorId = useMemo(() => id || Date.now().toString(16), [id]);
const suggestions = useMemo(() =>
baseSuggestion.concat((words || []).map(({ word, desc }) => ({
caption: word,
value: word,
meta: desc
}))),
[words]
);
const customCompleter = useMemo(() => ({
getCompletions: (editor, session, pos, prefix, callback) => {
callback(null, suggestions);
}
}), [suggestions]);
const handleQueryChange = useCallback(
debounce((val) => {
let err = null;
let parsed = null;
let freetext = '';
try {
freetext = val.trim();
if (!freetext) {
onSearch(null, {}, '');
return;
}
parsed = pegparser.parse(freetext);
} catch (e) {
err = e;
console.error('Query parsing error:', e);
} finally {
onSearch(err, parsed, freetext);
}
}, 300),
[onSearch]
);
useEffect(() => {
if (!aceEditor.current) return;
editorInstance.current = window.ace.edit(aceEditor.current);
editorInstance.current.$blockScrolling = true;
const session = editorInstance.current.getSession();
session.setMode(new SimpleQueryMode());
session.setUseWrapMode(true);
const langTools = window.ace.acequire('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
});
editorInstance.current.commands.addCommand({
name: 'submit-query',
bindKey: {
mac: "Enter",
win: "Enter"
},
exec: editor => handleQueryChange(editor.getSession().getValue())
});
if (queryText) {
session.setValue(queryText);
handleQueryChange(queryText);
}
return () => {
if (editorInstance.current) {
editorInstance.current.destroy();
editorInstance.current = null;
}
};
}, [customCompleter, handleQueryChange, queryText]);
useEffect(() => {
if (editorInstance.current && queryText !== previousQueryText.current) {
const session = editorInstance.current.getSession();
const currentValue = session.getValue();
if (queryText !== currentValue) {
session.setValue(queryText || '');
handleQueryChange(queryText || '');
previousQueryText.current = queryText;
}
}
}, [queryText, handleQueryChange]);
return (
<div
className={className || 'flex-fill bg-white border py-2'}
role="search"
aria-label="Query input"
>
<div
ref={aceEditor}
id={editorId}
tabIndex="0"
/>
</div>
);
};
QueryBox.propTypes = {
id: PropTypes.string,
className: PropTypes.string,
onSearch: PropTypes.func.isRequired,
queryText: PropTypes.string,
words: PropTypes.arrayOf(
PropTypes.shape({
word: PropTypes.string.isRequired,
desc: PropTypes.string.isRequired
})
)
};
export default QueryBox;