diff --git a/src/components/Dashboard/common/FilterInput.jsx b/src/components/Dashboard/common/FilterInput.jsx index 4ec8bdc..e8a8a2f 100644 --- a/src/components/Dashboard/common/FilterInput.jsx +++ b/src/components/Dashboard/common/FilterInput.jsx @@ -29,12 +29,14 @@ import EqualIcon from '../../Icons/EqualIcon' const operandIconStyle = { fontSize: 8 } // Longer symbols first so ".." / "<>" / ">=" match before "." / "<" / ">". -// Wildcards (* ?) and @ stay as plain text — they are not operands. +// Wildcards (* ?) are highlighted in place; @ stays as plain text — none are operands. +const WILDCARDS = new Set(['*', '?']) + const OPERANDS = [ { symbol: '..', label: , - color: 'blue' + color: 'purple' }, { symbol: '<>', @@ -93,6 +95,13 @@ const tokenize = (raw = '') => { } tokens.push({ type: 'operand', value: match.symbol }) i += match.symbol.length + } else if (WILDCARDS.has(value[i])) { + if (text) { + tokens.push({ type: 'text', value: text }) + text = '' + } + tokens.push({ type: 'wildcard', value: value[i] }) + i += 1 } else { text += value[i] i += 1 @@ -103,6 +112,14 @@ const tokenize = (raw = '') => { return tokens } +const renderWildcardNode = (char) => { + const span = document.createElement('span') + span.setAttribute('data-wildcard', char) + span.style.color = 'var(--color-primary)' + span.textContent = char + return span +} + const normalize = (raw = '') => tokenize(raw) .map((token) => token.value) @@ -302,32 +319,41 @@ const setCaretOffset = (root, targetOffset) => { scrollSelectionIntoView(root) } - for (let i = 0; i < root.childNodes.length; i += 1) { - const child = root.childNodes[i] - const length = nodeLength(child) + const walk = (container) => { + for (let i = 0; i < container.childNodes.length; i += 1) { + const child = container.childNodes[i] + const length = nodeLength(child) - if (child.nodeType === Node.TEXT_NODE) { - if (remaining <= length) { - place(child, remaining) - return + if (child.nodeType === Node.TEXT_NODE) { + if (remaining <= length) { + place(child, remaining) + return true + } + remaining -= length + continue } - remaining -= length - continue - } - if (child.getAttribute?.('data-operand') != null) { - if (remaining === 0) { - place(root, i) - return + if (child.getAttribute?.('data-operand') != null) { + if (remaining === 0) { + place(container, i) + return true + } + if (remaining <= length) { + place(container, i + 1) + return true + } + remaining -= length + continue } - if (remaining <= length) { - place(root, i + 1) - return - } - remaining -= length + + // Element wrappers (e.g. wildcard spans): recurse into children. + if (walk(child)) return true } + return false } + if (walk(root)) return + const range = document.createRange() range.selectNodeContents(root) range.collapse(false) @@ -341,7 +367,10 @@ const domMatchesTokens = (root, raw) => { const tokens = tokenize(raw) const children = Array.from(root.childNodes).filter((node) => { if (node.nodeType === Node.TEXT_NODE) return (node.textContent ?? '') !== '' - return node.getAttribute?.('data-operand') != null + return ( + node.getAttribute?.('data-operand') != null || + node.getAttribute?.('data-wildcard') != null + ) }) // Empty editor may keep a single empty text node. @@ -363,6 +392,12 @@ const domMatchesTokens = (root, raw) => { child.nodeType === Node.TEXT_NODE && child.textContent === token.value ) } + if (token.type === 'wildcard') { + return ( + child.getAttribute?.('data-wildcard') === token.value && + (child.textContent ?? '') === token.value + ) + } return child.getAttribute?.('data-operand') === token.value }) } @@ -569,6 +604,8 @@ const FilterInput = ({ for (const item of tokens) { if (item.type === 'text') { nextChildren.push(document.createTextNode(item.value)) + } else if (item.type === 'wildcard') { + nextChildren.push(renderWildcardNode(item.value)) } else { nextChildren.push(takeChip(item.value)) }