I had missed (or not understood) your tip about forcing window.location.href
to navigate. Posting here the code I just wrote to sort that out. I’ll post more if I keep going, I think I’ll play around a little with the 2-way interactions again.
For my use case, in which I’m loading HTML from email bodies, it made sense to add Javascript to the HTML before loading it into the WebView. I also experimented with executing Javascript within the loaded WebView, but there were still some issues to work out.
In my testing, this has resulted in 100% caught link clicks, whereas I would often experience “dropped” clicks before.
Javascript to insert into the HTML
/** This ensures that all link clicks pass through `shouldAllowRequest` */
const routeAllLinksThroughWindowLocation = [
`window.addEventListener(`,
` 'click',`,
` event => {`,
` const closestLink = event.target && event.target.closest('a');`,
` if (!closestLink) return;`,
` event.preventDefault();`,
` const url = closestLink.getAttribute('href');`,
` window.location.href = url;`,
` },`,
` true`,
`);`,
]
.map(line => line.trim())
.join('');
Code to insert above script into the HTML body
I’m not sure if it’s all really necessary to satisfy the browser engine, but it works.
export const injectScriptInHtmlStr = (htmlStr, script) => {
const hasHead =
lowerIncludes(htmlStr, '<head') && lowerIncludes(htmlStr, '</head>');
const hasHtml =
lowerIncludes(htmlStr, '<html') && lowerIncludes(htmlStr, '</html>');
const scriptInTag = `<script>${script}</script>`;
const scriptTagInHead = `<head>${scriptInTag}</head>`;
const insertedIntoExistingHead = spliceInPlace(
htmlStr.split('</head>'),
1,
0,
`${scriptInTag}</head>`
).join('');
if (hasHead && hasHtml) return insertedIntoExistingHead;
if (hasHead && !hasHtml)
return ['<html>', insertedIntoExistingHead, '</html>'].join('');
if (!hasHead && hasHtml)
return [
'<html>',
`${scriptTagInHead}`,
splitByRegex(htmlStr, /<html[^<>]*>/i)[1],
].join('');
if (!hasHead && !hasHtml)
return [`<html>${scriptTagInHead}`, htmlStr, '</html>'].join('');
};
Utils used above
const lowerIncludes = (containingString, query) =>
containingString.toLowerCase().includes(query.toLowerCase());
const splitByRegex = (str, regex) => {
const uniqueRegexReplacer = UUID.string();
const globalRegex = new RegExp(
regex.source,
regex.flags.includes('g') ? regex.flags : regex.flags + 'g'
);
const withUniqueDividers = str.replace(globalRegex, uniqueRegexReplacer);
return withUniqueDividers.split(uniqueRegexReplacer);
};
const spliceInPlace = (arr, startIndex, deleteCount, ...items) => {
const shallowClone = [...arr];
shallowClone.splice(startIndex, deleteCount, ...items);
return shallowClone;
};
Edit – bad splicing