Skip to content

Commit fbba1e4

Browse files
committed
Highlighting in SearchControl
1 parent e970549 commit fbba1e4

File tree

18 files changed

+364
-133
lines changed

18 files changed

+364
-133
lines changed

Signum.Entities/DynamicQuery/Filter.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,7 @@ public static List<FilterFullText> TableFilters(List<Filter> filters)
503503
return filters.SelectMany(a => a.GetAllFilters()).OfType<FilterFullText>().Where(a => a.IsTable).ToList();
504504
}
505505

506+
//Keep in sync with Finder.tsx extractComplexConditions
506507
public override IEnumerable<string> GetKeywords()
507508
{
508509
if (this.Operation == FullTextFilterOperation.FreeText)

Signum.Entities/DynamicQuery/Tokens/EntityPropertyToken.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ protected override List<QueryToken> SubTokensOverride(SubTokensOptions options)
142142
result.Add(new FullTextRankToken(this));
143143
}
144144

145-
if (this.HasSnippet)
145+
if (this.HasSnippet && (options & SubTokensOptions.CanToArray) != 0)
146146
{
147147
result.Add(new StringSnippetToken(this));
148148
}

Signum.Entities/EnumMessages.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ public enum SearchMessage
180180
[Description("Delete all filter")]
181181
DeleteAllFilter,
182182
Filters,
183+
Columns,
183184
Find,
184185
[Description("Finder of {0}")]
185186
FinderOf0,

