diff --git a/cypress/e2e/searchreplace.cy.ts b/cypress/e2e/searchreplace.cy.ts index c3b833e..e877b8b 100644 --- a/cypress/e2e/searchreplace.cy.ts +++ b/cypress/e2e/searchreplace.cy.ts @@ -27,14 +27,14 @@ describe('Search Replace ', () => { it('counts the correct number of occurrences', () => { cy.document().then((document) => { const occurences = getSearchOccurrences(document, SEARCHPATTERNGLOBAL, false) - expect(occurences).to.equal(6) + expect(occurences).to.equal(8) }) }) it('counts the correct number of visible occurrences', () => { cy.document().then((document) => { const occurences = getSearchOccurrences(document, SEARCHPATTERNGLOBAL, true) - expect(occurences).to.equal(5) + expect(occurences).to.equal(7) }) }) @@ -48,14 +48,14 @@ describe('Search Replace ', () => { it('counts the correct number of occurrences for inputs only', () => { cy.document().then((document) => { const occurences = getSearchOccurrences(document, SEARCHPATTERNGLOBAL, false, true) - expect(occurences).to.equal(4) + expect(occurences).to.equal(5) }) }) it('counts the correct number of occurrences for visible inputs only', () => { cy.document().then((document) => { const occurences = getSearchOccurrences(document, SEARCHPATTERNGLOBAL, true, true) - expect(occurences).to.equal(3) + expect(occurences).to.equal(4) }) }) diff --git a/manifest.json b/manifest.json index 3521095..0cce9d4 100644 --- a/manifest.json +++ b/manifest.json @@ -31,7 +31,7 @@ "permissions": ["activeTab", "storage", "notifications"], "host_permissions": ["http://*/*", "https://*/*"], "update_url": "http://clients2.google.com/service/update2/crx", - "version": "1.7.1", + "version": "1.7.2", "options_page": "assets/options.html", "icons": { "16": "assets/icon-16.png", diff --git a/package.json b/package.json index f9e13e5..bac32a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "search_and_replace", - "version": "1.7.1", + "version": "1.7.2", "resolutions": { "author": "Chris Taylor " }, diff --git a/src/popup.ts b/src/popup.ts index 60d3023..72c8c27 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -265,7 +265,9 @@ export async function tabQuery( function tabQueryCallback(msg, translationFn: TranslationProxy) { removeLoader() + console.log('tabQueryCallback') if (msg && 'inIframe' in msg && msg['inIframe'] === false) { + console.log('msg', msg) if ('searchTermCount' in msg && getSearchTermElement().value.length >= MIN_SEARCH_TERM_LENGTH) { ;(document.getElementById('searchTermCount')).innerHTML = `${ msg['searchTermCount'] @@ -336,7 +338,7 @@ function sendToStorage( } port.postMessage(storageMessage) port.onMessage.addListener(function (msg) { - console.debug('Message received: ' + msg) + console.log('Message received: ' + msg) }) } diff --git a/src/searchreplace.ts b/src/searchreplace.ts index d70b32e..4ef77b7 100644 --- a/src/searchreplace.ts +++ b/src/searchreplace.ts @@ -1,6 +1,6 @@ 'use strict' import { RegexFlags, RichTextEditor, SearchReplaceMessage } from './types/index' -import { elementIsVisible, getSearchOccurrences, inIframe } from './util' +import { elementIsVisible, getIframeElements, getInputElements, getSearchOccurrences, inIframe } from './util' import { getHints } from './hints' import { ELEMENT_FILTER, INPUT_TEXTAREA_FILTER, RICH_TEXT_EDITORS } from './constants' import { getFlags, getSearchPattern } from './regex' @@ -34,7 +34,7 @@ function setNativeValue(element, value) { function replaceInInput( document: Document, - input: HTMLInputElement, + input: HTMLInputElement | HTMLTextAreaElement, searchPattern: RegExp, replaceTerm: string, usesKnockout: boolean @@ -130,7 +130,7 @@ function replaceHTMLInBody(body: HTMLBodyElement, searchPattern: RegExp, replace function replaceInInputs( document: Document, - inputs: HTMLInputElement[], + inputs: (HTMLInputElement | HTMLTextAreaElement)[], searchPattern: RegExp, replaceTerm: string, flags: string @@ -169,16 +169,9 @@ function replaceInputFields( flags: string, visibleOnly: boolean ): boolean { - const iframes = document.querySelectorAll('iframe') - const allInputs: NodeListOf = document.querySelectorAll('input, textarea') - const inputTypeFilter: string[] = [] - if (visibleOnly) { - inputTypeFilter.push('hidden') - } - const allInputsArr: HTMLInputElement[] = Array.from(allInputs).filter( - ({ type }) => inputTypeFilter.indexOf(type) === -1 - ) as HTMLInputElement[] - const replaced = replaceInInputs(document, allInputsArr, searchPattern, replaceTerm, flags) + const iframes = getIframeElements(document) + const allInputs = getInputElements(document, visibleOnly) + const replaced = replaceInInputs(document, allInputs, searchPattern, replaceTerm, flags) if (flags === RegexFlags.CaseInsensitive && replaced) { return replaced } @@ -186,12 +179,8 @@ function replaceInputFields( for (let iframeCount = 0; iframeCount < iframes.length; iframeCount++) { const iframe = iframes[0] if (iframe.src.match('^http://' + window.location.host) || !iframe.src.match('^https?')) { - const iframeInputs: NodeListOf = - document.querySelectorAll('input, textarea') - const iframeInputsArr: HTMLInputElement[] = Array.from(iframeInputs).filter( - ({ type }) => inputTypeFilter.indexOf(type) === -1 - ) as HTMLInputElement[] - const replaced = replaceInInputs(document, iframeInputsArr, searchPattern, replaceTerm, flags) + const iframeInputs = getInputElements(iframe.contentDocument!, visibleOnly) + const replaced = replaceInInputs(document, iframeInputs, searchPattern, replaceTerm, flags) if (replaceNextOnly(flags) && replaced) { return replaced } @@ -233,18 +222,18 @@ function replaceHTML( function replaceHTMLInIframes( document: Document, - iframes, + iframes: NodeListOf, searchPattern: RegExp, replaceTerm: string, flags: string, visibleOnly: boolean ): boolean { let replaced = false - for (let iframeCount = 0; iframeCount < iframes.length; iframeCount++) { - const iframe = iframes[0] + for (const iframe of iframes) { if (iframe.src.match('^http://' + window.location.host) || !iframe.src.match('^https?')) { try { - const content = iframe.contentDocument.documentElement.body as HTMLBodyElement + const content = iframe.contentDocument?.body as HTMLBodyElement + console.log('iframe.body', content) if (visibleOnly) { replaced = replaceVisibleOnly(document, [content], searchPattern, replaceTerm, flags) } else { @@ -309,7 +298,6 @@ function replaceVisibleOnly( // Custom Functions async function cmsEditor( - window: Window, document: Document, searchPattern: RegExp, replaceTerm: string, @@ -336,7 +324,7 @@ async function cmsEditor( console.log('inner HTML', editor.innerHTML) const newText = initialText.replace(searchPattern, replaceTerm) console.log('newText', newText) - await replaceInContentEditableElement(window, editor, initialText, newText) + await replaceInContentEditableElement(editor, initialText, newText) replaced = initialText !== newText } } catch (err) { @@ -349,7 +337,6 @@ async function cmsEditor( // taken from https://stackoverflow.com/a/69656905/1178971 async function replaceInContentEditableElement( - window: Window, element: HTMLElement, initialText: string, replacementText: string @@ -382,6 +369,32 @@ function selectElementContents(window: Window, el: HTMLElement) { } } +async function replaceInCMSEditors( + document: Document, + searchPattern: RegExp, + replaceTerm: string, + flags: string, + visibleOnly: boolean +): Promise { + let replaced = false + // replacement functions for pages with text editors + for (const richTextEditor of RICH_TEXT_EDITORS) { + if (richTextEditor.container) { + if (document.querySelectorAll(richTextEditor.container.value).length) { + console.log('Replacing in rich text editor') + replaced = await cmsEditor(document, searchPattern, replaceTerm, flags, richTextEditor) + } + } else { + if (document.querySelectorAll(richTextEditor.editor.value).length) { + console.log('Replacing in rich text editor') + replaced = await cmsEditor(document, searchPattern, replaceTerm, flags, richTextEditor) + } + } + } + + return replaced +} + export async function searchReplace( window: Window, searchTerm: string, @@ -397,19 +410,35 @@ export async function searchReplace( let replaced = false // replacement functions for pages with text editors - for (const richTextEditor of RICH_TEXT_EDITORS) { - if (richTextEditor.container) { - if (document.querySelectorAll(richTextEditor.container.value).length) { - replaced = await cmsEditor(window, document, searchPattern, replaceTerm, flags, richTextEditor) - } - } else { - if (document.querySelectorAll(richTextEditor.editor.value).length) { - replaced = await cmsEditor(window, document, searchPattern, replaceTerm, flags, richTextEditor) + replaced = await replaceInCMSEditors(document, searchPattern, replaceTerm, flags, visibleOnly) + + if (replaceNextOnly(flags) && replaced) { + return replaced + } + + // TODO loop everything over document and then iframes + // replacement functions for iframes with rich text editors + const iframes = getIframeElements(document) + for (const iframe of iframes) { + if (iframe.src.match('^http://' + window.location.host) || !iframe.src.match('^https?')) { + const richTextEditors = RICH_TEXT_EDITORS.filter((editor) => editor.container?.iframe) + replaced = await replaceInCMSEditors( + iframe.contentDocument!, + searchPattern, + replaceTerm, + flags, + visibleOnly + ) + if (replaceNextOnly(flags) && replaced) { + return replaced } } } + + // Check to see if the search term is still present const searchTermPresentAndGlobalSearch = getSearchOccurrences(document, searchPattern, visibleOnly) > 0 && flags.indexOf(RegexFlags.Global) > -1 + // we check other places if text was not replaced in a text editor if (!replaced || searchTermPresentAndGlobalSearch) { if (inputFieldsOnly) { diff --git a/src/util.ts b/src/util.ts index f3482b2..a5aaf28 100644 --- a/src/util.ts +++ b/src/util.ts @@ -43,12 +43,18 @@ export const clearHistoryMessage: SearchReplaceStorageMessage = { actions: { clearHistory: true }, } -function getInputElements(document: Document, visibleOnly?: boolean): (HTMLInputElement | HTMLTextAreaElement)[] { +export function getInputElements( + document: Document, + visibleOnly?: boolean +): (HTMLInputElement | HTMLTextAreaElement)[] { const inputs = Array.from(>document.querySelectorAll('input,textarea')) return visibleOnly ? inputs.filter((input) => elementIsVisible(input)) : inputs } -//TODO fix this spaghetti +export function getIframeElements(document: Document): HTMLIFrameElement[] { + return Array.from(>document.querySelectorAll('iframe')) +} + export function getSearchOccurrences( document: Document, searchPattern: RegExp, @@ -58,39 +64,49 @@ export function getSearchOccurrences( ): number { let matches let iframeMatches = 0 - console.log('inputFieldsOnly', inputFieldsOnly, 'visibleOnly', visibleOnly) if (visibleOnly && !inputFieldsOnly) { + // Get visible matches only, anywhere on the page matches = document.body.innerText.match(searchPattern) || [] const inputs = getInputElements(document, visibleOnly) const inputMatches = inputs.map((input) => input.value.match(searchPattern) || []) - if (!iframe) { - const iframes = Array.from(document.querySelectorAll('iframe')) - iframeMatches = iframes - .map((iframe) => { - try { - return getSearchOccurrences(iframe.contentDocument!, searchPattern, visibleOnly, true) - } catch (e) { - return 0 - } - }) - .reduce((a, b) => a + b, 0) - } // combine the matches from the body and the inputs and remove empty matches - if (inputFieldsOnly) { - matches = inputMatches.filter((match) => match.length > 0).flat() - } else { - matches = [...matches, ...inputMatches].filter((match) => match.length > 0).flat() - } + matches = [...matches, ...inputMatches].filter((match) => match.length > 0).flat() } else if (inputFieldsOnly) { + // Get matches in input fields only, visible or hidden, depending on `visibleOnly` const inputs = getInputElements(document, visibleOnly) const inputMatches = inputs.map((input) => input.value.match(searchPattern) || []) matches = inputMatches.filter((match) => match.length > 0).flat() } else { + // Get matches anywhere in the page, visible or not matches = Array.from(document.body.innerHTML.match(searchPattern) || []) } + + // Now check in any iframes by calling this function again, summing the total number of matches from each iframe + const iframes = getIframeElements(document) + if (!iframe) { + iframeMatches = iframes + .map((iframe) => { + try { + return getSearchOccurrences( + iframe.contentDocument!, + searchPattern, + visibleOnly, + inputFieldsOnly, + true + ) + } catch (e) { + return 0 + } + }) + .reduce((a, b) => a + b, 0) + } + let occurences = 0 if (matches) { + console.debug( + `Matches ${matches.length}, Iframe matches: ${iframeMatches}, Total: ${matches.length + iframeMatches}` + ) occurences = matches.length + iframeMatches } @@ -98,14 +114,16 @@ export function getSearchOccurrences( } export function elementIsVisible(element: HTMLElement): boolean { - const styleVisible = element.style.display !== 'none' - - if (element.nodeName === 'INPUT') { - const inputElement = element as HTMLInputElement - return inputElement.type !== 'hidden' && styleVisible - } else { - return styleVisible + if (element && 'style' in element) { + const styleVisible = element.style.display !== 'none' + if (element.nodeName === 'INPUT') { + const inputElement = element as HTMLInputElement + return inputElement.type !== 'hidden' && styleVisible + } else { + return styleVisible + } } + return false } export function inIframe() { diff --git a/tests/iframe.html b/tests/iframe.html new file mode 100644 index 0000000..4cdaba1 --- /dev/null +++ b/tests/iframe.html @@ -0,0 +1,13 @@ + + + Test + + + +

Iframe

+
+ This is a test!!! ¹² +
+ + + diff --git a/tests/test.html b/tests/test.html index 3830b56..43d473a 100644 --- a/tests/test.html +++ b/tests/test.html @@ -9,32 +9,33 @@

Tests

Input Field

- +

Text Area

- +

Text Area with HTML

- +

Editable div with [role="textbox"]

-
This is a test!!!
+
This is a test!!! ¹²

Hidden input

- +

Plain old div

-
This is a test!!!
+
This is a test!!! ¹²
+