import { CKTextEditor, ModelText } from "@triliumnext/ckeditor5"; import { useCallback, useEffect, useState } from "preact/hooks"; import { t } from "../../services/i18n"; import { useActiveNoteContext, useContentElement, useIsNoteReadOnly, useNoteProperty, useTextEditor } from "../react/hooks"; import RightPanelWidget from "./RightPanelWidget"; interface RawHighlight { id: string; text: string; attrs: { bold: boolean; italic: boolean; underline: boolean; color: string | undefined; background: string | undefined; } } export default function HighlightsList() { const { note, noteContext } = useActiveNoteContext(); const noteType = useNoteProperty(note, "type"); const { isReadOnly } = useIsNoteReadOnly(note, noteContext); return (noteType === "text") && ( {noteType === "text" && isReadOnly && } {noteType === "text" && !isReadOnly && } ); } function AbstractHighlightsList({ highlights, scrollToHighlight }: { highlights: T[], scrollToHighlight(highlight: T): void; }) { return (
    {highlights.map(highlight => (
  1. scrollToHighlight(highlight)} > {highlight.text}
  2. ))}
); } //#region Editable text (CKEditor) interface CKHighlight extends RawHighlight { textNode: ModelText; offset: number | null; } function EditableTextHighlightsList() { const { note, noteContext } = useActiveNoteContext(); const textEditor = useTextEditor(noteContext); const [ highlights, setHighlights ] = useState([]); useEffect(() => { if (!textEditor) return; setHighlights(extractHighlightsFromTextEditor(textEditor)); // React to changes. const changeCallback = () => { const changes = textEditor.model.document.differ.getChanges(); const affectsHighlights = changes.some(change => { // Text inserted or removed if (change.type === 'insert' || change.type === 'remove') { return true; } // Formatting attribute changed if (change.type === 'attribute' && ( change.attributeKey === 'bold' || change.attributeKey === 'italic' || change.attributeKey === 'underline' || change.attributeKey === 'fontColor' || change.attributeKey === 'fontBackgroundColor' ) ) { return true; } return false; }); if (affectsHighlights) { setHighlights(extractHighlightsFromTextEditor(textEditor)); } }; textEditor.model.document.on("change:data", changeCallback); return () => textEditor.model.document.off("change:data", changeCallback); }, [ textEditor, note ]); const scrollToHeading = useCallback((highlight: CKHighlight) => { if (!textEditor) return; const modelPos = textEditor.model.createPositionAt(highlight.textNode, "before"); const viewPos = textEditor.editing.mapper.toViewPosition(modelPos); const domConverter = textEditor.editing.view.domConverter; const domPos = domConverter.viewPositionToDom(viewPos); if (!domPos) return; if (domPos.parent instanceof HTMLElement) { domPos.parent.scrollIntoView(); } else if (domPos.parent instanceof Text) { domPos.parent.parentElement?.scrollIntoView(); } }, [ textEditor ]); return ; } function extractHighlightsFromTextEditor(editor: CKTextEditor) { const result: CKHighlight[] = []; const root = editor.model.document.getRoot(); if (!root) return []; for (const { item } of editor.model.createRangeIn(root).getWalker({ ignoreElementEnd: true })) { if (!item.is('$textProxy') || !item.data.trim()) continue; const attrs: RawHighlight["attrs"] = { bold: item.hasAttribute('bold'), italic: item.hasAttribute('italic'), underline: item.hasAttribute('underline'), color: item.getAttribute('fontColor') as string | undefined, background: item.getAttribute('fontBackgroundColor') as string | undefined }; if (Object.values(attrs).some(Boolean)) { result.push({ id: crypto.randomUUID(), text: item.data, attrs, textNode: item.textNode, offset: item.startOffset }); } } return result; } //#endregion //#region Read-only text interface DomHighlight extends RawHighlight { element: HTMLElement; } function ReadOnlyTextHighlightsList() { const { noteContext } = useActiveNoteContext(); const contentEl = useContentElement(noteContext); const highlights = extractHeadingsFromStaticHtml(contentEl); const scrollToHighlight = useCallback((highlight: DomHighlight) => { highlight.element.scrollIntoView(); }, []); return ; } function extractHeadingsFromStaticHtml(el: HTMLElement | null) { if (!el) return []; const { color: defaultColor, backgroundColor: defaultBackgroundColor } = getComputedStyle(el); const walker = document.createTreeWalker( el, NodeFilter.SHOW_TEXT, null ); const highlights: DomHighlight[] = []; let node: Node | null; while ((node = walker.nextNode())) { const el = node.parentElement; if (!el || !node.textContent?.trim()) continue; const style = getComputedStyle(el); if ( el.closest('strong, em, u') || style.color !== defaultColor || style.backgroundColor !== defaultBackgroundColor ) { const attrs: RawHighlight["attrs"] = { bold: !!el.closest("strong"), italic: !!el.closest("em"), underline: !!el.closest("u"), background: el.style.backgroundColor, color: el.style.color }; if (Object.values(attrs).some(Boolean)) { highlights.push({ id: crypto.randomUUID(), text: node.textContent, element: el, attrs }); } } } return highlights; } //#endregion