From 139e2f6089d773f5497a17f0a6444ac3d9178365 Mon Sep 17 00:00:00 2001 From: JesseLiii Date: Mon, 22 Jul 2024 20:54:55 -0700 Subject: [PATCH] feat: moved container to be based on useEffect() --- package-lock.json | 13 +++ package.json | 2 + src/scripts/highlighter/HighlighterApp.tsx | 100 +++++++++-------- .../highlighter/utils/createHighlight.tsx | 103 ++++++++++++++++++ 4 files changed, 172 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4fa51cc..6b2cf72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "cmdk": "^1.0.0", + "lodash": "^4.17.21", "lucide-react": "^0.365.0", "rangy": "^1.3.1", "react": "^18.2.0", @@ -29,6 +30,7 @@ "devDependencies": { "@crxjs/vite-plugin": "^2.0.0-beta.23", "@types/chrome": "^0.0.266", + "@types/lodash": "^4.17.7", "@types/node": "^20.12.4", "@types/react": "^18.2.55", "@types/react-dom": "^18.2.19", @@ -2007,6 +2009,12 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true + }, "node_modules/@types/node": { "version": "20.12.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.4.tgz", @@ -3809,6 +3817,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", diff --git a/package.json b/package.json index 308b6db..7f97ce8 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "cmdk": "^1.0.0", + "lodash": "^4.17.21", "lucide-react": "^0.365.0", "rangy": "^1.3.1", "react": "^18.2.0", @@ -31,6 +32,7 @@ "devDependencies": { "@crxjs/vite-plugin": "^2.0.0-beta.23", "@types/chrome": "^0.0.266", + "@types/lodash": "^4.17.7", "@types/node": "^20.12.4", "@types/react": "^18.2.55", "@types/react-dom": "^18.2.19", diff --git a/src/scripts/highlighter/HighlighterApp.tsx b/src/scripts/highlighter/HighlighterApp.tsx index 4c26ea5..501f193 100644 --- a/src/scripts/highlighter/HighlighterApp.tsx +++ b/src/scripts/highlighter/HighlighterApp.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import ActionBar from '@/scripts/highlighter/components/ActionBar/ActionBar'; import { Highlight } from '@/scripts/highlighter/components/Highlight'; @@ -10,6 +10,7 @@ import { getMarkerPosition, getSelectedText, } from '@/scripts/highlighter/utils/markerUtils'; +import { isEqual } from 'lodash'; const HighlighterApp = () => { const initialHighlights: { [key: string]: HighlightData } = {}; @@ -25,6 +26,8 @@ const HighlighterApp = () => { }[]; }>({}); + const prevHighlightsRef = useRef({}); + const [markerPosition, setMarkerPosition] = useState< MarkerPosition | { display: 'none' } >({ display: 'none' }); @@ -57,27 +60,52 @@ const HighlighterApp = () => { }; }, []); + useEffect(() => { + const newContainers: typeof highlightContainers = { + ...highlightContainers, + }; + let hasChanges = false; + + Object.entries(highlights).forEach(([uuid, highlightData]) => { + const prevHighlight = prevHighlightsRef.current[uuid]; + if ( + !prevHighlight || + !isEqual(prevHighlight.matching, highlightData.matching) + ) { + const containers = createHighlightElement(highlightData); + if (containers) { + newContainers[uuid] = containers.map((range) => ({ + range, + uuid, + })); + hasChanges = true; + } + } + }); + + // Remove containers for deleted highlights + Object.keys(highlightContainers).forEach((uuid) => { + if (!highlights[uuid]) { + delete newContainers[uuid]; + hasChanges = true; + } + }); + + if (hasChanges) { + setHighlightContainers(newContainers); + } + + prevHighlightsRef.current = highlights; + }, [highlights]); + const handleEditHighlight = (highlightData: HighlightData) => { setHighlights((prevHighlights) => ({ ...prevHighlights, [highlightData.uuid]: highlightData, })); - // setHighlightContainers((prev) => { - // const newMap = new Map(prev); - // if (newMap.has(highlightData.uuid)) { - // const container = newMap.get(highlightData.uuid); - // if (container) { - // newMap.set(highlightData.uuid, { - // ...container, - // highlightData, - // }); - // } - // } - // return newMap; - // }); }; - const handleHighlight = (openNotes: boolean = false) => { + const handleHighlight = () => { const userSelection = window.getSelection(); if (userSelection) { const highlightData = extractHighlightData(userSelection); @@ -86,29 +114,23 @@ const HighlighterApp = () => { ...highlights, [highlightData.uuid]: highlightData, }); - const containers = createHighlightElement(highlightData); - setHighlightContainers((prev) => { - const newContainers = { ...prev }; - if (!newContainers[highlightData.uuid]) { - newContainers[highlightData.uuid] = []; - } - containers?.forEach((range, index) => { - newContainers[highlightData.uuid].push({ - range: range, - uuid: highlightData.uuid, - notesOpen: - openNotes && index === containers.length - 1, // Open notes for the first container if openNotes is true - }); - }); - return newContainers; - }); } window.getSelection()?.empty(); } }; const handleAddNote = () => { - handleHighlight(true); // Pass true to open notes for the new highlight + const userSelection = window.getSelection(); + if (userSelection) { + const highlightData = extractHighlightData(userSelection); + if (highlightData) { + setHighlights({ + ...highlights, + [highlightData.uuid]: highlightData, + }); + } + window.getSelection()?.empty(); + } }; const handleClose = () => { @@ -124,20 +146,6 @@ const HighlighterApp = () => { ...prevHighlights, [highlightData.uuid]: { ...highlightData, rating }, })); - const containers = createHighlightElement(highlightData); - setHighlightContainers((prev) => { - const newContainers = { ...prev }; - if (!newContainers[highlightData.uuid]) { - newContainers[highlightData.uuid] = []; - } - containers?.forEach((range) => { - newContainers[highlightData.uuid].push({ - range: range, - uuid: highlightData.uuid, - }); - }); - return newContainers; - }); } window.getSelection()?.empty(); } diff --git a/src/scripts/highlighter/utils/createHighlight.tsx b/src/scripts/highlighter/utils/createHighlight.tsx index b5c3ee7..a89fbc7 100644 --- a/src/scripts/highlighter/utils/createHighlight.tsx +++ b/src/scripts/highlighter/utils/createHighlight.tsx @@ -284,3 +284,106 @@ const createHighlight = { }; export const createHighlightElement = createHighlight[strategy]; + +export const checkOverlap = ( + existing: HighlightData, + newHighlight: HighlightData +): boolean => { + const existingStart = existing.matching.textPosition.start; + const existingEnd = existing.matching.textPosition.end; + const newStart = newHighlight.matching.textPosition.start; + const newEnd = newHighlight.matching.textPosition.end; + + return ( + (newStart >= existingStart && newStart <= existingEnd) || + (newEnd >= existingStart && newEnd <= existingEnd) || + (newStart <= existingStart && newEnd >= existingEnd) + ); +}; + +export const extendHighlight = ( + existing: HighlightData, + newHighlight: HighlightData +): HighlightData => { + const start = Math.min( + existing.matching.textPosition.start, + newHighlight.matching.textPosition.start + ); + const end = Math.max( + existing.matching.textPosition.end, + newHighlight.matching.textPosition.end + ); + + // Determine which highlight starts first + const [firstHighlight, secondHighlight] = + existing.matching.textPosition.start < + newHighlight.matching.textPosition.start + ? [existing, newHighlight] + : [newHighlight, existing]; + + // Calculate the overlap + const overlapStart = Math.max( + firstHighlight.matching.textPosition.start, + secondHighlight.matching.textPosition.start + ); + const overlapEnd = Math.min( + firstHighlight.matching.textPosition.end, + secondHighlight.matching.textPosition.end + ); + + // Construct the merged body + let mergedBody = firstHighlight.matching.body; + + if (overlapStart > firstHighlight.matching.textPosition.start) { + // If there's non-overlapping content at the start of the second highlight + mergedBody += secondHighlight.matching.body.slice( + 0, + overlapStart - secondHighlight.matching.textPosition.start + ); + } + + if (overlapEnd < secondHighlight.matching.textPosition.end) { + // If there's non-overlapping content at the end of the second highlight + mergedBody += secondHighlight.matching.body.slice( + overlapEnd - secondHighlight.matching.textPosition.start + ); + } + + return { + ...existing, + matching: { + ...existing.matching, + body: mergedBody, + textPosition: { start, end }, + rangeSelector: { + startOffset: Math.min( + existing.matching.rangeSelector.startOffset, + newHighlight.matching.rangeSelector.startOffset + ), + endOffset: Math.max( + existing.matching.rangeSelector.endOffset, + newHighlight.matching.rangeSelector.endOffset + ), + startContainer: + start === existing.matching.textPosition.start + ? existing.matching.rangeSelector.startContainer + : newHighlight.matching.rangeSelector.startContainer, + endContainer: + end === existing.matching.textPosition.end + ? existing.matching.rangeSelector.endContainer + : newHighlight.matching.rangeSelector.endContainer, + }, + surroundingText: { + prefix: + start === existing.matching.textPosition.start + ? existing.matching.surroundingText.prefix + : newHighlight.matching.surroundingText.prefix, + suffix: + end === existing.matching.textPosition.end + ? existing.matching.surroundingText.suffix + : newHighlight.matching.surroundingText.suffix, + }, + }, + updatedAt: new Date(), + }; +};