@abaktiar/ql-input
Version:
React QL input component with intelligent autocomplete, parameterized functions, and query validation. Framework-independent with zero external dependencies.
1,486 lines • 56.7 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import { jsxs, jsx } from "react/jsx-runtime";
import * as React from "react";
import { useState, useRef, useCallback, useEffect } from "react";
class QLExpressionParser {
constructor() {
__publicField(this, "tokens", []);
__publicField(this, "position", 0);
}
parse(tokens) {
this.tokens = tokens.filter((t) => t.type !== "whitespace");
this.position = 0;
if (this.tokens.length === 0) {
return null;
}
const hasNonKeywordTokens = this.tokens.some((token) => token.type !== "keyword" && token.type !== "whitespace");
if (!hasNonKeywordTokens) {
return null;
}
return this.parseExpression();
}
parseExpression() {
return this.parseOrExpression();
}
parseOrExpression() {
var _a, _b;
let left = this.parseAndExpression();
while (((_a = this.current()) == null ? void 0 : _a.type) === "logical" && ((_b = this.current()) == null ? void 0 : _b.value) === "OR") {
this.advance();
const right = this.parseAndExpression();
if (this.isLogicalGroup(left) && left.operator === "OR") {
left.conditions.push(right);
} else {
left = {
operator: "OR",
conditions: [left, right]
};
}
}
return left;
}
parseAndExpression() {
var _a, _b;
let left = this.parsePrimaryExpression();
while (((_a = this.current()) == null ? void 0 : _a.type) === "logical" && ((_b = this.current()) == null ? void 0 : _b.value) === "AND") {
this.advance();
const right = this.parsePrimaryExpression();
if (this.isLogicalGroup(left) && left.operator === "AND") {
left.conditions.push(right);
} else {
left = {
operator: "AND",
conditions: [left, right]
};
}
}
return left;
}
parsePrimaryExpression() {
var _a, _b;
const token = this.current();
if (!token) {
throw new Error("Unexpected end of input");
}
if (token.type === "logical" && token.value === "NOT") {
this.advance();
const expr = this.parsePrimaryExpression();
if ("operator" in expr && "conditions" in expr) {
return { ...expr, not: true };
} else {
return { ...expr, not: true };
}
}
if (token.type === "parenthesis" && token.value === "(") {
this.advance();
const expr = this.parseExpression();
if (((_a = this.current()) == null ? void 0 : _a.type) !== "parenthesis" || ((_b = this.current()) == null ? void 0 : _b.value) !== ")") {
throw new Error("Expected closing parenthesis");
}
this.advance();
return expr;
}
if (token.type === "field") {
return this.parseCondition();
}
throw new Error(`Unexpected token: ${token.type} "${token.value}"`);
}
parseCondition() {
const fieldToken = this.current();
if (!fieldToken || fieldToken.type !== "field") {
throw new Error("Expected field name");
}
const condition = {
field: fieldToken.value
};
this.advance();
const operatorToken = this.current();
if (!operatorToken || operatorToken.type !== "operator") {
throw new Error("Expected operator");
}
condition.operator = operatorToken.value;
this.advance();
if (this.operatorRequiresValue(condition.operator)) {
condition.value = this.parseValue(condition.operator);
}
return condition;
}
parseValue(operator) {
if (operator === "IN" || operator === "NOT IN") {
return this.parseInList();
}
const token = this.current();
if (!token) {
throw new Error("Expected value");
}
if (token.type === "value" || token.type === "field" || token.type === "function" || token.type === "unknown") {
let value = token.value;
const nextToken = this.tokens[this.position + 1];
if (nextToken && nextToken.type === "parenthesis" && nextToken.value === "(") {
return this.parseFunctionCall(value);
}
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
value = value.slice(1, -1);
}
this.advance();
return value;
}
throw new Error(`Expected value, got ${token.type} "${token.value}"`);
}
parseFunctionCall(functionName) {
var _a, _b, _c, _d, _e;
this.advance();
this.advance();
const parameters = [];
while (this.current() && !(((_a = this.current()) == null ? void 0 : _a.type) === "parenthesis" && ((_b = this.current()) == null ? void 0 : _b.value) === ")")) {
const token = this.current();
if ((token == null ? void 0 : token.type) === "value" || (token == null ? void 0 : token.type) === "field" || (token == null ? void 0 : token.type) === "function" || (token == null ? void 0 : token.type) === "unknown") {
let paramValue = token.value;
const nextToken = this.tokens[this.position + 1];
if (nextToken && nextToken.type === "parenthesis" && nextToken.value === "(") {
paramValue = this.parseFunctionCall(paramValue);
} else {
if (paramValue.startsWith('"') && paramValue.endsWith('"') || paramValue.startsWith("'") && paramValue.endsWith("'")) {
paramValue = paramValue.slice(1, -1);
}
this.advance();
}
parameters.push(paramValue);
if (((_c = this.current()) == null ? void 0 : _c.type) === "comma") {
this.advance();
}
} else {
throw new Error(`Unexpected token in function parameters: ${token == null ? void 0 : token.type} "${token == null ? void 0 : token.value}"`);
}
}
if (((_d = this.current()) == null ? void 0 : _d.type) !== "parenthesis" || ((_e = this.current()) == null ? void 0 : _e.value) !== ")") {
throw new Error("Expected closing parenthesis for function call");
}
this.advance();
if (parameters.length === 0) {
return `${functionName}()`;
} else {
return `${functionName}(${parameters.join(", ")})`;
}
}
parseInList() {
var _a, _b, _c, _d, _e, _f, _g;
if (((_a = this.current()) == null ? void 0 : _a.type) !== "parenthesis" || ((_b = this.current()) == null ? void 0 : _b.value) !== "(") {
throw new Error("Expected opening parenthesis for IN list");
}
this.advance();
const values = [];
while (this.current() && !(((_c = this.current()) == null ? void 0 : _c.type) === "parenthesis" && ((_d = this.current()) == null ? void 0 : _d.value) === ")")) {
const token = this.current();
if ((token == null ? void 0 : token.type) === "value" || (token == null ? void 0 : token.type) === "field" || (token == null ? void 0 : token.type) === "function" || (token == null ? void 0 : token.type) === "unknown") {
let value = token.value;
const nextToken = this.tokens[this.position + 1];
if (nextToken && nextToken.type === "parenthesis" && nextToken.value === "(") {
value = this.parseFunctionCall(value);
} else {
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
value = value.slice(1, -1);
}
this.advance();
}
values.push(value);
if (((_e = this.current()) == null ? void 0 : _e.type) === "comma") {
this.advance();
}
} else {
throw new Error(`Unexpected token in IN list: ${token == null ? void 0 : token.type} "${token == null ? void 0 : token.value}"`);
}
}
if (((_f = this.current()) == null ? void 0 : _f.type) !== "parenthesis" || ((_g = this.current()) == null ? void 0 : _g.value) !== ")") {
throw new Error("Expected closing parenthesis for IN list");
}
this.advance();
return values;
}
operatorRequiresValue(operator) {
return !["IS EMPTY", "IS NOT EMPTY"].includes(operator);
}
current() {
return this.tokens[this.position];
}
advance() {
this.position++;
}
isLogicalGroup(expr) {
return "operator" in expr && "conditions" in expr;
}
}
const LOGICAL_OPERATORS = ["AND", "OR", "NOT"];
const COMPARISON_OPERATORS = ["=", "!=", ">", "<", ">=", "<="];
const TEXT_OPERATORS = ["~", "!~"];
const LIST_OPERATORS = ["IN", "NOT IN"];
const NULL_OPERATORS = ["IS EMPTY", "IS NOT EMPTY"];
const ALL_OPERATORS = [
...COMPARISON_OPERATORS,
...TEXT_OPERATORS,
...LIST_OPERATORS,
...NULL_OPERATORS
];
const KEYWORDS = ["ORDER", "BY", "ASC", "DESC"];
class QLParser {
constructor(_config) {
}
/**
* Tokenize the input string into QL tokens
*/
tokenize(input) {
const tokens = [];
let position = 0;
while (position < input.length) {
const char = input[position];
const remaining = input.slice(position);
if (/\s/.test(char)) {
const start2 = position;
while (position < input.length && /\s/.test(input[position])) {
position++;
}
tokens.push({
type: "whitespace",
value: input.slice(start2, position),
start: start2,
end: position
});
continue;
}
let matched = false;
if (char === '"' || char === "'") {
const quote = char;
const start2 = position;
position++;
while (position < input.length && input[position] !== quote) {
if (input[position] === "\\") {
position += 2;
} else {
position++;
}
}
if (position < input.length) {
position++;
}
tokens.push({
type: "value",
value: input.slice(start2, position),
start: start2,
end: position
});
continue;
}
if (char === "(" || char === ")") {
tokens.push({
type: "parenthesis",
value: char,
start: position,
end: position + 1
});
position++;
continue;
}
if (char === ",") {
tokens.push({
type: "comma",
value: char,
start: position,
end: position + 1
});
position++;
continue;
}
if (["=", "!", ">", "<", "~"].includes(char)) {
const start2 = position;
let operator = char;
if (position + 1 < input.length) {
const nextChar = input[position + 1];
if (char === "!" && nextChar === "=" || char === ">" && nextChar === "=" || char === "<" && nextChar === "=" || char === "!" && nextChar === "~") {
operator += nextChar;
position++;
}
}
tokens.push({
type: "operator",
value: operator,
start: start2,
end: position + 1
});
position++;
continue;
}
for (const op of ALL_OPERATORS.sort((a, b) => b.length - a.length)) {
if (remaining.toUpperCase().startsWith(op)) {
const nextChar = remaining[op.length];
if (!nextChar || /\s/.test(nextChar) || nextChar === "(" || nextChar === ")") {
tokens.push({
type: "operator",
value: op,
// Always store in uppercase for consistency
start: position,
end: position + op.length
});
position += op.length;
matched = true;
break;
}
}
}
if (matched) continue;
for (const op of LOGICAL_OPERATORS) {
if (remaining.toUpperCase().startsWith(op)) {
const nextChar = remaining[op.length];
if (!nextChar || /\s/.test(nextChar) || nextChar === "(" || nextChar === ")") {
tokens.push({
type: "logical",
value: op,
// Always store in uppercase for consistency
start: position,
end: position + op.length
});
position += op.length;
matched = true;
break;
}
}
}
if (matched) continue;
for (const keyword of KEYWORDS) {
if (remaining.toUpperCase().startsWith(keyword)) {
const nextChar = remaining[keyword.length];
if (!nextChar || /\s/.test(nextChar) || nextChar === "(" || nextChar === ")") {
tokens.push({
type: "keyword",
value: keyword,
// Always store in uppercase for consistency
start: position,
end: position + keyword.length
});
position += keyword.length;
matched = true;
break;
}
}
}
if (matched) continue;
const start = position;
while (position < input.length && !/\s/.test(input[position]) && input[position] !== "(" && input[position] !== ")" && input[position] !== "," && input[position] !== '"' && input[position] !== "'") {
position++;
}
if (position > start) {
const value = input.slice(start, position);
tokens.push({
type: "unknown",
// Will be classified later
value,
start,
end: position
});
} else {
position++;
}
}
return this.classifyTokens(tokens);
}
/**
* Classify unknown tokens based on context
*/
classifyTokens(tokens) {
const context = {
expectingField: true,
expectingOperator: false,
expectingValue: false,
expectingLogical: false,
parenthesesLevel: 0,
inOrderBy: false
};
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.type === "whitespace") continue;
if (token.type === "unknown") {
if (context.expectingField) {
token.type = "field";
context.expectingField = false;
context.expectingOperator = true;
} else if (context.expectingValue) {
token.type = "value";
context.expectingValue = false;
context.expectingLogical = true;
} else {
token.type = "field";
}
}
if (token.type === "field") {
context.expectingOperator = true;
context.expectingField = false;
context.expectingValue = false;
context.expectingLogical = false;
} else if (token.type === "operator") {
context.expectingValue = true;
context.expectingField = false;
context.expectingOperator = false;
context.expectingLogical = false;
} else if (token.type === "value") {
context.expectingLogical = true;
context.expectingField = false;
context.expectingOperator = false;
context.expectingValue = false;
} else if (token.type === "logical") {
context.expectingField = true;
context.expectingOperator = false;
context.expectingValue = false;
context.expectingLogical = false;
} else if (token.type === "parenthesis") {
if (token.value === "(") {
context.parenthesesLevel++;
context.expectingField = true;
context.expectingOperator = false;
context.expectingValue = false;
context.expectingLogical = false;
} else {
context.parenthesesLevel--;
context.expectingLogical = true;
context.expectingField = false;
context.expectingOperator = false;
context.expectingValue = false;
}
} else if (token.type === "keyword") {
if (token.value.toUpperCase() === "ORDER") {
context.inOrderBy = true;
context.expectingField = false;
context.expectingOperator = false;
context.expectingValue = false;
context.expectingLogical = false;
} else if (token.value.toUpperCase() === "BY") {
context.expectingField = true;
context.expectingOperator = false;
context.expectingValue = false;
context.expectingLogical = false;
}
}
}
return tokens;
}
/**
* Parse tokens into a QL query object
*/
parse(input) {
const errors = [];
let whereExpression = void 0;
let orderBy = [];
try {
const { whereClause, orderByClause } = this.splitQuery(input);
if (whereClause.trim()) {
const tokens = this.tokenize(whereClause);
const expressionParser = new QLExpressionParser();
const result = expressionParser.parse(tokens);
whereExpression = result || void 0;
}
if (orderByClause.trim()) {
orderBy = this.parseOrderBy(orderByClause);
}
} catch (error) {
errors.push(error instanceof Error ? error.message : "Parse error");
}
return {
where: whereExpression || void 0,
orderBy,
raw: input,
valid: errors.length === 0,
errors
};
}
splitQuery(input) {
const orderByMatch = input.match(/\s*ORDER\s+BY\s+/i);
if (!orderByMatch) {
return { whereClause: input, orderByClause: "" };
}
const orderByIndex = orderByMatch.index + orderByMatch[0].length;
const whereClause = input.substring(0, orderByMatch.index);
const orderByClause = input.substring(orderByIndex);
return { whereClause, orderByClause };
}
parseOrderBy(orderByClause) {
const orderByItems = [];
const items = orderByClause.split(",").map((item) => item.trim());
for (const item of items) {
const parts = item.trim().split(/\s+/);
if (parts.length === 0 || !parts[0]) {
continue;
}
const field = parts[0];
const direction = parts.length > 1 && parts[1].toUpperCase() === "DESC" ? "DESC" : "ASC";
orderByItems.push({
field,
direction
});
}
return orderByItems;
}
}
function isCondition(expr) {
return "field" in expr && "operator" in expr;
}
function isLogicalGroup(expr) {
return "operator" in expr && "conditions" in expr;
}
function toMongooseQuery(expr) {
if (isCondition(expr)) {
return conditionToMongoDB(expr);
} else if (isLogicalGroup(expr)) {
return logicalGroupToMongoDB(expr);
}
return {};
}
function conditionToMongoDB(condition) {
const { field, operator, value } = condition;
switch (operator) {
case "=":
return { [field]: value };
case "!=":
return { [field]: { $ne: value } };
case ">":
return { [field]: { $gt: value } };
case "<":
return { [field]: { $lt: value } };
case ">=":
return { [field]: { $gte: value } };
case "<=":
return { [field]: { $lte: value } };
case "IN":
return { [field]: { $in: Array.isArray(value) ? value : [value] } };
case "NOT IN":
return { [field]: { $nin: Array.isArray(value) ? value : [value] } };
case "~":
return { [field]: { $regex: value, $options: "i" } };
case "!~":
return { [field]: { $not: { $regex: value, $options: "i" } } };
case "IS EMPTY":
return { $or: [{ [field]: null }, { [field]: { $exists: false } }] };
case "IS NOT EMPTY":
return { [field]: { $ne: null, $exists: true } };
default:
return { [field]: value };
}
}
function logicalGroupToMongoDB(group) {
const conditions = group.conditions.map(toMongooseQuery);
if (group.operator === "AND") {
return { $and: conditions };
} else if (group.operator === "OR") {
return { $or: conditions };
}
return {};
}
function toSQLQuery(expr) {
if (isCondition(expr)) {
return conditionToSQL(expr);
} else if (isLogicalGroup(expr)) {
return logicalGroupToSQL(expr);
}
return "";
}
function conditionToSQL(condition) {
const { field, operator, value } = condition;
switch (operator) {
case "=":
return `${field} = '${value}'`;
case "!=":
return `${field} != '${value}'`;
case ">":
return `${field} > '${value}'`;
case "<":
return `${field} < '${value}'`;
case ">=":
return `${field} >= '${value}'`;
case "<=":
return `${field} <= '${value}'`;
case "IN":
const inValues = Array.isArray(value) ? value : [value];
return `${field} IN (${inValues.map((v) => `'${v}'`).join(", ")})`;
case "NOT IN":
const notInValues = Array.isArray(value) ? value : [value];
return `${field} NOT IN (${notInValues.map((v) => `'${v}'`).join(", ")})`;
case "~":
return `${field} LIKE '%${value}%'`;
case "!~":
return `${field} NOT LIKE '%${value}%'`;
case "IS EMPTY":
return `${field} IS NULL`;
case "IS NOT EMPTY":
return `${field} IS NOT NULL`;
default:
return `${field} = '${value}'`;
}
}
function logicalGroupToSQL(group) {
const conditions = group.conditions.map(toSQLQuery);
const operator = group.operator;
if (conditions.length === 1) {
return conditions[0];
}
return `(${conditions.join(` ${operator} `)})`;
}
function countConditions(expr) {
if (isCondition(expr)) {
return 1;
} else if (isLogicalGroup(expr)) {
return expr.conditions.reduce((count, condition) => count + countConditions(condition), 0);
}
return 0;
}
function getUsedFields(expr) {
if (isCondition(expr)) {
return [expr.field];
} else if (isLogicalGroup(expr)) {
return expr.conditions.flatMap(getUsedFields);
}
return [];
}
function printExpression(expr, indent = 0) {
const spaces = " ".repeat(indent);
if (isCondition(expr)) {
return `${spaces}${expr.field} ${expr.operator} ${Array.isArray(expr.value) ? `[${expr.value.join(", ")}]` : expr.value}`;
} else if (isLogicalGroup(expr)) {
const conditions = expr.conditions.map((c) => printExpression(c, indent + 1)).join("\n");
return `${spaces}${expr.operator}:
${conditions}`;
}
return "";
}
function classNames(...classes) {
return classes.filter(Boolean).join(" ");
}
class QLSuggestionEngine {
constructor(config) {
__publicField(this, "config");
this.config = config;
}
/**
* Get suggestions based on current context
*/
getSuggestions(context) {
const suggestions = [];
const { partialInput = "" } = context;
if (context.expectingField) {
suggestions.push(...this.getFieldSuggestions());
if (this.config.allowFunctions) {
suggestions.push(...this.getFunctionSuggestions());
}
} else if (context.expectingOperator && context.lastField) {
suggestions.push(...this.getOperatorSuggestions(context.lastField));
} else if (context.expectingValue && context.lastField) {
if (context.inInList) {
suggestions.push(...this.getInListValueSuggestions(context.lastField));
} else {
suggestions.push(...this.getValueSuggestions(context.lastField));
}
} else if (context.expectingLogical) {
suggestions.push(...this.getLogicalOperatorSuggestions());
} else if (context.inOrderBy) {
if (context.expectingField) {
suggestions.push(...this.getSortableFieldSuggestions());
} else {
suggestions.push(...this.getSortDirectionSuggestions());
}
}
if (this.config.allowParentheses && (context.expectingField || context.expectingLogical)) {
suggestions.push({
type: "parenthesis",
value: "(",
displayValue: "(",
description: "Group conditions",
insertText: "("
});
}
if (context.inInList) {
suggestions.push({
type: "comma",
value: ",",
displayValue: ",",
description: "Add another value",
insertText: ", "
});
}
const filtered = this.filterSuggestions(suggestions, partialInput);
const maxSuggestions = this.config.maxSuggestions || 10;
return filtered.slice(0, maxSuggestions);
}
/**
* Get field suggestions
*/
getFieldSuggestions() {
return this.config.fields.map((field) => ({
type: "field",
value: field.name,
displayValue: field.displayName || field.name,
description: field.description,
category: "Fields"
}));
}
/**
* Get operator suggestions for a specific field
*/
getOperatorSuggestions(fieldName) {
const field = this.config.fields.find((f) => f.name === fieldName);
if (!field) return [];
return field.operators.map((operator) => {
const isListOperator = operator === "IN" || operator === "NOT IN";
const insertText = isListOperator ? `${operator} (` : operator;
return {
type: "operator",
value: operator,
displayValue: operator,
description: this.getOperatorDescription(operator),
insertText,
category: "Operators"
};
});
}
/**
* Get value suggestions for a specific field
*/
getValueSuggestions(fieldName) {
const field = this.config.fields.find((f) => f.name === fieldName);
if (!field) return [];
const suggestions = [];
if (field.options) {
suggestions.push(
...field.options.map((option) => {
const needsQuotes = /\s/.test(option.value) || /[,()"]/.test(option.value);
const quotedValue = needsQuotes ? `"${option.value}"` : option.value;
return {
type: "value",
value: option.value,
displayValue: option.displayValue || option.value,
description: option.description,
category: "Values",
insertText: needsQuotes ? quotedValue : void 0
// Only set insertText if quoting is needed
};
})
);
}
if (field.type === "boolean") {
suggestions.push(
{ type: "value", value: "true", displayValue: "true", category: "Values" },
{ type: "value", value: "false", displayValue: "false", category: "Values" }
);
}
if (this.config.allowFunctions) {
suggestions.push(...this.getFunctionSuggestions());
}
return suggestions;
}
/**
* Get operator description
*/
getOperatorDescription(operator) {
const descriptions = {
"=": "Equals",
"!=": "Not equals",
">": "Greater than",
"<": "Less than",
">=": "Greater than or equal",
"<=": "Less than or equal",
"~": "Contains",
"!~": "Does not contain",
IN: "In list",
"NOT IN": "Not in list",
"IS EMPTY": "Is empty",
"IS NOT EMPTY": "Is not empty"
};
return descriptions[operator] || operator;
}
/**
* Get logical operator suggestions
*/
getLogicalOperatorSuggestions() {
const operators = this.config.logicalOperators || ["AND", "OR", "NOT"];
return operators.map((operator) => ({
type: "logical",
value: operator,
displayValue: operator,
description: this.getLogicalOperatorDescription(operator),
category: "Logical"
}));
}
/**
* Get sortable field suggestions for ORDER BY
*/
getSortableFieldSuggestions() {
return this.config.fields.filter((field) => field.sortable !== false).map((field) => ({
type: "field",
value: field.name,
displayValue: field.displayName || field.name,
description: field.description,
category: "Sortable Fields"
}));
}
/**
* Get sort direction suggestions
*/
getSortDirectionSuggestions() {
const directions = ["ASC", "DESC"];
return directions.map((direction) => ({
type: "keyword",
value: direction,
displayValue: direction,
description: direction === "ASC" ? "Ascending" : "Descending",
category: "Sort Direction"
}));
}
/**
* Get function suggestions
*/
getFunctionSuggestions() {
if (!this.config.functions) return [];
return this.config.functions.map((func) => {
var _a;
const hasParameters = func.parameters && func.parameters.length > 0;
let insertText;
let displayValue;
if (hasParameters) {
const paramPlaceholders = func.parameters.map((param) => {
let placeholder;
switch (param.type) {
case "text":
placeholder = param.required ? `"${param.name}"` : `"${param.name}?"`;
break;
case "number":
placeholder = param.required ? param.name : `${param.name}?`;
break;
case "date":
case "datetime":
placeholder = param.required ? `"YYYY-MM-DD"` : `"YYYY-MM-DD?"`;
break;
default:
placeholder = param.required ? param.name : `${param.name}?`;
}
return placeholder;
});
insertText = `${func.name}(${paramPlaceholders.join(", ")})`;
displayValue = func.displayName || `${func.name}(${((_a = func.parameters) == null ? void 0 : _a.map((p) => p.name).join(", ")) || ""})`;
} else {
insertText = `${func.name}()`;
displayValue = func.displayName || `${func.name}()`;
}
return {
type: "function",
value: func.name,
displayValue,
description: func.description,
insertText,
category: "Functions"
};
});
}
/**
* Get value suggestions for IN lists (inside parentheses)
*/
getInListValueSuggestions(fieldName) {
const field = this.config.fields.find((f) => f.name === fieldName);
if (!field) return [];
const suggestions = [];
if (field.options) {
suggestions.push(
...field.options.map((option) => {
const needsQuotes = /\s/.test(option.value) || /[,()"]/.test(option.value);
const quotedValue = needsQuotes ? `"${option.value}"` : option.value;
return {
type: "value",
value: option.value,
displayValue: option.displayValue || option.value,
description: option.description,
category: "Values",
// Add comma and space after value for easier multi-selection in IN lists
insertText: `${quotedValue}, `
};
})
);
}
if (this.config.allowFunctions && this.config.functions) {
suggestions.push(
...this.config.functions.map((func) => ({
type: "function",
value: func.name,
displayValue: func.displayName || func.name,
description: func.description,
category: "Functions",
// Add comma and space after function for easier multi-selection in IN lists
insertText: `${func.name}(), `
}))
);
}
return suggestions;
}
/**
* Get logical operator description
*/
getLogicalOperatorDescription(operator) {
const descriptions = {
AND: "Both conditions must be true",
OR: "Either condition can be true",
NOT: "Negates the condition"
};
return descriptions[operator] || operator;
}
/**
* Filter suggestions based on partial input
*/
filterSuggestions(suggestions, partialInput) {
if (!partialInput.trim()) {
return suggestions;
}
const searchTerm = this.config.caseSensitive ? partialInput : partialInput.toLowerCase();
const filtered = suggestions.filter((suggestion) => {
const value = this.config.caseSensitive ? suggestion.value : suggestion.value.toLowerCase();
const displayValue = this.config.caseSensitive ? suggestion.displayValue || suggestion.value : (suggestion.displayValue || suggestion.value).toLowerCase();
const matches = value.includes(searchTerm) || displayValue.includes(searchTerm);
return matches;
});
return filtered;
}
/**
* Get suggestion context from input and cursor position
*/
getSuggestionContext(input, cursorPosition, tokens) {
const context = {
input,
cursorPosition,
tokens,
expectingField: true,
expectingOperator: false,
expectingValue: false,
expectingLogical: false,
inOrderBy: false,
parenthesesLevel: 0,
inInList: false
};
let currentTokenIndex = -1;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token.start <= cursorPosition && cursorPosition <= token.end) {
currentTokenIndex = i;
break;
} else if (token.start > cursorPosition) {
currentTokenIndex = i - 1;
break;
}
}
if (currentTokenIndex === -1 && tokens.length > 0) {
currentTokenIndex = tokens.length - 1;
}
if (currentTokenIndex >= 0) {
context.currentToken = tokens[currentTokenIndex];
context.previousToken = currentTokenIndex > 0 ? tokens[currentTokenIndex - 1] : void 0;
context.nextToken = currentTokenIndex < tokens.length - 1 ? tokens[currentTokenIndex + 1] : void 0;
}
let expectingField = true;
let expectingOperator = false;
let expectingValue = false;
let expectingLogical = false;
let inOrderBy = false;
let parenthesesLevel = 0;
let inInList = false;
let lastField;
let lastOperator;
let originalField;
for (let i = 0; i <= currentTokenIndex; i++) {
const token = tokens[i];
if (token.type === "whitespace") continue;
if (token.type === "keyword" && token.value.toUpperCase() === "ORDER") {
inOrderBy = true;
expectingField = false;
expectingOperator = false;
expectingValue = false;
expectingLogical = false;
} else if (token.type === "keyword" && token.value.toUpperCase() === "BY") {
expectingField = true;
expectingOperator = false;
expectingValue = false;
expectingLogical = false;
} else if (token.type === "field") {
if (!inInList) {
lastField = token.value;
originalField = token.value;
}
if (inOrderBy) {
expectingField = false;
expectingOperator = false;
expectingValue = false;
expectingLogical = false;
} else if (!inInList) {
expectingField = false;
expectingOperator = true;
expectingValue = false;
expectingLogical = false;
}
} else if (token.type === "operator") {
lastOperator = token.value;
expectingField = false;
expectingOperator = false;
expectingValue = true;
expectingLogical = false;
if (token.value === "IN" || token.value === "NOT IN") {
for (let j = i + 1; j < tokens.length; j++) {
const nextToken = tokens[j];
if (nextToken.type === "whitespace") continue;
if (nextToken.type === "parenthesis" && nextToken.value === "(") {
inInList = true;
}
break;
}
}
} else if (token.type === "value") {
expectingField = false;
expectingOperator = false;
expectingValue = false;
expectingLogical = true;
} else if (token.type === "logical") {
expectingField = true;
expectingOperator = false;
expectingValue = false;
expectingLogical = false;
} else if (token.type === "parenthesis") {
if (token.value === "(") {
parenthesesLevel++;
if (!inInList) {
expectingField = true;
expectingOperator = false;
expectingValue = false;
expectingLogical = false;
}
} else if (token.value === ")") {
parenthesesLevel--;
if (inInList && parenthesesLevel === 0) {
inInList = false;
expectingField = false;
expectingOperator = false;
expectingValue = false;
expectingLogical = true;
} else {
expectingField = false;
expectingOperator = false;
expectingValue = false;
expectingLogical = true;
}
}
} else if (token.type === "comma") {
if (inInList) {
expectingField = false;
expectingOperator = false;
expectingValue = true;
expectingLogical = false;
}
}
}
let partialInput = "";
if (context.currentToken && context.currentToken.type !== "whitespace") {
const tokenStart = context.currentToken.start;
const tokenEnd = context.currentToken.end;
if (cursorPosition >= tokenStart && cursorPosition <= tokenEnd) {
partialInput = input.slice(tokenStart, cursorPosition);
if (context.currentToken.type === "parenthesis" || context.currentToken.type === "comma") {
partialInput = "";
}
} else {
partialInput = context.currentToken.value;
if (context.currentToken.type === "parenthesis" || context.currentToken.type === "comma") {
partialInput = "";
}
}
} else {
const beforeCursor = input.slice(0, cursorPosition);
const match = beforeCursor.match(/(\w+)$/);
if (match) {
partialInput = match[1];
}
}
if (context.currentToken && context.currentToken.type !== "whitespace" && partialInput) {
const isPartialToken = cursorPosition >= context.currentToken.start && cursorPosition <= context.currentToken.end;
if (isPartialToken) {
if (context.currentToken.type === "field" || context.currentToken.type === "unknown") {
if (inInList) {
expectingField = false;
expectingOperator = false;
expectingValue = true;
expectingLogical = false;
} else {
expectingField = true;
expectingOperator = false;
expectingValue = false;
expectingLogical = false;
}
} else if (context.currentToken.type === "operator") {
expectingField = false;
expectingOperator = true;
expectingValue = false;
expectingLogical = false;
} else if (context.currentToken.type === "value") {
expectingField = false;
expectingOperator = false;
expectingValue = true;
expectingLogical = false;
} else if (context.currentToken.type === "logical") {
expectingField = false;
expectingOperator = false;
expectingValue = false;
expectingLogical = true;
} else if (context.currentToken.type === "function") {
expectingField = false;
expectingOperator = false;
expectingValue = true;
expectingLogical = false;
}
}
}
context.expectingField = expectingField;
context.expectingOperator = expectingOperator;
context.expectingValue = expectingValue;
context.expectingLogical = expectingLogical;
context.inOrderBy = inOrderBy;
context.parenthesesLevel = parenthesesLevel;
context.inInList = inInList;
context.lastField = inInList && originalField ? originalField : lastField;
context.lastOperator = lastOperator;
context.partialInput = partialInput;
return context;
}
}
function useQLInput({
config,
initialValue = "",
onChange,
onExecute,
getAsyncValueSuggestions,
getPredefinedValueSuggestions
}) {
const [state, setState] = useState({
value: initialValue,
tokens: [],
suggestions: [],
showSuggestions: false,
selectedSuggestionIndex: 0,
cursorPosition: 0,
query: { raw: initialValue, valid: true, errors: [] },
validationErrors: [],
isLoading: false,
justSelectedSuggestion: false
});
const parser = useRef(new QLParser(config));
const suggestionEngine = useRef(new QLSuggestionEngine(config));
const updateQuery = useCallback(
(value) => {
const tokens = parser.current.tokenize(value);
const query = parser.current.parse(value);
setState((prev) => ({
...prev,
tokens,
query
}));
onChange == null ? void 0 : onChange(value, query);
},
[onChange]
);
const updateSuggestions = useCallback(
async (value, cursorPosition) => {
const tokens = parser.current.tokenize(value);
const context = suggestionEngine.current.getSuggestionContext(value, cursorPosition, tokens);
let suggestions = suggestionEngine.current.getSuggestions(context);
if (context.expectingValue && context.lastField && getAsyncValueSuggestions) {
const field = config.fields.find((f) => f.name === context.lastField);
if ((field == null ? void 0 : field.asyncValueSuggestions) && context.partialInput) {
setState((prev) => ({ ...prev, isLoading: true }));
try {
const asyncValues = await getAsyncValueSuggestions(context.lastField, context.partialInput);
const asyncSuggestions = asyncValues.map((value2) => ({
type: "value",
value: value2.value,
displayValue: value2.displayValue || value2.value,
description: value2.description,
insertText: value2.value.includes(" ") ? `"${value2.value}"` : value2.value,
category: "Dynamic Values"
}));
suggestions = [...asyncSuggestions, ...suggestions];
} catch (error) {
console.error("Failed to fetch async suggestions:", error);
} finally {
setState((prev) => ({ ...prev, isLoading: false }));
}
}
}
if (context.expectingValue && context.lastField && getPredefinedValueSuggestions) {
const predefinedValues = getPredefinedValueSuggestions(context.lastField);
const predefinedSuggestions = predefinedValues.map((value2) => ({
type: "value",
value: value2.value,
displayValue: value2.displayValue || value2.value,
description: value2.description,
insertText: value2.value.includes(" ") ? `"${value2.value}"` : value2.value,
category: "Predefined Values"
}));
suggestions = [...predefinedSuggestions, ...suggestions];
}
setState((prev) => ({
...prev,
suggestions,
selectedSuggestionIndex: 0,
showSuggestions: suggestions.length > 0
}));
},
[config.fields, getAsyncValueSuggestions, getPredefinedValueSuggestions]
);
const handleInputChange = useCallback(
(value, cursorPosition) => {
const currentState = state;
const prevValue = currentState.value;
const lastCharTyped = value.length > prevValue.length ? value[value.length - 1] : "";
const shouldShowSuggestions = !currentState.justSelectedSuggestion || lastCharTyped === " ";
setState((prev) => ({
...prev,
value,
cursorPosition,
justSelectedSuggestion: prev.justSelectedSuggestion && lastCharTyped !== " "
// Clear flag when space is typed
}));
updateQuery(value);
if (shouldShowSuggestions) {
updateSuggestions(value, cursorPosition);
} else {
setState((prev) => ({
...prev,
showSuggestions: false,
suggestions: [],
selectedSuggestionIndex: 0
}));
}
},
[state, updateQuery, updateSuggestions]
);
const selectSuggestion = useCallback(
(suggestion) => {
const { value, cursorPosition, tokens } = state;
const currentToken = tokens.find((token) => cursorPosition >= token.start && cursorPosition <= token.end);
let newValue;
let newCursorPosition;
if (currentToken && currentToken.type !== "whitespace") {
const isInListPunctuation = currentToken.type === "parenthesis" && currentToken.value === "(" || currentToken.type === "comma";
if (isInListPunctuation && (suggestion.type === "value" || suggestion.type === "function")) {
const before = value.slice(0, currentToken.end);
const after = value.slice(currentToken.end);
const insertText = suggestion.insertText || suggestion.value;
newValue = before + insertText + after;
newCursorPosition = currentToken.end + insertText.length;
} else {
const before = value.slice(0, currentToken.start);
const after = value.slice(currentToken.end);
const insertText = suggestion.insertText || suggestion.value;
newValue = before + insertText + after;
newCursorPosition = currentToken.start + insertText.length;
}
} else {
const before = value.slice(0, cursorPosition);
const after = value.slice(cursorPosition);
const insertText = suggestion.insertText || suggestion.value;
newValue = before + insertText + after;
newCursorPosition = cursorPosition + insertText.length;
}
setState((prev) => ({
...prev,
value: newValue,
cursorPosition: newCursorPosition,
showSuggestions: false,
selectedSuggestionIndex: 0,
justSelectedSuggestion: true
// Flag to prevent immediate suggestions
}));
updateQuery(newValue);
},
[state, updateQuery]
);
const handleKeyDown = useCallback(
(event) => {
if (event.ctrlKey && event.key === " ") {
event.preventDefault();
setState((prev) => ({
...prev,
justSelectedSuggestion: false
}));
updateSuggestions(state.value, state.cursorPosition);
return;
}
if (!state.showSuggestions) {
if (event.key === "Enter") {
onExecute == null ? void 0 : onExecute(state.query);
return;
}
return;
}
switch (event.key) {
case "ArrowDown":
event.preventDefault();
setState((prev) => ({
...prev,
selectedSuggestionIndex: Math.min(prev.selectedSuggestionIndex + 1, prev.suggestions.length - 1)
}));
break;
case "ArrowUp":
event.preventDefault();
setState((prev) => ({
...prev,
selectedSuggestionIndex: Math.max(prev.selectedSuggestionIndex - 1, 0)
}));
break;
case "Enter":
event.preventDefault();
if (state.suggestions[state.selectedSuggestionIndex]) {
selectSuggestion(state.suggestions[state.selectedSuggestionIndex]);
}
break;
case "Escape":
event.preventDefault();
setState((prev) => ({
...prev,
showSuggestions: false,
selectedSuggestionIndex: 0
}));
break;
case "Tab":
event.preventDefault();
if (state.suggestions[state.selectedSuggestionIndex]) {
selectSuggestion(state.suggestions[state.selectedSuggestionIndex]);
}
break;
}
},
[
state.showSuggestions,
state.suggestions,
state.selectedSuggestionIndex,
state.query,
state.value,
state.cursorPosition,
onExecute,
selectSuggestion,
updateSuggestions
]
);
const hideSuggestions = useCallback(() => {
setState((prev) => ({
...prev,
showSuggestions: false,
selectedSuggestionIndex: 0
}));
}, []);
return {
state,
handleInputChange,
handleKeyDown,
selectSuggestion,
hideSuggestions
};
}
const SearchIcon = ({ className = "", size = 16 }) => /* @__PURE__ */ jsxs(
"svg",
{
className: `ql-icon-search ${className}`,
width: size,
height: size,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round",
children: [
/* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "8" }),
/* @__PURE__ */ jsx("path", { d: "m21 21-4.35-4.35" })
]
}
);
const XIcon = ({ className = "", size = 16 }) => /* @__PURE__ */ jsxs(
"svg",
{
className: `ql-icon-x ${className}`,
width: size,
height: size,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round",
style: { display: "block" },
children: [
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
]
}
);
const LoaderIcon = ({ className = "", size = 16 }) => /* @__PURE__ */ jsx(
"svg",
{
className: `ql-icon-loader ${className}`,
width: size,
height: size,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round",
children: /* @__PURE__ */ jsx("path", { d: "M21 12a9 9 0 11-6.219-8.56" })
}
);
const ChevronDownIcon = ({ className = "", size = 16 }) => /* @__PURE__ */ jsx(
"svg",
{
className: `ql-icon-chevron-down ${className}`,
width: size,
height: size,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round",
children: /* @__PURE__ */ jsx("path", { d: "m6 9 6 6 6-6" })
}
);
const CheckIcon = ({ className = "", size = 16 }) => /* @__PURE__ */ jsx(
"svg",
{
className: `ql-icon-check ${className}`,
width: size,
height: size,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round",
strokeLinejoin: "round",
children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" })
}
);
const QLInput = React.forwardRef(
({
value,
onChange,
onExecute,
config,
placeholder = "Enter QL query...",
disabled = false,
className,
showSearchIcon = true,
showClearIcon = true,
getAsyncValueSuggestions,
getPredefinedValueSuggestions,
...props
}, ref) => {
const inputRef = React.useRef(null);
const suggestionsRef = React.useRef(null);
const [isOpen, setIsOpen] = React.useState(false);
const { state, h