import React, { useRef, MouseEventHandler, useCallback, useEffect, useMemo } from 'react' import ReactDOM from 'react-dom/client' import { deserializeRange, serializeRange } from '../../libs/serialize' import { generateId } from '../../libs/uid' import { getPopoverElement, getSpanElement } from '../../libs/wrapperElements' import DefaultPopover from '../DeafultPopover' import { useSelections } from '../../hooks/UseSelection' import { SelectionType, PopoverChildrentype } from '../../types' import { defaultMinSelectionLength, defaultSelectionWrapperClassName } from '../../constants/constants' import { addHighlight, isHighlightable } from '../../libs/dom' import { getOriginalRange, getRangeStartEndContainerText } from '../../libs/createRange' import { sortByPositionAndOffset } from '../../libs/sort' type BaseHighlighterProps = { htmlString: string minSelectionLength?: number maxSelectionLength?: number className?: string selectionWrapperClassName?: string PopoverClassName?: string PopoverChildren?: PopoverChildrentype disablePopover?: boolean disableMultiColorHighlight?: boolean identifier: string onClickHighlight?: (selection: SelectionType, event: MouseEvent) => void onClick?: MouseEventHandler onSelection?: (selection: SelectionType) => void onCopy?: (selection: SelectionType) => void } const extractHighlightsFromHtml = ( htmlString: string, identifier: string, ): { cleanedHtml: string; initialSelections: Record } => { const preprocessedHtml = htmlString .replace(/]*)(\sid="selection-[^"]+")([^>]*)>/gi, '') .replace(/<\/div>/gi, '') const parser = new DOMParser() const doc = parser.parseFromString(preprocessedHtml, 'text/html') const initialSelections: Record = {} const highlightElements = doc.querySelectorAll('span[id^="selection-"]') const data: SelectionType[] = [] highlightElements.forEach((el) => { const id = el.getAttribute('id') || `selection-${generateId()}` const className = el.className || defaultSelectionWrapperClassName const text = el.textContent || '' const parent = el.parentElement if (parent && text) { const prevSibling = el.previousSibling const nextSibling = el.nextSibling let leadingSpace = '' let trailingSpace = '' if (prevSibling?.nodeType === Node.TEXT_NODE) { leadingSpace = prevSibling.textContent?.match(/\s+$/)?.pop() || '' prevSibling.textContent = prevSibling.textContent?.replace(/\s+$/, '') || '' } if (nextSibling?.nodeType === Node.TEXT_NODE) { trailingSpace = nextSibling.textContent?.match(/^\s+/)?.pop() || '' nextSibling.textContent = nextSibling.textContent?.replace(/^\s+/, '') || '' } const textNode = doc.createTextNode(`${leadingSpace}${text}${trailingSpace}`) parent.replaceChild(textNode, el) const range = doc.createRange() range.selectNodeContents(textNode) // const meta = serializeRange(range, parent); const meta = el.getAttribute('data-meta') || serializeRange(range, parent) data.push({ id, text, className, meta, startContainerText: text, endContainerText: text, }) } }) initialSelections[identifier] = data const cleanedHtml = doc.body.innerHTML.trim() // const cleanedHtml = `
` +doc.body.innerHTML.trim() + `
`; console.log(doc.body.innerHTML, 'doc.body.innerHTML cleanedHtml', identifier, initialSelections) // const cleanedHtml = `

hhhhh

question 101

