Skip to content

Commit

Permalink
83 search count and replace not working in wordpress iframe editor (#84)
Browse files Browse the repository at this point in the history
* wip

* add tests for iframes

* Allow replacing in content editables inside iframes

* iframe test file and version bump
  • Loading branch information
forgetso authored Oct 30, 2023
1 parent a000230 commit 796e12f
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 74 deletions.
8 changes: 4 additions & 4 deletions cypress/e2e/searchreplace.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
})

Expand All @@ -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)
})
})

Expand Down
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "search_and_replace",
"version": "1.7.1",
"version": "1.7.2",
"resolutions": {
"author": "Chris Taylor <[email protected]>"
},
Expand Down
4 changes: 3 additions & 1 deletion src/popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
;(<HTMLDivElement>document.getElementById('searchTermCount')).innerHTML = `${
msg['searchTermCount']
Expand Down Expand Up @@ -336,7 +338,7 @@ function sendToStorage(
}
port.postMessage(storageMessage)
port.onMessage.addListener(function (msg) {
console.debug('Message received: ' + msg)
console.log('Message received: ' + msg)
})
}

Expand Down
97 changes: 63 additions & 34 deletions src/searchreplace.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -34,7 +34,7 @@ function setNativeValue(element, value) {

function replaceInInput(
document: Document,
input: HTMLInputElement,
input: HTMLInputElement | HTMLTextAreaElement,
searchPattern: RegExp,
replaceTerm: string,
usesKnockout: boolean
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -169,29 +169,18 @@ function replaceInputFields(
flags: string,
visibleOnly: boolean
): boolean {
const iframes = document.querySelectorAll('iframe')
const allInputs: NodeListOf<HTMLInputElement | HTMLTextAreaElement> = 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
}

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<HTMLInputElement | HTMLTextAreaElement> =
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
}
Expand Down Expand Up @@ -233,18 +222,18 @@ function replaceHTML(

function replaceHTMLInIframes(
document: Document,
iframes,
iframes: NodeListOf<HTMLIFrameElement>,
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 {
Expand Down Expand Up @@ -309,7 +298,6 @@ function replaceVisibleOnly(
// Custom Functions

async function cmsEditor(
window: Window,
document: Document,
searchPattern: RegExp,
replaceTerm: string,
Expand All @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -382,6 +369,32 @@ function selectElementContents(window: Window, el: HTMLElement) {
}
}

async function replaceInCMSEditors(
document: Document,
searchPattern: RegExp,
replaceTerm: string,
flags: string,
visibleOnly: boolean
): Promise<boolean> {
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,
Expand All @@ -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) {
Expand Down
72 changes: 45 additions & 27 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(<NodeListOf<HTMLInputElement>>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(<NodeListOf<HTMLIFrameElement>>document.querySelectorAll('iframe'))
}

export function getSearchOccurrences(
document: Document,
searchPattern: RegExp,
Expand All @@ -58,54 +64,66 @@ 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
}

return occurences
}

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() {
Expand Down
13 changes: 13 additions & 0 deletions tests/iframe.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<html>
<head>
<title>Test</title>
</head>
<!-- An HTML page containing all kinds of input fields in which we will test replacing text -->
<body>
<h1>Iframe</h1>
<div>
This is a test!!! ¹²
</div>
<input type="text" name="text" value="This is a test!!! ¹²">
</body>
</html>
Loading

0 comments on commit 796e12f

Please sign in to comment.