diff --git a/app/src/pages/Editor/Document.tsx b/app/src/pages/Editor/Document.tsx index 98a0ea70..609cec81 100644 --- a/app/src/pages/Editor/Document.tsx +++ b/app/src/pages/Editor/Document.tsx @@ -2,7 +2,7 @@ import { useDispatch, useSelector } from 'react-redux'; import { RootState } from '../../state'; import { V3TimedDocumentItem } from '../../core/document'; import * as React from 'react'; -import { KeyboardEventHandler, MouseEventHandler, RefObject, useEffect, useRef } from 'react'; +import { KeyboardEventHandler, MouseEventHandler, RefObject, useEffect } from 'react'; import { Cursor } from './Cursor'; import { Paragraph } from './Paragraph'; import { basename, extname } from 'path'; @@ -46,7 +46,7 @@ const DocumentContainer = styled.div<{ displaySpeakerNames: boolean }>` } `; -export function Document(): JSX.Element { +export function Document({ documentRef }: { documentRef: RefObject }): JSX.Element { const dispatch = useDispatch(); const content = useSelector((state: RootState) => state.editor.present ? memoizedTimedDocumentItems(state.editor.present.document.content) : [] @@ -67,18 +67,18 @@ export function Document(): JSX.Element { }); const speakerColorIndices = memoizedSpeakerIndices(contentMacros); - const ref = useRef(null); const theme: Theme = useTheme(); useEffect(() => { - ref.current && ref.current.focus(); - }, [ref.current]); + console.log('setting current focus'); + documentRef.current ? documentRef.current.focus() : {}; + }, [documentRef.current]); const mouseDownHandler: MouseEventHandler = (e) => { if (e.detail != 1 || e.buttons != 1) return; e.preventDefault(); - ref.current?.focus(); // sometimes we loose focus and then it is nice to be able to gain it back + documentRef.current?.focus(); // sometimes we loose focus and then it is nice to be able to gain it back if (e.detail == 1 && !e.shiftKey) { handleWordClick(dispatch, content, e); @@ -135,7 +135,7 @@ export function Document(): JSX.Element { return ( - - + + dispatch(setFilterPopup(true))} accelerator={'CommandOrControl+Shift+F'} /> + + dispatch(toggleSearchOverlay())} + accelerator={'CommandOrControl+F'} + /> x.type == 'text') + .map((x) => x.text) + .join(' '); +} +function filterContentParagraphLevel( + content: V3DocumentItem[], + tester: (_a0: string) => boolean +): V3DocumentItem[] { + const filteredContent: V3DocumentItem[] = []; + let para = []; + for (const item of content) { + if (item.type !== 'paragraph_end') { + para.push(item); + } else { + const text = paraToText(para); + if (tester(text)) { + filteredContent.push(para[0]); + } + para = []; + } + } + return filteredContent; +} + +function filterContentWordLevel( + content: V3DocumentItem[], + tester: (_a0: string) => boolean +): V3DocumentItem[] { + const filteredContent: V3DocumentItem[] = []; + for (const item of content) { + if (item.type == 'text' && tester(item.text)) { + filteredContent.push(item); + } + } + return filteredContent; +} + +function getScrollToItem( + content: V3DocumentItem[], + { + searchString, + level, + }: { + searchString: string; + level: 'word' | 'paragraph'; + } +): V3DocumentItem | null { + switch (level) { + case 'paragraph': { + const filteredParas = filterContentParagraphLevel(content, (s: string) => + s.includes(searchString) + ); + if (filteredParas.length > 0) { + return filteredParas[0]; + } + return null; + } + case 'word': { + const filteredParas = filterContentWordLevel(content, (s: string) => + s.includes(searchString) + ); + if (filteredParas.length > 0) { + return filteredParas[0]; + } + return null; + } + } +} +export function SearchOverlay(): JSX.Element { + const popupState = useSelector((state: RootState) => state.editor.present?.showSearchOverlay); + + const [formState, setFormState] = useState<{ + searchString: string; + level: 'word' | 'paragraph'; + caseInsensitive: boolean; + useRegex: boolean; + }>({ searchString: '', level: 'paragraph', caseInsensitive: false, useRegex: false }); + + const matchElement = useSelector((state: RootState) => + getScrollToItem(state.editor.present?.document.content || [], formState) + ); + + if (matchElement) { + console.log(matchElement); + const firstMachtItem = document.getElementById(`item-${matchElement.uuid}`); + const scrollContainer = document.getElementById('scroll-container'); + if (firstMachtItem && scrollContainer) { + setTimeout(() => { + firstMachtItem.style.backgroundColor = 'rgba(255,255,0,0.2)'; + scrollContainer.scrollTo({ top: firstMachtItem.offsetTop - 30, behavior: 'smooth' }); + }); + } + } + + return popupState ? ( + <> + ) => { + console.log(`val: "${e.target.value}"`); + setFormState((state) => ({ ...state, searchString: e.target.value })); + }} + /> + + ) : ( + <> + ); +} diff --git a/app/src/pages/Editor/index.tsx b/app/src/pages/Editor/index.tsx index f120a320..14c98954 100644 --- a/app/src/pages/Editor/index.tsx +++ b/app/src/pages/Editor/index.tsx @@ -4,7 +4,7 @@ import styled from 'styled-components'; import { EditorTitleBar } from './TitleBar'; import { Document } from './Document'; import { Player } from './Player'; -import { KeyboardEventHandler } from 'react'; +import { KeyboardEventHandler, useRef } from 'react'; import { useDispatch } from 'react-redux'; import { ExportDocumentDialog } from './ExportDocumentDialog'; import { EditorTour } from '../../tour/EditorTour'; @@ -12,6 +12,7 @@ import { togglePlaying } from '../../state/editor/play'; import { insertParagraphEnd } from '../../state/editor/edit'; import { EditorMenuBar } from './MenuBar'; import { FilterDialog } from './Filter'; +import { SearchOverlay } from './Search'; const MainContainer = styled(MainCenterColumn)` justify-content: start; @@ -23,12 +24,17 @@ const MainContainer = styled(MainCenterColumn)` `; export function EditorPage(): JSX.Element { const dispatch = useDispatch(); + + const documentRef = useRef(null); const handleKeyPress: KeyboardEventHandler = (e) => { - if (e.key === ' ') { - dispatch(togglePlaying()); - e.preventDefault(); - } else if (e.key === 'Enter') { - dispatch(insertParagraphEnd()); + if (document.activeElement?.tagName !== 'INPUT') { + // ignore key presses going to input fields + if (e.key === ' ') { + dispatch(togglePlaying()); + e.preventDefault(); + } else if (e.key === 'Enter') { + dispatch(insertParagraphEnd()); + } } }; @@ -41,9 +47,10 @@ export function EditorPage(): JSX.Element { + - + ); diff --git a/app/src/state/editor/display.ts b/app/src/state/editor/display.ts index 3d7f1615..3df3febb 100644 --- a/app/src/state/editor/display.ts +++ b/app/src/state/editor/display.ts @@ -43,6 +43,13 @@ export const setFilterPopup = createActionWithReducer( } ); +export const toggleSearchOverlay = createActionWithReducer( + 'editor/toggleSearchOverlay', + (state) => { + state.showSearchOverlay = !state.showSearchOverlay; + } +); + export const setExportState = createActionWithReducer< EditorState, { running: boolean; progress: number } diff --git a/app/src/state/editor/types.ts b/app/src/state/editor/types.ts index f2cf9ce5..c2bec3e4 100644 --- a/app/src/state/editor/types.ts +++ b/app/src/state/editor/types.ts @@ -48,6 +48,7 @@ export interface EditorState { exportPopup: ExportPopupState; filterPopup: boolean; + showSearchOverlay: boolean; transcriptCorrectionState: string | null; } @@ -80,6 +81,7 @@ export const defaultEditorState: EditorState = { exportPopup: 'hidden', filterPopup: false, + showSearchOverlay: false, transcriptCorrectionState: null, };