question
` return { cleanedHtml, initialSelections } } export const Highlighter: React.FC = ({ htmlString, onClickHighlight, disablePopover, maxSelectionLength, minSelectionLength, className, PopoverChildren, PopoverClassName, selectionWrapperClassName, onSelection, onClick, onCopy, disableMultiColorHighlight, identifier, // selections, }) => { console.log(htmlString, 'removal string cleandHtml') const { cleanedHtml, initialSelections } = useMemo( () => extractHighlightsFromHtml(htmlString, identifier), [htmlString], ) const { selections, setSelections, addSelection, removeSelection, updateSelection } = useSelections() // const rootRef = useRef(null) // const tempRef = useRef(null) // const div = document.createElement('div') // tempRef.current = div // tempRef.current.innerHTML = htmlString useEffect(() => { console.log('cleanedHtml comming here0', initialSelections, selections, cleanedHtml) if ( (!selections[identifier] || selections[identifier]?.length === 0) && initialSelections[identifier]?.length > 0 ) { console.log('cleanedHtml comming here1', initialSelections) setSelections(initialSelections) } }, [initialSelections]) const rootRef = useRef(null) const tempRef = useRef(null) const div = document.createElement('div') tempRef.current = div tempRef.current.innerHTML = cleanedHtml console.log(cleanedHtml, 'cleanedHtml', selections, 'initialSelections', initialSelections) const handleHoverAndClickEffects = () => { if (!rootRef.current) return rootRef.current.querySelectorAll('.hover-content-mark').forEach((mark) => { const uniqueId = mark.getAttribute('data-hover-id') const shortHtml = mark.getAttribute('data-short-html') const longHtml = mark.getAttribute('data-long-html') if (!uniqueId || (!shortHtml && !longHtml)) return const markElement = mark as HTMLElement if (shortHtml && shortHtml.trim() !== '') { const popup = document.createElement('div') popup.className = 'hover-content-popup' popup.innerHTML = shortHtml + (longHtml ? `` : '') markElement.appendChild(popup) let timeout: any const showPopup = () => { clearTimeout(timeout) popup.style.display = 'block' } const hidePopup = () => { timeout = setTimeout(() => { popup.style.display = 'none' }, 100) } markElement.addEventListener('mouseenter', showPopup) markElement.addEventListener('mouseleave', hidePopup) popup.addEventListener('mouseenter', showPopup) popup.addEventListener('mouseleave', hidePopup) popup.querySelector('.view-more')?.addEventListener('click', () => { const modal = document.createElement('div') modal.className = 'modal' modal.innerHTML = ` ` document.body.appendChild(modal) modal.querySelector('.close-button')?.addEventListener('click', () => { document.body.removeChild(modal) }) }) } else { markElement.addEventListener('click', (event) => { event.preventDefault() const modal = document.createElement('div') modal.className = 'modal' modal.innerHTML = ` ` document.body.appendChild(modal) modal.querySelector('.close-button')?.addEventListener('click', () => { document.body.removeChild(modal) }) }) } }) } const getWrapper = useCallback( (selection: SelectionType) => { const span = getSpanElement({ className: selection?.className || defaultSelectionWrapperClassName, meta: selection?.meta || '', }) if (!disablePopover) { const popover = getPopoverElement({ className: PopoverClassName }) if (!PopoverClassName) { span.onmouseover = () => { popover.style.visibility = 'visible' popover.style.opacity = '1' } span.onmouseout = () => { popover.style.visibility = 'hidden' popover.style.opacity = '0' } } popover.id = `pop-${selection.id}` span.appendChild(popover) } if (onClickHighlight) { span.onclick = (e) => onClickHighlight(selection, e) } span.id = selection.id return span }, [PopoverClassName, disablePopover, onClickHighlight], ) const handleMouseUp: MouseEventHandler = () => { // e.stopPropagation() const selection = window.getSelection() if (!selection) return if (!minSelectionLength) { minSelectionLength = defaultMinSelectionLength } if (minSelectionLength && selection?.toString().length < minSelectionLength) return if (maxSelectionLength && selection?.toString().length > maxSelectionLength) return const range = selection.getRangeAt(0) if (!isHighlightable(range)) return const expRange = getOriginalRange(range, tempRef.current!) if (!expRange) return const { startContainerText, endContainerText } = getRangeStartEndContainerText(range) const newSelection: SelectionType = { meta: serializeRange(expRange, tempRef.current!), text: range?.toString(), id: `selection-${generateId()}`, className: selectionWrapperClassName || defaultSelectionWrapperClassName, startContainerText, endContainerText, } addSelection(newSelection, identifier) onSelection && onSelection(newSelection) console.log('cleanedHtml selection --?/p', selections) } function manageCopy(selection: SelectionType) { // const span = getSpanElement({ // className: selection.className || defaultSelectionWrapperClassName, // }) console.log(selection, 'Coping') onCopy && onCopy(selection) } useEffect(() => { const sortedSelections = sortByPositionAndOffset(selections[identifier]) if (!rootRef.current) return rootRef.current.innerHTML = '' // rootRef.current.innerHTML = htmlString rootRef.current.innerHTML = cleanedHtml handleHoverAndClickEffects() if (sortedSelections && sortedSelections?.length) { for (let i = 0; i < sortedSelections.length; i++) { const item = sortedSelections[i] as SelectionType const range = deserializeRange(item.meta, rootRef.current!) if (range) { addHighlight(range, getWrapper(item)) // onHiglightChange && onHiglightChange('') } const popoverRoot = document.getElementById(`pop-${item.id}`) if (!popoverRoot) return const root = ReactDOM.createRoot(popoverRoot) if (PopoverChildren) { root.render( manageCopy(selection)} disableMultiColorHighlight={disableMultiColorHighlight} identifier={identifier} />, ) } else { root.render( manageCopy(selection)} disableMultiColorHighlight={disableMultiColorHighlight} identifier={identifier} />, ) } } } }, [ selections, getWrapper, PopoverChildren, // htmlString, cleanedHtml, removeSelection, updateSelection, disableMultiColorHighlight, identifier, ]) const memoizedChildren = useMemo(() => { return (
) }, [onClick, handleMouseUp, className, identifier]) return memoizedChildren }