Skip to content

Commit

Permalink
2083 basic string search of full texts and opening the place in annot…
Browse files Browse the repository at this point in the history
…ator (#2459)

* add basic search capabilities

* Add search panel to header

* Fix search scroll in annotator

* Fix searching implementation

---------

Co-authored-by: Ján Mertel <[email protected]>
  • Loading branch information
adammertel and jancimertel authored Dec 2, 2024
1 parent 9277101 commit f8d1d62
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 13 deletions.
32 changes: 31 additions & 1 deletion packages/annotator/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@
</div>
</div>
</div>
<fieldset>
<label>search</label>
<input type="text" id="search" />
<button id="search-trigger">go</button>
<button id="searchTriggerNext">next</button>
</fieldset>
<fieldset>
<label>mode</label>
<select id="mode">
Expand Down Expand Up @@ -92,7 +98,7 @@
<fieldset>
<label>update text</label>
<textarea id="newText" cols="100" rows="10"></textarea>
<button id="scrollToAnchor">Update</button>
<button id="updateText">Update</button>
</fieldset>
</div>

Expand All @@ -109,6 +115,8 @@

const exampleResponse = await fetch("example.txt");
const exampleTxt = await exampleResponse.text();
const searchInput = document.getElementById("search");
const searchTriggerInput = document.getElementById("search-trigger");

let highlightEnabled = false;
const annotator = new Annotator(
Expand Down Expand Up @@ -171,6 +179,7 @@
},
};
});

annotator.onSelectText((text) => {
// console.log("onSelectText", text);
});
Expand Down Expand Up @@ -205,6 +214,27 @@
annotator.updateText(document.getElementById("newText").value);
};

let searchOccurence = 0;
document.getElementById("searchTriggerNext").onclick = () => {
const results = annotator.search(searchInput.value);
if (results.length) {
searchOccurence = searchOccurence + 1;

if (searchOccurence >= results.length) {
searchOccurence = 0;
}

annotator.selectSearchOccurence(results[searchOccurence]);
}
};

searchTriggerInput.onclick = function () {
const results = annotator.search(searchInput.value);
if (results.length) {
annotator.selectSearchOccurence(results[0]);
}
};

window.addEventListener("resize", resizeAnnotator);

function resizeAnnotator() {
Expand Down
53 changes: 53 additions & 0 deletions packages/annotator/src/lib/Annotator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1260,4 +1260,57 @@ export class Annotator {
this.scrollToLine(this.text.noLines - 1);
}
}

search(
toFind: string
): { segmentIndex: number; lineIndex: number; start: number; end: number }[] {
const occurrences = [];

for (const segmentI in this.text.segments) {
for (const lineI in this.text.segments[segmentI].lines) {
let startIndex = 0;
const line = this.text.segments[segmentI].lines[lineI];
while (startIndex < line.length) {
const index = line.indexOf(toFind, startIndex);
if (index === -1) break;

occurrences.push({
segmentIndex: parseInt(segmentI),
lineIndex: parseInt(lineI),
start: index,
end: index + toFind.length,
});
startIndex = index + 1;
}
}
}

return occurrences;
}

selectSearchOccurence(occurence: {
segmentIndex: number;
lineIndex: number;
start: number;
end: number;
}) {
this.cursor.selectStart = {
xLine: occurence.start,
yLine:
this.text.segments[occurence.segmentIndex].lineStart +
occurence.lineIndex,
};
this.cursor.selectEnd = {
xLine: occurence.end,
yLine:
this.text.segments[occurence.segmentIndex].lineStart +
occurence.lineIndex,
};

// this.cursor.xLine = this.cursor.selectStart.xLine;
// this.cursor.yLine = this.cursor.selectStart.yLine - this.viewport.lineStart;

this.scrollToLine(this.cursor.selectStart.yLine);
this.draw();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { toast } from "react-toastify";
import { v4 as uuidv4 } from "uuid";

import { Annotator, EditMode } from "@inkvisitor/annotator/src/lib";
import { EntityEnums } from "@shared/enums";
import { IDocument, IEntity, IResponseDocumentDetail } from "@shared/types";
import { Button } from "components/basic/Button/Button";
import { ButtonGroup } from "components/basic/ButtonGroup/ButtonGroup";
Expand All @@ -23,7 +24,6 @@ import {
StyledScrollerViewport,
} from "./AnnotatorStyles";
import { annotatorHighlight } from "./highlight";
import { EntityEnums } from "@shared/enums";

interface TextAnnotatorProps {
width: number;
Expand Down Expand Up @@ -509,6 +509,7 @@ export const TextAnnotator = ({
}}
/>
)}

