g (find all) and i{" "}
17 | (case-insensitive) flags if you need them.
18 |
19 | }
20 | initialValue={initialValue}
21 | highlight={/dogs?|cats?|g(oo|ee)se|land\s+sharks?/gi}
22 | code={code}
23 | codeSandbox="rhwta-regexp-5ois8"
24 | />
25 | );
26 | };
27 |
28 | export { Regexp };
29 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ## Prerequisites
2 |
3 | [Node.js](http://nodejs.org/) >= 10 must be installed.
4 |
5 | ## Installation
6 |
7 | - Running `npm install` in the component's root directory will install everything you need for development.
8 |
9 | ## Demo Development Server
10 |
11 | - `npm start` will run a development server with the component's demo app at [http://localhost:3000](http://localhost:3000) with hot module reloading.
12 |
13 | ## Running Tests
14 |
15 | - `npm test` will run the tests once.
16 |
17 | - `npm run test:coverage` will run the tests and produce a coverage report in `coverage/`.
18 |
19 | - `npm run test:watch` will run the tests on every change.
20 |
21 | ## Building
22 |
23 | - `npm run build` will build the component for publishing to npm and also bundle the demo app.
24 |
25 | - `npm run clean` will delete built resources.
26 |
--------------------------------------------------------------------------------
/demo/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "RHWTA",
3 | "name": "React Highlight Within Textarea",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "128x128 96x96 32x32 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "favicon-128x128.png",
12 | "type": "image/png",
13 | "sizes": "128x128"
14 | },
15 | {
16 | "src": "favicon-96x96.png",
17 | "type": "image/png",
18 | "sizes": "96x96"
19 | },
20 | {
21 | "src": "favicon-32x32.png",
22 | "type": "image/png",
23 | "sizes": "32x32"
24 | },
25 | {
26 | "src": "favicon-16x16.png",
27 | "type": "image/png",
28 | "sizes": "16x16"
29 | }
30 | ],
31 | "start_url": ".",
32 | "display": "standalone",
33 | "theme_color": "#000000",
34 | "background_color": "#ffffff"
35 | }
36 |
--------------------------------------------------------------------------------
/src/getType.tsx:
--------------------------------------------------------------------------------
1 | import { Highlight } from './types'
2 |
3 |
4 | // returns identifier strings that aren't necessarily "real" JavaScript types
5 | export default function getType(instance?: Highlight) {
6 | let type = typeof instance;
7 | if (!instance) {
8 | return "falsey";
9 | } else if (Array.isArray(instance)) {
10 | if (
11 | instance.length === 2 &&
12 | typeof instance[0] === "number" &&
13 | typeof instance[1] === "number"
14 | ) {
15 | return "range";
16 | } else {
17 | return "array";
18 | }
19 | } else if (type === "object") {
20 | if (instance instanceof RegExp) {
21 | return "regexp";
22 | } else if (instance.hasOwnProperty("highlight")) {
23 | return "custom";
24 | }
25 | } else if (type === "function") {
26 | return "strategy"
27 | } else if (type === "string") {
28 | return type;
29 | }
30 |
31 | return "other";
32 | }
33 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "compounds": [
7 | {
8 | "name": "start & debug",
9 | "configurations": ["yarn start", "attach"]
10 | }
11 | ],
12 | "configurations": [
13 | {
14 | "command": "yarn start",
15 | "name": "yarn start",
16 | "request": "launch",
17 | "type": "node-terminal"
18 | },
19 | {
20 | "command": "yarn test",
21 | "name": "yarn test",
22 | "request": "launch",
23 | "type": "node-terminal"
24 | },
25 | {
26 | "type": "chrome",
27 | "name": "attach",
28 | "request": "launch",
29 | "cwd": "${workspaceFolder}/demo",
30 | "url": "http://localhost:3000/"
31 | }
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/demo/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import { createHashRouter, RouterProvider } from "react-router-dom";
4 | import App from "./App";
5 | import { Performance } from "./Performance";
6 | import { DraftPerf } from "./DraftPerf";
7 | import { QuillPerf } from "./QuillPerf";
8 | import "./App.css"
9 |
10 | const root = ReactDOM.createRoot(
11 | document.getElementById("root") as HTMLElement
12 | );
13 |
14 | const router = createHashRouter([
15 | {
16 | path: "/",
17 | element: highlight and className properties. This lets you
8 | set CSS classes in the highlight markup for custom styling, such as changing
9 | the highlight color.
10 |
11 | );
12 |
13 | const code = `(added a matching ),
34 | the cursor would move to after the ) even if the desire
35 | was to allow text within the ().
36 |
37 | }
38 | initialValue="POTATO TOMAT <- ADD AN O HERE"
39 | highlight="TOMAT"
40 | onChange={(value) => {
41 | return value.toUpperCase().replaceAll("TOMATO", "ONION");
42 | }}
43 | code={code}
44 | codeSandbox="rhwta-dynamic-change-t869w"
45 | />
46 | >
47 | );
48 | };
49 |
50 | export { ChangeValue };
51 |
--------------------------------------------------------------------------------
/src/breakSpansByBlocks.ts:
--------------------------------------------------------------------------------
1 | import { ContentState } from "draft-js";
2 | import { BlockSpan } from "./types";
3 | import { Find } from "./types";
4 |
5 | interface BlockData {
6 | blockStart: number,
7 | blockEnd: number,
8 | blockText: string,
9 | blockKey: string,
10 | }
11 |
12 | const extractBlockData = (contentState: ContentState, text: string): BlockData[] => {
13 | let blocks = contentState.getBlocksAsArray();
14 | let blockData = [];
15 | let blockEnd = 0;
16 | for (const block of blocks) {
17 | let blockLength = block.getLength();
18 | if (blockLength == 0) {
19 | continue;
20 | }
21 | let blockText = block.getText();
22 | let blockStart = text.indexOf(blockText[0], blockEnd);
23 | blockEnd = blockStart + blockLength;
24 | blockData.push({
25 | blockStart: blockStart,
26 | blockEnd: blockEnd,
27 | blockText: text.slice(blockStart, blockEnd),
28 | blockKey: block.getKey(),
29 | });
30 | }
31 | return blockData;
32 | };
33 |
34 | export const breakSpansByBlocks = (
35 | contentState: ContentState,
36 | matches: Find[],
37 | text: string
38 | ): BlockSpan[] => {
39 | const blockData = extractBlockData(contentState, text);
40 | let newSpans = [];
41 | loop: for (const match of matches) {
42 | for (const block of blockData) {
43 | if (block.blockStart >= match.matchEnd) {
44 | continue loop;
45 | }
46 | if (block.blockEnd < match.matchStart) {
47 | continue;
48 | }
49 | const spanStart = Math.max(match.matchStart, block.blockStart);
50 | const spanEnd = Math.min(match.matchEnd, block.blockEnd);
51 | const spanText = text.slice(spanStart, spanEnd);
52 |
53 | newSpans.push({
54 | text: text,
55 | ...match,
56 | ...block,
57 | spanStart: spanStart,
58 | spanEnd: spanEnd,
59 | spanText: spanText,
60 | });
61 | }
62 | }
63 | return newSpans;
64 | };
65 |
66 |
--------------------------------------------------------------------------------
/demo/src/Example.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useState } from "react";
3 | import Row from "react-bootstrap/Row";
4 | import Col from "react-bootstrap/Col";
5 | import {
6 | HighlightWithinTextarea,
7 | Selection,
8 | Highlight,
9 | } from "react-highlight-within-textarea";
10 | import { Code } from "./Code";
11 | import { CodeSandbox } from "./CodeSandbox";
12 |
13 | const Example = (props: {
14 | title: string;
15 | text: JSX.Element | string;
16 | initialValue: string;
17 | highlight: Highlight;
18 | code: string;
19 | codeSandbox: string;
20 | onChange?: (nextValue: string, selection?: Selection) => string;
21 | selection?: Selection;
22 | }) => {
23 | let {
24 | title,
25 | text,
26 | initialValue,
27 | highlight,
28 | code,
29 | codeSandbox,
30 | onChange,
31 | selection,
32 | } = props;
33 | const [value, setValue] = useState
65 | {JSON.stringify(delta, null, 2)}
89 | {JSON.stringify(prevDelta, null, 2)}
91 | {JSON.stringify({ ...props, ...objects }, 0, 1)};
20 | return (
21 | {JSON.stringify({ ...props, ...objects }, null, 1)}
64 | );
65 | return (
66 |
110 |