@cbcruk/highlight-kit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +212 -8
- package/dist/core-DDCXN2b_.d.ts +527 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/react.d.ts +203 -1
- package/dist/react.js +300 -2
- package/dist/value-range-CH2vegQi.js +707 -0
- package/package.json +1 -1
- package/dist/core-B8g04jvB.d.ts +0 -168
- package/dist/core-LUnH63zG.js +0 -306
package/dist/react.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { F as TokenRule, N as OverlapStrategy, S as ValueRangeElement, a as HighlightSnapshot, m as highlights, o as MatchOptions, t as HighlightController } from "./core-DDCXN2b_.js";
|
|
2
2
|
import { CSSProperties, ComponentPropsWithoutRef, ElementType, ReactNode, RefObject } from "react";
|
|
3
3
|
//#region src/react.d.ts
|
|
4
4
|
/** The controller in scope — the shared singleton unless a provider overrides it. */
|
|
@@ -259,5 +259,207 @@ export interface HighlightStylesProps {
|
|
|
259
259
|
* {@link useHighlightSearch}.
|
|
260
260
|
*/
|
|
261
261
|
export declare function HighlightStyles({ styles }: HighlightStylesProps): ReactNode;
|
|
262
|
+
/** Options shared by the form-control hooks. */
|
|
263
|
+
export interface ValueHighlightOptions {
|
|
264
|
+
/**
|
|
265
|
+
* The control's current value, for a controlled component.
|
|
266
|
+
*
|
|
267
|
+
* Assigning `value` collapses every live range and fires no `input` event, so
|
|
268
|
+
* without this the highlight would go stale on a programmatic change (a reset
|
|
269
|
+
* button, loading a template). Omit it for an uncontrolled control: typing is
|
|
270
|
+
* tracked through `input` either way.
|
|
271
|
+
*/
|
|
272
|
+
value?: string;
|
|
273
|
+
/**
|
|
274
|
+
* Re-tokenize on the control's `input` events.
|
|
275
|
+
* @default true
|
|
276
|
+
*/
|
|
277
|
+
observe?: boolean;
|
|
278
|
+
}
|
|
279
|
+
/** Options for {@link useValueTokens}. */
|
|
280
|
+
export interface UseValueTokensOptions extends ValueHighlightOptions {
|
|
281
|
+
/**
|
|
282
|
+
* How matches covering the same characters resolve.
|
|
283
|
+
* @default 'first'
|
|
284
|
+
*/
|
|
285
|
+
overlap?: OverlapStrategy;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Return value of {@link useValueTokens}.
|
|
289
|
+
*
|
|
290
|
+
* @template T - The form control element type
|
|
291
|
+
*/
|
|
292
|
+
export interface UseValueTokensResult<T extends ValueRangeElement> {
|
|
293
|
+
/** Attach to the `<input>` or `<textarea>` to tokenize. */
|
|
294
|
+
ref: RefObject<T | null>;
|
|
295
|
+
/** Whether this browser and this element support value ranges. */
|
|
296
|
+
supported: boolean;
|
|
297
|
+
/** Match count per highlight name, with an entry for every rule name. */
|
|
298
|
+
counts: Readonly<Record<string, number>>;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Tokenize a form control's value and highlight each rule's matches.
|
|
302
|
+
*
|
|
303
|
+
* The control's text is re-tokenized on every `input` event, with one highlight
|
|
304
|
+
* name per rule name, and the previous generation of live ranges is released.
|
|
305
|
+
* Rules are compared by content, so an inline rule array is fine.
|
|
306
|
+
*
|
|
307
|
+
* Needs `OpaqueRange` (Chromium 152+). Everywhere else `supported` is false and
|
|
308
|
+
* the hook does nothing, leaving the control to render normally.
|
|
309
|
+
*
|
|
310
|
+
* @template T - The form control element type
|
|
311
|
+
* @param rules - Ordered rules; earlier ones win, see {@link TokenRule}
|
|
312
|
+
* @returns The control ref, support flag, and per-name match counts
|
|
313
|
+
*
|
|
314
|
+
* @example A textarea highlighted like a code editor
|
|
315
|
+
* ```tsx
|
|
316
|
+
* import { HighlightStyles, useValueTokens } from '@cbcruk/highlight-kit/react'
|
|
317
|
+
*
|
|
318
|
+
* const RULES = [
|
|
319
|
+
* { name: 'comment', pattern: /\/\/[^\n]*|\/\*[\s\S]*?\*\// },
|
|
320
|
+
* { name: 'string', pattern: /'[^']*'|"[^"]*"/ },
|
|
321
|
+
* { name: 'keyword', pattern: /\b(?:const|function|return)\b/ },
|
|
322
|
+
* ]
|
|
323
|
+
*
|
|
324
|
+
* function CodeArea() {
|
|
325
|
+
* const { ref, supported, counts } = useValueTokens<HTMLTextAreaElement>(RULES)
|
|
326
|
+
* return (
|
|
327
|
+
* <>
|
|
328
|
+
* <HighlightStyles
|
|
329
|
+
* styles={{
|
|
330
|
+
* comment: { color: '#6b7280' },
|
|
331
|
+
* string: { color: '#16a34a' },
|
|
332
|
+
* keyword: { color: '#7c3aed' },
|
|
333
|
+
* }}
|
|
334
|
+
* />
|
|
335
|
+
* <textarea ref={ref} defaultValue="const x = 1 // note" />
|
|
336
|
+
* {!supported && <p>This browser cannot highlight inside a textarea.</p>}
|
|
337
|
+
* <small>{counts.keyword ?? 0} keywords</small>
|
|
338
|
+
* </>
|
|
339
|
+
* )
|
|
340
|
+
* }
|
|
341
|
+
* ```
|
|
342
|
+
*/
|
|
343
|
+
export declare function useValueTokens<T extends ValueRangeElement = HTMLTextAreaElement>(rules: readonly TokenRule[], options?: UseValueTokensOptions): UseValueTokensResult<T>;
|
|
344
|
+
/** Options for {@link useValueHighlight}. */
|
|
345
|
+
export interface UseValueHighlightOptions extends MatchOptions, ValueHighlightOptions {
|
|
346
|
+
/** Text or expression to highlight. Falsy clears this source. */
|
|
347
|
+
query: string | RegExp;
|
|
348
|
+
/** CSS `::highlight()` name. Defaults to a unique per-instance name. */
|
|
349
|
+
name?: string;
|
|
350
|
+
/**
|
|
351
|
+
* Stacking order against other highlight names.
|
|
352
|
+
* @default 0
|
|
353
|
+
*/
|
|
354
|
+
priority?: number;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Return value of {@link useValueHighlight}.
|
|
358
|
+
*
|
|
359
|
+
* @template T - The form control element type
|
|
360
|
+
*/
|
|
361
|
+
export interface UseValueHighlightResult<T extends ValueRangeElement> extends HighlightSnapshot {
|
|
362
|
+
/** Attach to the `<input>` or `<textarea>` to scan. */
|
|
363
|
+
ref: RefObject<T | null>;
|
|
364
|
+
/** The resolved highlight name (auto-generated if not provided). */
|
|
365
|
+
name: string;
|
|
366
|
+
/** Whether this browser and this element support value ranges. */
|
|
367
|
+
supported: boolean;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Highlight one pattern inside an `<input>` or `<textarea>`.
|
|
371
|
+
*
|
|
372
|
+
* The single-pattern form of {@link useValueTokens} — the counterpart to
|
|
373
|
+
* {@link useHighlight}, which cannot reach into a form control's value.
|
|
374
|
+
*
|
|
375
|
+
* @template T - The form control element type
|
|
376
|
+
*
|
|
377
|
+
* @example Flagging a word as it is typed
|
|
378
|
+
* ```tsx
|
|
379
|
+
* import { useValueHighlight } from '@cbcruk/highlight-kit/react'
|
|
380
|
+
*
|
|
381
|
+
* function Composer() {
|
|
382
|
+
* const { ref, count, supported } = useValueHighlight<HTMLTextAreaElement>({
|
|
383
|
+
* query: /\bexample\b/gi,
|
|
384
|
+
* name: 'flagged',
|
|
385
|
+
* })
|
|
386
|
+
* return (
|
|
387
|
+
* <>
|
|
388
|
+
* <style>{'::highlight(flagged) { background: #fde68a }'}</style>
|
|
389
|
+
* <textarea ref={ref} />
|
|
390
|
+
* {supported && <small>{count} occurrences</small>}
|
|
391
|
+
* </>
|
|
392
|
+
* )
|
|
393
|
+
* }
|
|
394
|
+
* ```
|
|
395
|
+
*/
|
|
396
|
+
export declare function useValueHighlight<T extends ValueRangeElement = HTMLInputElement>(options: UseValueHighlightOptions): UseValueHighlightResult<T>;
|
|
397
|
+
/** Options for {@link useValueHighlightSearch}. */
|
|
398
|
+
export interface UseValueHighlightSearchOptions extends MatchOptions, ValueHighlightOptions {
|
|
399
|
+
/**
|
|
400
|
+
* Base highlight name. The active match is registered under
|
|
401
|
+
* `${name}-current` with a higher priority.
|
|
402
|
+
* @default 'search'
|
|
403
|
+
*/
|
|
404
|
+
name?: string;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Return value of {@link useValueHighlightSearch}.
|
|
408
|
+
*
|
|
409
|
+
* @template T - The form control element type
|
|
410
|
+
*/
|
|
411
|
+
export interface UseValueHighlightSearchResult<T extends ValueRangeElement> {
|
|
412
|
+
/** Attach to the `<input>` or `<textarea>` to search. */
|
|
413
|
+
ref: RefObject<T | null>;
|
|
414
|
+
/** Number of matches in the control's value. */
|
|
415
|
+
count: number;
|
|
416
|
+
/** Index of the active match, or -1 when there are no matches. */
|
|
417
|
+
active: number;
|
|
418
|
+
/** Move to the next match, wrapping to the first after the last. */
|
|
419
|
+
next(): void;
|
|
420
|
+
/** Move to the previous match, wrapping to the last before the first. */
|
|
421
|
+
prev(): void;
|
|
422
|
+
/** Whether this browser and this element support value ranges. */
|
|
423
|
+
supported: boolean;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Search inside an `<input>` or `<textarea>` with next/prev navigation.
|
|
427
|
+
*
|
|
428
|
+
* All matches go to `name`, the active one to `${name}-current` at a higher
|
|
429
|
+
* priority, and the control is scrolled just far enough to reveal the active
|
|
430
|
+
* match. Scrolling adjusts the control's own `scrollTop`/`scrollLeft` rather
|
|
431
|
+
* than calling `setSelectionRange()`, so the caret and the user's selection are
|
|
432
|
+
* left untouched.
|
|
433
|
+
*
|
|
434
|
+
* @template T - The form control element type
|
|
435
|
+
* @param query - Text or expression to find. Falsy clears the highlight
|
|
436
|
+
*
|
|
437
|
+
* @example
|
|
438
|
+
* ```tsx
|
|
439
|
+
* import { useState } from 'react'
|
|
440
|
+
* import {
|
|
441
|
+
* HighlightStyles,
|
|
442
|
+
* useValueHighlightSearch,
|
|
443
|
+
* } from '@cbcruk/highlight-kit/react'
|
|
444
|
+
*
|
|
445
|
+
* function NoteSearch() {
|
|
446
|
+
* const [query, setQuery] = useState('')
|
|
447
|
+
* const { ref, count, active, next, prev } =
|
|
448
|
+
* useValueHighlightSearch<HTMLTextAreaElement>(query)
|
|
449
|
+
*
|
|
450
|
+
* return (
|
|
451
|
+
* <>
|
|
452
|
+
* <HighlightStyles />
|
|
453
|
+
* <input value={query} onChange={(e) => setQuery(e.target.value)} />
|
|
454
|
+
* <span>{count === 0 ? '0/0' : `${active + 1}/${count}`}</span>
|
|
455
|
+
* <button onClick={prev}>Prev</button>
|
|
456
|
+
* <button onClick={next}>Next</button>
|
|
457
|
+
* <textarea ref={ref} rows={10} />
|
|
458
|
+
* </>
|
|
459
|
+
* )
|
|
460
|
+
* }
|
|
461
|
+
* ```
|
|
462
|
+
*/
|
|
463
|
+
export declare function useValueHighlightSearch<T extends ValueRangeElement = HTMLTextAreaElement>(query: string | RegExp, options?: UseValueHighlightSearchOptions): UseValueHighlightSearchResult<T>;
|
|
262
464
|
//#endregion
|
|
263
465
|
export { highlights };
|
package/dist/react.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as highlights, d as computeRanges, h as generateHighlightCSS, i as disconnectValueRanges, n as createValueRangeRegistry, p as createHighlightController, r as createValueRanges, s as scrollValueRangeIntoView, t as createValueHighlighter, u as tokenizeValue, y as isHighlightSupported } from "./value-range-CH2vegQi.js";
|
|
2
2
|
import { createContext, createElement, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
//#region src/react.tsx
|
|
@@ -316,5 +316,303 @@ const DEFAULT_SEARCH_STYLES = {
|
|
|
316
316
|
function HighlightStyles({ styles = DEFAULT_SEARCH_STYLES }) {
|
|
317
317
|
return /* @__PURE__ */ jsx("style", { children: generateHighlightCSS(styles) });
|
|
318
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Stable identity for a rule list.
|
|
321
|
+
*
|
|
322
|
+
* Rule arrays are almost always written inline, so a new reference arrives on
|
|
323
|
+
* every render. Keying effects on the rules' *content* instead keeps a fresh
|
|
324
|
+
* array from tearing down and rebuilding the highlighter each render, which
|
|
325
|
+
* would discard and recreate every live range.
|
|
326
|
+
*/
|
|
327
|
+
function useRulesKey(rules) {
|
|
328
|
+
return useMemo(() => JSON.stringify(rules.map((rule) => [
|
|
329
|
+
rule.name,
|
|
330
|
+
String(rule.pattern),
|
|
331
|
+
rule.caseSensitive ?? false,
|
|
332
|
+
rule.wholeWord ?? false,
|
|
333
|
+
rule.priority ?? 0
|
|
334
|
+
])), [rules]);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Tokenize a form control's value and highlight each rule's matches.
|
|
338
|
+
*
|
|
339
|
+
* The control's text is re-tokenized on every `input` event, with one highlight
|
|
340
|
+
* name per rule name, and the previous generation of live ranges is released.
|
|
341
|
+
* Rules are compared by content, so an inline rule array is fine.
|
|
342
|
+
*
|
|
343
|
+
* Needs `OpaqueRange` (Chromium 152+). Everywhere else `supported` is false and
|
|
344
|
+
* the hook does nothing, leaving the control to render normally.
|
|
345
|
+
*
|
|
346
|
+
* @template T - The form control element type
|
|
347
|
+
* @param rules - Ordered rules; earlier ones win, see {@link TokenRule}
|
|
348
|
+
* @returns The control ref, support flag, and per-name match counts
|
|
349
|
+
*
|
|
350
|
+
* @example A textarea highlighted like a code editor
|
|
351
|
+
* ```tsx
|
|
352
|
+
* import { HighlightStyles, useValueTokens } from '@cbcruk/highlight-kit/react'
|
|
353
|
+
*
|
|
354
|
+
* const RULES = [
|
|
355
|
+
* { name: 'comment', pattern: /\/\/[^\n]*|\/\*[\s\S]*?\*\// },
|
|
356
|
+
* { name: 'string', pattern: /'[^']*'|"[^"]*"/ },
|
|
357
|
+
* { name: 'keyword', pattern: /\b(?:const|function|return)\b/ },
|
|
358
|
+
* ]
|
|
359
|
+
*
|
|
360
|
+
* function CodeArea() {
|
|
361
|
+
* const { ref, supported, counts } = useValueTokens<HTMLTextAreaElement>(RULES)
|
|
362
|
+
* return (
|
|
363
|
+
* <>
|
|
364
|
+
* <HighlightStyles
|
|
365
|
+
* styles={{
|
|
366
|
+
* comment: { color: '#6b7280' },
|
|
367
|
+
* string: { color: '#16a34a' },
|
|
368
|
+
* keyword: { color: '#7c3aed' },
|
|
369
|
+
* }}
|
|
370
|
+
* />
|
|
371
|
+
* <textarea ref={ref} defaultValue="const x = 1 // note" />
|
|
372
|
+
* {!supported && <p>This browser cannot highlight inside a textarea.</p>}
|
|
373
|
+
* <small>{counts.keyword ?? 0} keywords</small>
|
|
374
|
+
* </>
|
|
375
|
+
* )
|
|
376
|
+
* }
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
function useValueTokens(rules, options = {}) {
|
|
380
|
+
const { overlap, value, observe = true } = options;
|
|
381
|
+
const controller = useHighlightController();
|
|
382
|
+
const sourceId = useId();
|
|
383
|
+
const ref = useRef(null);
|
|
384
|
+
const rulesKey = useRulesKey(rules);
|
|
385
|
+
const rulesRef = useRef(rules);
|
|
386
|
+
rulesRef.current = rules;
|
|
387
|
+
const highlighterRef = useRef(null);
|
|
388
|
+
const [supported, setSupported] = useState(false);
|
|
389
|
+
useIsomorphicLayoutEffect(() => {
|
|
390
|
+
const element = ref.current;
|
|
391
|
+
if (!element) return;
|
|
392
|
+
const highlighter = createValueHighlighter({
|
|
393
|
+
element,
|
|
394
|
+
rules: rulesRef.current,
|
|
395
|
+
overlap,
|
|
396
|
+
controller,
|
|
397
|
+
sourceId,
|
|
398
|
+
observe
|
|
399
|
+
});
|
|
400
|
+
highlighterRef.current = highlighter;
|
|
401
|
+
setSupported(highlighter.supported);
|
|
402
|
+
return () => {
|
|
403
|
+
highlighterRef.current = null;
|
|
404
|
+
highlighter.dispose();
|
|
405
|
+
};
|
|
406
|
+
}, [
|
|
407
|
+
controller,
|
|
408
|
+
sourceId,
|
|
409
|
+
rulesKey,
|
|
410
|
+
overlap,
|
|
411
|
+
observe
|
|
412
|
+
]);
|
|
413
|
+
const seenFirstValue = useRef(false);
|
|
414
|
+
useIsomorphicLayoutEffect(() => {
|
|
415
|
+
if (!seenFirstValue.current) {
|
|
416
|
+
seenFirstValue.current = true;
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
highlighterRef.current?.refresh();
|
|
420
|
+
}, [value]);
|
|
421
|
+
const snapshots = useHighlightSnapshots();
|
|
422
|
+
return {
|
|
423
|
+
ref,
|
|
424
|
+
supported,
|
|
425
|
+
counts: useMemo(() => {
|
|
426
|
+
const result = {};
|
|
427
|
+
for (const rule of rulesRef.current) result[rule.name] = snapshots[rule.name]?.count ?? 0;
|
|
428
|
+
return result;
|
|
429
|
+
}, [snapshots, rulesKey])
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Highlight one pattern inside an `<input>` or `<textarea>`.
|
|
434
|
+
*
|
|
435
|
+
* The single-pattern form of {@link useValueTokens} — the counterpart to
|
|
436
|
+
* {@link useHighlight}, which cannot reach into a form control's value.
|
|
437
|
+
*
|
|
438
|
+
* @template T - The form control element type
|
|
439
|
+
*
|
|
440
|
+
* @example Flagging a word as it is typed
|
|
441
|
+
* ```tsx
|
|
442
|
+
* import { useValueHighlight } from '@cbcruk/highlight-kit/react'
|
|
443
|
+
*
|
|
444
|
+
* function Composer() {
|
|
445
|
+
* const { ref, count, supported } = useValueHighlight<HTMLTextAreaElement>({
|
|
446
|
+
* query: /\bexample\b/gi,
|
|
447
|
+
* name: 'flagged',
|
|
448
|
+
* })
|
|
449
|
+
* return (
|
|
450
|
+
* <>
|
|
451
|
+
* <style>{'::highlight(flagged) { background: #fde68a }'}</style>
|
|
452
|
+
* <textarea ref={ref} />
|
|
453
|
+
* {supported && <small>{count} occurrences</small>}
|
|
454
|
+
* </>
|
|
455
|
+
* )
|
|
456
|
+
* }
|
|
457
|
+
* ```
|
|
458
|
+
*/
|
|
459
|
+
function useValueHighlight(options) {
|
|
460
|
+
const { query, priority = 0, caseSensitive, wholeWord, value, observe } = options;
|
|
461
|
+
const autoId = useId();
|
|
462
|
+
const name = options.name ?? `hk-value-${autoId}`;
|
|
463
|
+
const { ref, supported, counts } = useValueTokens(useMemo(() => query ? [{
|
|
464
|
+
name,
|
|
465
|
+
pattern: query,
|
|
466
|
+
caseSensitive,
|
|
467
|
+
wholeWord,
|
|
468
|
+
priority
|
|
469
|
+
}] : [], [
|
|
470
|
+
name,
|
|
471
|
+
query,
|
|
472
|
+
caseSensitive,
|
|
473
|
+
wholeWord,
|
|
474
|
+
priority
|
|
475
|
+
]), {
|
|
476
|
+
value,
|
|
477
|
+
observe
|
|
478
|
+
});
|
|
479
|
+
const count = counts[name] ?? 0;
|
|
480
|
+
return {
|
|
481
|
+
ref,
|
|
482
|
+
name,
|
|
483
|
+
supported,
|
|
484
|
+
count,
|
|
485
|
+
active: count > 0
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Search inside an `<input>` or `<textarea>` with next/prev navigation.
|
|
490
|
+
*
|
|
491
|
+
* All matches go to `name`, the active one to `${name}-current` at a higher
|
|
492
|
+
* priority, and the control is scrolled just far enough to reveal the active
|
|
493
|
+
* match. Scrolling adjusts the control's own `scrollTop`/`scrollLeft` rather
|
|
494
|
+
* than calling `setSelectionRange()`, so the caret and the user's selection are
|
|
495
|
+
* left untouched.
|
|
496
|
+
*
|
|
497
|
+
* @template T - The form control element type
|
|
498
|
+
* @param query - Text or expression to find. Falsy clears the highlight
|
|
499
|
+
*
|
|
500
|
+
* @example
|
|
501
|
+
* ```tsx
|
|
502
|
+
* import { useState } from 'react'
|
|
503
|
+
* import {
|
|
504
|
+
* HighlightStyles,
|
|
505
|
+
* useValueHighlightSearch,
|
|
506
|
+
* } from '@cbcruk/highlight-kit/react'
|
|
507
|
+
*
|
|
508
|
+
* function NoteSearch() {
|
|
509
|
+
* const [query, setQuery] = useState('')
|
|
510
|
+
* const { ref, count, active, next, prev } =
|
|
511
|
+
* useValueHighlightSearch<HTMLTextAreaElement>(query)
|
|
512
|
+
*
|
|
513
|
+
* return (
|
|
514
|
+
* <>
|
|
515
|
+
* <HighlightStyles />
|
|
516
|
+
* <input value={query} onChange={(e) => setQuery(e.target.value)} />
|
|
517
|
+
* <span>{count === 0 ? '0/0' : `${active + 1}/${count}`}</span>
|
|
518
|
+
* <button onClick={prev}>Prev</button>
|
|
519
|
+
* <button onClick={next}>Next</button>
|
|
520
|
+
* <textarea ref={ref} rows={10} />
|
|
521
|
+
* </>
|
|
522
|
+
* )
|
|
523
|
+
* }
|
|
524
|
+
* ```
|
|
525
|
+
*/
|
|
526
|
+
function useValueHighlightSearch(query, options = {}) {
|
|
527
|
+
const { name = "search", caseSensitive, wholeWord, value, observe = true } = options;
|
|
528
|
+
const controller = useHighlightController();
|
|
529
|
+
const sourceId = useId();
|
|
530
|
+
const ref = useRef(null);
|
|
531
|
+
const registryRef = useRef(null);
|
|
532
|
+
const [spans, setSpans] = useState([]);
|
|
533
|
+
const [active, setActive] = useState(0);
|
|
534
|
+
const [supported, setSupported] = useState(false);
|
|
535
|
+
useIsomorphicLayoutEffect(() => {
|
|
536
|
+
const element = ref.current;
|
|
537
|
+
if (!element) return;
|
|
538
|
+
const registry = createValueRangeRegistry({
|
|
539
|
+
element,
|
|
540
|
+
controller,
|
|
541
|
+
sourceId
|
|
542
|
+
});
|
|
543
|
+
registryRef.current = registry;
|
|
544
|
+
setSupported(registry.supported);
|
|
545
|
+
return () => {
|
|
546
|
+
registryRef.current = null;
|
|
547
|
+
registry.dispose();
|
|
548
|
+
};
|
|
549
|
+
}, [controller, sourceId]);
|
|
550
|
+
useIsomorphicLayoutEffect(() => {
|
|
551
|
+
const element = ref.current;
|
|
552
|
+
if (!element) return;
|
|
553
|
+
const recompute = () => {
|
|
554
|
+
const next = query ? tokenizeValue(element.value, [{
|
|
555
|
+
name,
|
|
556
|
+
pattern: query,
|
|
557
|
+
caseSensitive,
|
|
558
|
+
wholeWord
|
|
559
|
+
}]).map(({ start, end }) => ({
|
|
560
|
+
start,
|
|
561
|
+
end
|
|
562
|
+
})) : [];
|
|
563
|
+
setSpans((previous) => previous.length === next.length && previous.every((span, i) => span.start === next[i].start && span.end === next[i].end) ? previous : next);
|
|
564
|
+
};
|
|
565
|
+
recompute();
|
|
566
|
+
if (!observe) return;
|
|
567
|
+
element.addEventListener("input", recompute);
|
|
568
|
+
return () => element.removeEventListener("input", recompute);
|
|
569
|
+
}, [
|
|
570
|
+
name,
|
|
571
|
+
query,
|
|
572
|
+
caseSensitive,
|
|
573
|
+
wholeWord,
|
|
574
|
+
value,
|
|
575
|
+
observe
|
|
576
|
+
]);
|
|
577
|
+
useIsomorphicLayoutEffect(() => {
|
|
578
|
+
setActive((a) => spans.length === 0 ? 0 : Math.min(a, spans.length - 1));
|
|
579
|
+
}, [spans]);
|
|
580
|
+
useIsomorphicLayoutEffect(() => {
|
|
581
|
+
const registry = registryRef.current;
|
|
582
|
+
if (!registry) return;
|
|
583
|
+
const current = spans[active];
|
|
584
|
+
registry.commit([{
|
|
585
|
+
name,
|
|
586
|
+
priority: 0,
|
|
587
|
+
spans
|
|
588
|
+
}, ...current ? [{
|
|
589
|
+
name: `${name}-current`,
|
|
590
|
+
priority: 1,
|
|
591
|
+
spans: [current]
|
|
592
|
+
}] : []]);
|
|
593
|
+
}, [
|
|
594
|
+
name,
|
|
595
|
+
spans,
|
|
596
|
+
active
|
|
597
|
+
]);
|
|
598
|
+
useEffect(() => {
|
|
599
|
+
const element = ref.current;
|
|
600
|
+
const current = spans[active];
|
|
601
|
+
if (!element || !current) return;
|
|
602
|
+
const ranges = createValueRanges(element, [current]);
|
|
603
|
+
if (ranges[0]) scrollValueRangeIntoView(element, ranges[0]);
|
|
604
|
+
disconnectValueRanges(ranges);
|
|
605
|
+
}, [spans, active]);
|
|
606
|
+
const next = useCallback(() => setActive((a) => spans.length ? (a + 1) % spans.length : 0), [spans.length]);
|
|
607
|
+
const prev = useCallback(() => setActive((a) => spans.length ? (a - 1 + spans.length) % spans.length : 0), [spans.length]);
|
|
608
|
+
return {
|
|
609
|
+
ref,
|
|
610
|
+
count: spans.length,
|
|
611
|
+
active: spans.length ? active : -1,
|
|
612
|
+
next,
|
|
613
|
+
prev,
|
|
614
|
+
supported
|
|
615
|
+
};
|
|
616
|
+
}
|
|
319
617
|
//#endregion
|
|
320
|
-
export { Highlight, HighlightMatch, HighlightProvider, HighlightRoot, HighlightStyles, highlights, useHighlight, useHighlightController, useHighlightRanges, useHighlightSearch, useHighlightSnapshots, useHighlightState, useHighlightSupport, useTextMatches };
|
|
618
|
+
export { Highlight, HighlightMatch, HighlightProvider, HighlightRoot, HighlightStyles, highlights, useHighlight, useHighlightController, useHighlightRanges, useHighlightSearch, useHighlightSnapshots, useHighlightState, useHighlightSupport, useTextMatches, useValueHighlight, useValueHighlightSearch, useValueTokens };
|