<StyledMainCanvas
onMouseDown={() => setIsSelectingText(true)}
onMouseUp={() => setIsSelectingText(false)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import {
StyledHeaderBreadcrumbRow,
StyledHeaderRow,
StyledHeading,
StyledModeSwitcher,
StyledInfoText,
StyledMoveToParent,
StyledSuggesterRow,
} from "./StatementListHeaderStyles";
Expand Down Expand Up @@ -508,7 +508,7 @@ export const StatementListHeader: React.FC<StatementListHeader> = ({
)}
</StyledActionsWrapper>

<StyledModeSwitcher>
<StyledInfoText>
{"Mode "}
<ButtonGroup style={{ marginLeft: "5px" }}>
<Button
Expand All @@ -530,7 +530,7 @@ export const StatementListHeader: React.FC<StatementListHeader> = ({
inverted={displayMode === StatementListDisplayMode.LIST}
></Button>
</ButtonGroup>
</StyledModeSwitcher>
</StyledInfoText>
</StyledSuggesterRow>
</StyledHeader>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const StyledFaStar = styled(FaStar)`
margin-right: 0.5rem;
color: ${({ theme }) => theme.color["warning"]};
`;
export const StyledModeSwitcher = styled.div`
export const StyledInfoText = styled.div`
font-size: ${({ theme }) => theme.fontSize["sm"]};
font-weight: ${({ theme }) => theme.fontWeight["normal"]};
display: flex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { EntityEnums, UserEnums } from "@shared/enums";
import { IEntity, IResponseEntity, IResponseStatement } from "@shared/types";
import { useQuery } from "@tanstack/react-query";
import api from "api";
import { Button, Loader } from "components";
import { Button, Input, Loader } from "components";
import Dropdown, { EntitySuggester, EntityTag } from "components/advanced";
import TextAnnotator from "components/advanced/Annotator/Annotator";
import AnnotatorProvider from "components/advanced/Annotator/AnnotatorProvider";
Expand All @@ -16,12 +16,13 @@ import { TiDocumentText } from "react-icons/ti";
import { ThemeContext } from "styled-components";
import { COLLAPSED_TABLE_WIDTH } from "Theme/constants";
import useResizeObserver from "use-resize-observer";
import { StyledModeSwitcher } from "../StatementListHeader/StatementListHeaderStyles";
import { StyledInfoText } from "../StatementListHeader/StatementListHeaderStyles";
import {
StyledDocumentTag,
StyledDocumentTitle,
} from "../StatementLitBoxStyles";
import { entitiesDict } from "@shared/dictionaries/entity";
import { useDebounce } from "hooks";

interface StatementListTextAnnotator {
statements: IResponseStatement[];
Expand Down Expand Up @@ -82,6 +83,52 @@ export const StatementListTextAnnotator: React.FC<
}, []);

const [annotator, setAnnotator] = useState<Annotator | undefined>(undefined);
const [searchTerm, setSearchTerm] = useState<string>("");
const [searchOccurences, setSearchOccurences] = useState<
{ segmentIndex: number; lineIndex: number; start: number; end: number }[]
>([]);
const [searchActiveOccurence, setSearchActiveOccurence] = useState<number>(0);

useEffect(() => {
const newSelectedOccurence = searchOccurences[searchActiveOccurence];

console.log(
`selecting ${searchActiveOccurence} of`,
searchOccurences,
` => ${newSelectedOccurence}`
);

if (newSelectedOccurence) {
annotator?.selectSearchOccurence(newSelectedOccurence);
}
}, [searchActiveOccurence, searchOccurences]);

const dSearchTerm = useDebounce(searchTerm, 1000);

const isSearchTermValid = useMemo<boolean>(() => {
return dSearchTerm.length > 2;
}, [dSearchTerm]);

useEffect(() => {
if (annotator) {
if (isSearchTermValid) {
annotator?.search(searchTerm);
const occurences = annotator?.search(searchTerm);

setSearchOccurences(occurences);

setTimeout(() => {
setSearchActiveOccurence(0);
}, 1000);

// if (occurences.length > 0) {
// annotator?.selectSearchOccurence(
// searchOccurences[searchActiveOccurence]
// );
// }
}
}
}, [dSearchTerm]);

const animatedStyle = useSpring({
opacity: showAnnotator ? 1 : 0,
Expand Down Expand Up @@ -213,6 +260,22 @@ export const StatementListTextAnnotator: React.FC<

const themeContext = useContext(ThemeContext);

const isSearchAllowed = useMemo<boolean>(() => {
return annotator !== undefined && selectedDocument !== undefined;
}, [annotator, selectedDocument]);

const annotatorHeight = useMemo<number>(() => {
let height = contentHeight - 70;

if (isSearchAllowed) {
height -= 30;
}
if (selectorHeight) {
height -= selectorHeight;
}
return height;
}, [contentHeight, selectorHeight, isSearchAllowed]);

return (
<animated.div style={animatedStyle}>
<div
Expand Down Expand Up @@ -324,14 +387,14 @@ export const StatementListTextAnnotator: React.FC<
style={{
display: "flex",
alignItems: "center",
gap: "4px",
marginBottom: "5px",
gap: themeContext?.space[6],
marginBottom: themeContext?.space[2],
}}
ref={selectorRef}
>
<StyledModeSwitcher style={{ textWrap: "nowrap" }}>
<StyledInfoText style={{ textWrap: "nowrap" }}>
Highlight
</StyledModeSwitcher>
</StyledInfoText>
<Dropdown.Multi.Entity
options={entitiesDict}
disableEmpty={true}
Expand All @@ -350,6 +413,66 @@ export const StatementListTextAnnotator: React.FC<
</div>
)}

{isSearchAllowed && (
<div
style={{
display: "flex",
alignItems: "center",
gap: themeContext?.space[6],
marginBottom: themeContext?.space[2],
}}
>
<StyledInfoText style={{ textWrap: "nowrap", marginRight: "13px" }}>
Search
</StyledInfoText>
<Input
value={searchTerm}
onChangeFn={(newText) => {
setSearchTerm(newText);
}}
changeOnType
/>
{isSearchTermValid && (
<div
style={{
display: "flex",
alignItems: "center",
gap: themeContext?.space[2],
}}
>
<div
style={{
color: themeContext?.color["info"],
fontSize: themeContext?.fontSize["xs"],
}}
>
{searchActiveOccurence + 1} / {searchOccurences.length}{" "}
occurences
</div>
<Button
label="⬇"
color="info"
onClick={() => {
const nextOccurence =
(searchActiveOccurence + 1) % searchOccurences.length;
setSearchActiveOccurence(nextOccurence);
}}
/>
<Button
label="⬆"
color="info"
onClick={() => {
const prevOccurence =
(searchActiveOccurence - 1 + searchOccurences.length) %
searchOccurences.length;
setSearchActiveOccurence(prevOccurence);
}}
/>
</div>
)}
</div>
)}

{/* Annotator */}
<div style={{ marginTop: "0.2rem" }}>
<AnnotatorProvider>
Expand All @@ -367,7 +490,7 @@ export const StatementListTextAnnotator: React.FC<
thisTerritoryEntityId={territoryId}
initialScrollEntityId={territoryId}
displayLineNumbers={true}
height={contentHeight - selectorHeight - 70}
height={annotatorHeight}
documentId={selectedDocumentId}
handleCreateStatement={handleCreateStatement}
handleCreateTerritory={handleCreateTerritory}
Expand Down

0 comments on commit f8d1d62

Please sign in to comment.