|
1849 | 1849 | const matchCase = params?.matchCase === true; |
1850 | 1850 | const backwards = params?.backwards === true; |
1851 | 1851 | const wrap = params?.wrap !== false; |
1852 | | - // Match browser Find semantics across the whole document, including |
1853 | | - // embedded frames. Without searchInFrames, find_text falsely reports a |
1854 | | - // miss for text that Ctrl/Cmd+F would find inside an iframe. |
| 1852 | + // Match browser Find semantics across the whole page, including frames. |
| 1853 | + // When the active match moves into a frame, the top document can retain |
| 1854 | + // an older selection; frame focus keeps that stale range from verifying |
| 1855 | + // the current hit. |
1855 | 1856 | const found = window.find(text, matchCase, backwards, wrap, false, true, false); |
1856 | 1857 | if (!found) { |
1857 | 1858 | return { |
|
1863 | 1864 | error: `find_text: "${text}" was not found on the current page. Re-read the page or try a shorter literal phrase.`, |
1864 | 1865 | }; |
1865 | 1866 | } |
| 1867 | + const activeElement = window.document?.activeElement; |
| 1868 | + const activeElementTag = String(activeElement?.tagName || '').toUpperCase(); |
| 1869 | + const inputType = String(activeElement?.type || 'text').toLowerCase(); |
| 1870 | + const isTextControl = activeElementTag === 'TEXTAREA' |
| 1871 | + || (activeElementTag === 'INPUT' && ['text', 'search', 'url', 'tel', 'email'].includes(inputType)); |
| 1872 | + const normalizedQuery = text.normalize('NFC'); |
| 1873 | + const matchesQuery = (value) => { |
| 1874 | + const normalizedValue = String(value || '').normalize('NFC'); |
| 1875 | + return matchCase |
| 1876 | + ? normalizedValue === normalizedQuery |
| 1877 | + : normalizedValue.toLowerCase() === normalizedQuery.toLowerCase(); |
| 1878 | + }; |
| 1879 | + const hasVisibleBounds = (candidate) => !!candidate |
| 1880 | + && [candidate.x, candidate.y, candidate.width, candidate.height].every(Number.isFinite) |
| 1881 | + && candidate.width > 0 |
| 1882 | + && candidate.height > 0; |
1866 | 1883 | const selection = window.getSelection?.(); |
1867 | | - const selectedText = String(selection?.toString?.() || ''); |
| 1884 | + const documentSelectedText = String(selection?.toString?.() || ''); |
| 1885 | + const documentBounds = selection?.rangeCount |
| 1886 | + ? selection.getRangeAt(0).getBoundingClientRect() |
| 1887 | + : undefined; |
| 1888 | + let controlSelectedText = ''; |
| 1889 | + let controlBounds; |
| 1890 | + const controlSelectionStart = activeElement?.selectionStart; |
| 1891 | + const controlSelectionEnd = activeElement?.selectionEnd; |
| 1892 | + if ( |
| 1893 | + isTextControl |
| 1894 | + && Number.isInteger(controlSelectionStart) |
| 1895 | + && Number.isInteger(controlSelectionEnd) |
| 1896 | + && controlSelectionStart >= 0 |
| 1897 | + && controlSelectionEnd > controlSelectionStart |
| 1898 | + ) { |
| 1899 | + controlSelectedText = String(activeElement.value || '').slice(controlSelectionStart, controlSelectionEnd); |
| 1900 | + controlBounds = activeElement.getBoundingClientRect?.(); |
| 1901 | + } |
| 1902 | + const documentMatchesQuery = matchesQuery(documentSelectedText); |
| 1903 | + const controlMatchesQuery = matchesQuery(controlSelectedText); |
| 1904 | + let selectedText = documentSelectedText; |
| 1905 | + let selectionSource = 'document'; |
| 1906 | + let bounds = documentBounds; |
| 1907 | + if (documentMatchesQuery && hasVisibleBounds(documentBounds)) { |
| 1908 | + // Keep the page selection. A focused field can retain an unrelated |
| 1909 | + // selection while window.find advances to a normal document match. |
| 1910 | + } else if (controlMatchesQuery && hasVisibleBounds(controlBounds)) { |
| 1911 | + selectedText = controlSelectedText; |
| 1912 | + bounds = controlBounds; |
| 1913 | + selectionSource = 'text_control'; |
| 1914 | + } |
1868 | 1915 | let rect; |
1869 | | - if (selection?.rangeCount) { |
1870 | | - const bounds = selection.getRangeAt(0).getBoundingClientRect(); |
| 1916 | + if (bounds) { |
1871 | 1917 | const scrollX = Number.isFinite(Number(window.scrollX)) ? Number(window.scrollX) : 0; |
1872 | 1918 | const scrollY = Number.isFinite(Number(window.scrollY)) ? Number(window.scrollY) : 0; |
1873 | 1919 | rect = { |
|
1879 | 1925 | height: bounds.height, |
1880 | 1926 | }; |
1881 | 1927 | } |
| 1928 | + const hasVisibleRect = !!rect |
| 1929 | + && [rect.x, rect.y, rect.pageX, rect.pageY, rect.width, rect.height].every(Number.isFinite) |
| 1930 | + && rect.width > 0 |
| 1931 | + && rect.height > 0; |
| 1932 | + const selectionMatchesQuery = matchesQuery(selectedText); |
| 1933 | + const selectionInFrame = activeElementTag === 'IFRAME' || activeElementTag === 'FRAME'; |
| 1934 | + const hasSelectionIdentity = selectionSource !== 'text_control' |
| 1935 | + || ( |
| 1936 | + Number.isInteger(controlSelectionStart) |
| 1937 | + && Number.isInteger(controlSelectionEnd) |
| 1938 | + && controlSelectionStart >= 0 |
| 1939 | + && controlSelectionEnd > controlSelectionStart |
| 1940 | + ); |
| 1941 | + const verified = selectionMatchesQuery && hasVisibleRect && hasSelectionIdentity && !selectionInFrame; |
1882 | 1942 | return { |
1883 | 1943 | success: true, |
1884 | 1944 | found: true, |
| 1945 | + verified, |
1885 | 1946 | query: text, |
1886 | 1947 | selectedText, |
| 1948 | + selectionMatchesQuery, |
| 1949 | + selectionSource, |
| 1950 | + selectionInFrame, |
| 1951 | + selectionScope: selectionInFrame ? 'frame_match_unverified' : 'current_match_only', |
| 1952 | + ...(selectionSource === 'text_control' |
| 1953 | + ? { selectionStart: controlSelectionStart, selectionEnd: controlSelectionEnd } |
| 1954 | + : {}), |
| 1955 | + replacesPreviousSelection: !selectionInFrame, |
| 1956 | + browserFindUiOpened: false, |
1887 | 1957 | ...(rect ? { rect } : {}), |
| 1958 | + warning: verified |
| 1959 | + ? 'Only this match is selected. This call replaced any previous page selection, and it did not open the browser Find UI. Do not claim earlier find_text matches remain highlighted.' |
| 1960 | + : 'window.find reported a match, but WebBrain could not verify a visible current selection in the top document (for example, the active match may be inside a frame while an older top-document selection remains). Do not claim it is visibly highlighted. The browser Find UI was not opened.', |
1888 | 1961 | }; |
1889 | 1962 | } catch (error) { |
1890 | 1963 | return { success: false, found: false, dispatched: false, noDispatch: true, error: `find_text failed: ${error.message || error}` }; |
|
0 commit comments