Signum.React.Extensions/Authorization/AuthAdminClient.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react'
22
import { RouteObject } from 'react-router'
3-
import { ModifiableEntity, EntityPack, is, OperationSymbol, SearchMessage, Lite, getToString, EntityControlMessage } from '@framework/Signum.Entities';
3+
import { ModifiableEntity, EntityPack, is, OperationSymbol, SearchMessage, Lite, getToString, EntityControlMessage, liteKeyLong } from '@framework/Signum.Entities';
44
import { ifError } from '@framework/Globals';
55
import { ajaxPost, ajaxGet, ajaxGetRaw, saveFile, ServiceError } from '@framework/Services';
66
import * as Services from '@framework/Services';
@@ -41,14 +41,14 @@ export function start(options: { routes: RouteObject[], types: boolean; properti
4141
permissions = options.permissions;
4242

4343
Navigator.addSettings(new EntitySettings(UserEntity, e => import('./Templates/User'), {
44-
renderLite: (lite, subStr) => {
44+
renderLite: (lite, hl) => {
4545
if (UserLiteModel.isInstance(lite.model))
4646
return (
47-
<span className="d-inline-flex align-items-center"><SmallProfilePhoto user={lite} className="me-1" /><span>{TypeaheadOptions.highlightedText(getToString(lite), subStr)}</span></span>
47+
<span className="d-inline-flex align-items-center"><SmallProfilePhoto user={lite} className="me-1" /><span>{hl.highlight(getToString(lite))}</span></span>
4848
);
4949

5050
if (typeof lite.model == "string")
51-
return TypeaheadOptions.highlightedText(getToString(lite), subStr);
51+
return hl.highlight(getToString(lite));
5252

5353
return lite.EntityType;
5454
}

Signum.React.Extensions/Basics/Templates/IconTypeahead.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { TypeContext } from '@framework/TypeContext'
77
import { library } from '@fortawesome/fontawesome-svg-core'
88
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
99
import { useForceUpdate } from '@framework/Hooks'
10-
import { TypeaheadOptions } from '@framework/Components/Typeahead'
10+
import { TextHighlighter, TypeaheadOptions } from '@framework/Components/Typeahead'
1111
import { IconName, IconProp, IconPrefix } from "@fortawesome/fontawesome-svg-core";
1212

1313
export interface IconTypeaheadLineProps {
@@ -99,13 +99,13 @@ export function IconTypeahead(p: IconTypeaheadProps) {
9999
return item as string;
100100
}
101101

102-
function handleRenderItem(item: unknown, query: string) {
102+
function handleRenderItem(item: unknown, hl: TextHighlighter) {
103103
var icon = parseIcon(item as string);
104104

105105
return (
106106
<span>
107107
{icon && <FontAwesomeIcon icon={icon} className="icon" style={{ width: "12px", height: "12px" }} />}
108-
{TypeaheadOptions.highlightedTextAll(item as string, query)}
108+
{hl.highlight(item as string)}
109109
</span>
110110
);
111111
}

Signum.React/Scripts/AppContext.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ declare global {
155155

156156
interface Array<T> {
157157
joinCommaHtml(this: Array<T>, lastSeparator: string): React.ReactElement<any>;
158+
joinHtml(this: Array<T>, separator: string | React.ReactElement<any>): React.ReactElement<any>;
158159
}
159160
}
160161

@@ -196,3 +197,20 @@ Array.prototype.joinCommaHtml = function (this: any[], lastSeparator: string) {
196197
return React.createElement("span", undefined, ...result);
197198
}
198199

200+
Array.prototype.joinHtml = function (this: any[], separator: string | React.ReactElement<any>) {
201+
const args = arguments;
202+
203+
const result: (string | React.ReactElement<any>)[] = [];
204+
for (let i = 0; i < this.length -1; i++) {
205+
result.push(this[i]);
206+
result.push(separator);
207+
}
208+
209+
210+
if (this.length >= 1) {
211+
result.push(this[this.length - 1]);
212+
}
213+
214+
return React.createElement("span", undefined, ...result);
215+
}
216+

Signum.React/Scripts/Components/Typeahead.tsx

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export interface TypeaheadProps {
1313
itemsDelay?: number;
1414
minLength?: number;
1515
renderList?: (typeahead: TypeaheadController) => React.ReactNode;
16-
renderItem?: (item: unknown, query: string) => React.ReactNode;
16+
renderItem?: (item: unknown, highlighter: TextHighlighter) => React.ReactNode;
1717
onSelect?: (item: unknown, e: React.KeyboardEvent<any> | React.MouseEvent<any>) => string | null;
1818
scrollHeight?: number;
1919
inputAttrs?: React.InputHTMLAttributes<HTMLInputElement>;
@@ -257,6 +257,8 @@ export const Typeahead = React.forwardRef(function Typeahead(p: TypeaheadProps,
257257

258258
function renderDefaultList() {
259259
var items = controller.items;
260+
261+
var highlighter = TextHighlighter.fromString(controller.query);
260262
return (
261263
<Dropdown.Menu align={controller.rtl ? "end" : undefined} className="typeahead">
262264
{
@@ -268,7 +270,7 @@ export const Typeahead = React.forwardRef(function Typeahead(p: TypeaheadProps,
268270
onMouseLeave={e => controller.handleElementMouseLeave(e, i)}
269271
onMouseUp={e => controller.handleMenuMouseUp(e, i)}
270272
{...p.itemAttrs && p.itemAttrs(item)}>
271-
{p.renderItem!(item, controller.query!)}
273+
{p.renderItem!(item, highlighter)}
272274
</button>)
273275
}
274276
</Dropdown.Menu>
@@ -293,7 +295,7 @@ Typeahead.defaultProps = {
293295
getItems: undefined as any,
294296
itemsDelay: 200,
295297
minLength: 1,
296-
renderItem: (item, query) => TypeaheadOptions.highlightedText(item as string, query),
298+
renderItem: (item, highlighter) => highlighter.highlight(item as string),
297299
onSelect: (elem, event) => (elem as string),
298300
scrollHeight: 0,
299301

@@ -302,57 +304,55 @@ Typeahead.defaultProps = {
302304

303305

304306
export namespace TypeaheadOptions {
305-
export function highlightedText(val: string, query?: string): React.ReactChild {
306307

307-
if (query == undefined)
308-
return val;
308+
export function normalizeString(str: string): string {
309+
return str;
310+
}
311+
}
309312

310-
const index = val.toLowerCase().indexOf(query.toLowerCase());
311-
if (index == -1)
312-
return val;
313+
export class TextHighlighter {
314+
query?: string;
315+
parts?: string[];
316+
regex?: RegExp;
313317

314-
return (
315-
<>
316-
{val.substr(0, index)}
317-
<strong key={0}>{val.substr(index, query.length)}</strong>
318-
{val.substr(index + query.length)}
319-
</>
320-
);
318+
static fromString(query: string | undefined) {
319+
var hl = new TextHighlighter(query?.split(" "));
320+
hl.query = query;
321+
return hl;
321322
}
322323

323-
export function highlightedTextAll(val: string, query: string | undefined): React.ReactChild {
324-
if (query == undefined)
325-
return val;
324+
constructor(parts: string[] | undefined) {
325+
this.parts = parts?.filter(a => a != null && a.length > 0).orderByDescending(a => a.length);
326+
if (this.parts?.length)
327+
this.regex = new RegExp(this.parts.map(p => RegExp.escape(p)).join("|"), "gi");
328+
}
326329

327-
const parts = query.toLocaleLowerCase().split(" ").filter(a => a.length > 0).orderByDescending(a => a.length);
330+
highlight(text: string): React.ReactChild {
331+
if (!text || !this.regex)
332+
return text;
328333

329-
function splitText(str: string, partIndex: number): React.ReactChild {
334+
var matches = Array.from(text.matchAll(this.regex));
330335

331-
if (str.length == 0)
332-
return str;
336+
if (matches.length == 0)
337+
return text;
333338

334-
if (parts.length <= partIndex)
335-
return str;
339+
var result = [];
336340

337-
var part = parts[partIndex];
341+
var pos = 0;
342+
for (var i = 0; i < matches.length; i++) {
343+
var m = matches[i];
338344

339-
const index = str.toLowerCase().indexOf(part);
340-
if (index == -1)
341-
return splitText(str, partIndex + 1);
345+
if (pos < m.index!) {
346+
result.push(text.substring(pos, m.index));
347+
}
342348

343-
return (
344-
<>
345-
{splitText(str.substr(0, index), partIndex + 1)}
346-
<strong key={0}>{str.substr(index, part.length)}</strong>
347-
{splitText(str.substr(index + part.length), partIndex + 1)}
348-
</>
349-
);
349+
pos = m.index! + m[0].length;
350+
result.push(<strong>{text.substring(m.index!, pos)}</strong>);
350351
}
351352

352-
return splitText(val, 0);
353-
}
353+
if (pos < text.length)
354+
result.push(text.substring(pos));
354355

355-
export function normalizeString(str: string): string {
356-
return str;
356+
return React.createElement(React.Fragment, undefined, ...result);
357357
}
358358
}

0 commit comments

Comments
 (0)