Skip to content

Commit

Permalink
Fix for srcdoc iframes (#113)
Browse files Browse the repository at this point in the history
* Fix for srcdoc

* Lime survey test html

* Fix for blobs
  • Loading branch information
forgetso authored Jan 31, 2024
1 parent 12acc5b commit a5df325
Show file tree
Hide file tree
Showing 4 changed files with 660 additions and 20 deletions.
94 changes: 94 additions & 0 deletions src/cypress/e2e/limeSurvey.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/// <reference types="cypress" />

import { ELEMENT_FILTER } from '../../constants'
import { isBlobIframe } from '../../elements'
import { searchReplace } from '../../searchreplace'

const SEARCHTERM = 'Phone Type:'
const REPLACETERM = 'The replacement'
const BASEURL = 'http://localhost:9000'
describe('Search Replace LimeSurvey', { baseUrl: BASEURL, responseTimeout: 120e3 }, () => {
beforeEach(() => {
cy.visit('http://localhost:9000/tests/limeSurvey/test.html')
})

it('counts the correct number of occurrences', () => {
cy.window().then((window) => {
console.log(window.location.host)
const iframes = Array.from(
<NodeListOf<HTMLIFrameElement>>window.document.querySelectorAll('iframe')
).filter((iframe) => !isBlobIframe(iframe))
cy.wrap(
searchReplace(
'count',
window,
SEARCHTERM,
REPLACETERM,
false,
false,
true,
false,
false,
false,
true,
false,
iframes,
ELEMENT_FILTER
).then((result) => {
console.log(`result`, result)
expect(result.searchReplaceResult.count.original).to.equal(2)
})
).then(() => {
console.log('done')
})
})
})
it('replaces the search term and then replaces it again', () => {
cy.window().then((window) => {
console.log(window.location.host)
const iframes = Array.from(
<NodeListOf<HTMLIFrameElement>>window.document.querySelectorAll('iframe')
).filter((iframe) => !isBlobIframe(iframe))
cy.wrap(
searchReplace(
'searchReplace',
window,
SEARCHTERM,
REPLACETERM,
false,
false,
true,
false,
false,
false,
true,
false,
iframes,
ELEMENT_FILTER
).then((result1) => {
expect(result1.searchReplaceResult.count.replaced).to.equal(2)
searchReplace(
'searchReplace',
window,
REPLACETERM,
SEARCHTERM,
false,
false,
true,
false,
false,
false,
true,
false,
iframes,
ELEMENT_FILTER
).then((result2) => {
expect(result2.searchReplaceResult.count.replaced).to.equal(2)
})
})
).then(() => {
console.log('done')
})
})
})
})
9 changes: 8 additions & 1 deletion src/elements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ export function containsPartialClass(element: Element, partialClass: string) {
}

export function getLocalIframes(window: Window, document: Document): HTMLIFrameElement[] {
console.log('iframes', document.querySelectorAll('iframe'))
return Array.from(<NodeListOf<HTMLIFrameElement>>document.querySelectorAll('iframe')).filter((iframe) => {
return iframe.src === '' || iframe.src === 'about:blank' || iframe.src === window.location.href
return iframe.src === '' || iframe.src === 'about:blank' || iframe.srcdoc || iframe.src === window.location.href
})
}

Expand Down Expand Up @@ -72,6 +73,8 @@ export const waitForIframeLoad = async (iframe: HTMLIFrameElement): Promise<HTML
} else {
resolve(iframe)
}
} else if (iframe.srcdoc) {
resolve(iframe)
} else {
resolve(null)
}
Expand Down Expand Up @@ -200,6 +203,10 @@ export function elementIsVisible(element: HTMLElement, ancestorCheck = true, clo

export function getInitialIframeElement(iframe: HTMLIFrameElement): HTMLElement | null {
let element: HTMLElement | null = null
console.log('iframe', iframe)
if (iframe.srcdoc) {
return iframe
}
if (iframe.contentDocument) {
element = iframe.contentDocument?.body || iframe.contentDocument?.querySelector('div')
}
Expand Down
70 changes: 51 additions & 19 deletions src/searchreplace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,15 @@ function replaceInInputShadow(

function getValue(node: Element | Node, config: SearchReplaceConfig): string {
const nodeElement = getElementFromNode(node)
console.log('nodeElement', nodeElement)
// if it's an input or a textarea, take the value
if (nodeElement && (nodeElement.nodeName === 'INPUT' || nodeElement.nodeName === 'TEXTAREA')) {
return nodeElement['value']
}
// if it's an iframe with srcdoc, take the srcdoc
if (nodeElement && nodeElement.nodeName === 'IFRAME' && nodeElement.hasAttribute('srcdoc')) {
return nodeElement['srcdoc']
}

// if it's a contenteditable div, take the outerHTML if we're replacing HTML, otherwise take the innerHTML
if (nodeElement && nodeElement.nodeName.match(/DIV|BODY/g) && nodeElement.hasAttribute('contenteditable')) {
console.log('returning outer / innerHTML')
Expand Down Expand Up @@ -189,12 +193,10 @@ function containsAncestor(element: Element, results: Map<Element, SearchReplaceR
function countOccurrences(el: HTMLElement, config: SearchReplaceConfig): number {
let target = getValue(el, config)

if (config.hiddenContent && config.searchTarget === 'innerText') {
if (config.hiddenContent && config.searchTarget === 'innerText' && el.tagName !== 'IFRAME') {
// textContent contains text of visible and hidden elements
console.log('using textContent')
target = (el as HTMLElement).textContent || ''
}
console.log('counting in', target)
const matches = target.match(config.globalSearchPattern) || []
return matches.length
}
Expand Down Expand Up @@ -233,6 +235,7 @@ function replaceInNodeOrElement(
replaced = true
}
if (replaced) {
console.log('adding', config.replaceAll ? occurrences.length : 1, 'to replaced count')
replacementCount = config.replaceAll ? occurrences.length : 1 // adds one to replaced count if a replacement was made, adds occurrences if a global replace is made
}
nodeElement?.dispatchEvent(new Event('input', { bubbles: true }))
Expand Down Expand Up @@ -392,7 +395,7 @@ function replaceInner(
}

const occurrences = countOccurrences(element, config)
console.log('occurrences', occurrences, element)
console.log('occurrences', occurrences)
elementsChecked = updateResults(elementsChecked, element, false, occurrences, 0)

const ancestorChecked = containsAncestor(element, elementsChecked)
Expand Down Expand Up @@ -527,6 +530,24 @@ function equivalentInIgnoredElements(ignoredElements: Set<Element>, element: Ele
return false
}

function replaceInSrcDocIframe(
config: SearchReplaceConfig,
iframe: HTMLIFrameElement,
searchReplaceResult: SearchReplaceResult,
elementsChecked: Map<Element, SearchReplaceResult>
): ReplaceFunctionReturnType {
const occurrences = countOccurrences(iframe, config)
console.log("occurrences in iframe's srcdoc", occurrences)
elementsChecked = updateResults(elementsChecked, iframe, false, occurrences, 0)
searchReplaceResult.count.original = searchReplaceResult.count.original + occurrences
if (config.replace && occurrences) {
iframe.srcdoc = iframe.srcdoc.replace(config.searchPattern, config.replaceTerm)
console.log('adding', config.replaceAll ? occurrences : 1, 'to replaced count')
searchReplaceResult.count.replaced += config.replaceAll ? occurrences : 1
}
return { searchReplaceResult, elementsChecked }
}

function replaceInHTML(
config: SearchReplaceConfig,
document: Document,
Expand All @@ -535,7 +556,6 @@ function replaceInHTML(
elementsChecked: Map<Element, SearchReplaceResult>
): ReplaceFunctionReturnType {
for (const [originalIndex, originalElement] of originalElements.entries()) {
console.log('replacing in element', originalElement)
let clonedElement = originalElement.cloneNode(true) as HTMLElement

const { clonedElementRemoved, removedSet } = copyElementAndRemoveSelectedElements(
Expand All @@ -544,9 +564,11 @@ function replaceInHTML(
// - match the element filter, but are not blob iframes, nor are WYSIWYG iframes.
// Removes SCRIPT, STYLE, IFRAME, etc.
// - match the input filter, as these are handled later
// - are already in the elementsChecked map
(el: HTMLElement) =>
(!!el.nodeName.match(config.elementFilter) && !isBlobIframe(el) && !isWYSIWYGEditorIframe(el)) ||
isInputElement(el),
isInputElement(el) ||
elementsChecked.has(el),
false
)
clonedElement = clonedElementRemoved as HTMLElement
Expand Down Expand Up @@ -671,7 +693,7 @@ export async function searchReplace(
shadowRoots,
}
// we check other places if text was not replaced in a text editor
let result: ReplaceFunctionReturnType
let result: ReplaceFunctionReturnType = { searchReplaceResult, elementsChecked }
if (inputFieldsOnly) {
result = replaceInputFields(config, document, searchReplaceResult, elementsChecked)
if (config.replaceNext && result.searchReplaceResult.replaced) {
Expand All @@ -680,17 +702,27 @@ export async function searchReplace(
} else {
const startingElement = document.body || document.querySelector('div')

const searchableIframes = (await getSearchableIframes(window, document))
.map(getInitialIframeElement)
.filter(notEmpty)
console.log('searchable iframes', searchableIframes)
result = replaceInHTML(
config,
document,
[startingElement, ...searchableIframes],
searchReplaceResult,
elementsChecked
)
const searchableIframesInitial = await getSearchableIframes(window, document)
const srcDocIframes = searchableIframesInitial.filter((iframe) => iframe.hasAttribute('srcdoc'))
srcDocIframes.map((iframe) => {
const srcDocResult = replaceInSrcDocIframe(
config,
iframe,
result.searchReplaceResult,
result.elementsChecked
)
result.searchReplaceResult = srcDocResult.searchReplaceResult
result.elementsChecked = srcDocResult.elementsChecked
})
console.log(JSON.stringify(result.searchReplaceResult))
console.log('searchableIframesInitial', searchableIframesInitial)
const searchableIframes = searchableIframesInitial.filter((iframe: HTMLIFrameElement) => {
return iframe.srcdoc === '' || iframe.srcdoc === undefined
})
const searchable = searchableIframes.map(getInitialIframeElement).filter(notEmpty)
console.log('searchableIframes', searchableIframes)
console.log('searchable', searchable)
result = replaceInHTML(config, document, [startingElement, ...searchable], searchReplaceResult, elementsChecked)
}

return result
Expand Down
507 changes: 507 additions & 0 deletions tests/limeSurvey/test.html

Large diffs are not rendered by default.

0 comments on commit a5df325

Please sign in to comment.