I’m trying to select and modify text on my webpage which are not unique and then modify them. For instance suppose I have multiple HTML elements that are siblings of one another and they contain the same text. If I try to select the text in one of them using Regex and then modify it i.e. (wrap it in span tags, replace it with different text, delete it, etc.), Regex will only show the first match that it had found which may or may not be the text I selected.
I did some research into using the Treewalker API for this but the examples I saw weren’t very helpful. Please help suggest the best solution that will take care of situations where the same selected text exists in multiple HTML elements.
const getDuplicates = function() {
// a map of existing text content
// e.g. { 'Some other content': true }
const exists = {}
const isNewLine = function (node) {
return /^[\n\r]+\s*/.test(node.textContent)
}
return document.createNodeIterator(
document.body,
NodeFilter.SHOW_TEXT,
(node) => {
// ignore new line text nodes e.g. '\n '
if (isNewLine(node)) return NodeFilter.FILTER_SKIP
// if node with same text content already exists
// return duplicate
if (exists[node.textContent]) {
return NodeFilter.FILTER_ACCEPT
}
// first appearance then store in exists
exists[node.textContent] = true
return NodeFilter.FILTER_SKIP
},
false
);
}
// get the iterator
const duplicatesIterator = getDuplicates()
let currentNode
let duplicateNodes = []
// iterate and store in an array
while (currentNode = duplicatesIterator.nextNode()) {
duplicateNodes.push(currentNode)
}
// creates a span around a text element
const createSpan = function(textContent) {
const span = document.createElement('span')
span.className = 'duplicate'
span.textContent = textContent
return span
}
// loop through duplicates and wrap a span
duplicateNodes.forEach(
(node) => node.replaceWith(createSpan(node.textContent))
)
Why are you doing this with Javascript? Feels like we’re missing a piece of the situation/problem statement here…
If it’s your webpage… go into the HTML in your HTML-editor of choice, find the duplicates and… do whatever you were going to do with them?
Hi all, Thanks for your replies especially you rpg_digital. The reason for creating this functionality is for presentation purposes. Suppose I want to be able to highlight some info on my page on the fly while giving a presentation to a group of people in a video conference, if I can highlight or increase the size of any piece of info (especially dynamically loaded info) that is deemed important to the group it would be very useful as it can draw the attentions of all meeting attendees to those highlighted info.
I think there is an issue with replacing body.innerHTML in that you will lose all attached eventListeners on the page.
That and there seems to be some weirdness with ending up with orphaned elements — I need to do some looking into that.
Anyway have taken m_hutley’s getSelection and replaceAll and adapted it.
const walkTheDom = function (node, callback) {
callback(node)
node = node.firstChild
while (node) {
walkTheDom(node, callback)
node = node.nextSibling
}
}
const isValidTextNode = function (node) {
const isNewLine = /^[\n\r]+\s*/
return (node.nodeType === 3 && !isNewLine.test(node.textContent))
}
const getTextParentNodes = function (root = document.body) {
const parents = []
walkTheDom(root, (node) => {
if (isValidTextNode(node)) parents.push(node.parentElement)
})
return parents
}
const highlightDuplicates = function (strgToMatch) {
const parents = getTextParentNodes()
const strgRx = new RegExp(`\\b${strgToMatch}\\b`, 'ig')
parents.forEach((parent) => {
parent.innerHTML = parent.innerHTML.replaceAll(
strgRx, `<span class="emphasise">${strgToMatch}</span>`
)
})
}
const removeHighlights = function (strgToMatch) {
const spans = document.querySelectorAll('.wrapper span.emphasise')
spans.forEach((span) => {
if (span.textContent !== strgToMatch) return
span.replaceWith(document.createTextNode(strgToMatch))
})
}
const highlightHandler = function () {
const selection = window.getSelection()
const selectedString = selection.toString().trim()
if (selectedString === '') return
const anchorNode = selection.anchorNode
// if selection is wrapped in a span, remove spans for that word/words
if (anchorNode.parentElement.matches('span.emphasise')) {
removeHighlights(anchorNode.textContent)
} else {
highlightDuplicates(selectedString)
}
}
const highlightBtn = document.querySelector('#toggle-highlight')
highlightBtn.addEventListener('click', highlightHandler)
The code could do with some refactoring. There’s a question mark in how I am selecting nodes. But for now here is a codepen.
Currently you need to select on whole words to highlight all instances — you can also select multiple words.
The highlight button is a toggle, so if you select on a hightlighted word or part of it, it will remove the span tags for those duplicate words. Does that make sense?
First off I wanted to match on text selection rather than having to click a button first. I also wanted the selection to expand to full words automatically.
I did try using the built in selection.modify, but in the end opted for using while loops to trim and expand the selections. In particular I wanted selections with leading or trailing spaces to trim to the word rather than grab adjacent words.
This was a project in itself.
Expand selection to full words
I have that gut feeling that this could have been achieved in just a few lines of code.
Then the match duplicates script which imports expand selection
Highlight matching text
Selections work backwards and forwards and include inline elements, spans, italic etc.
Selecting selections that include already highlighted text, removes the inner spans and wraps that selection with a new highlighting span.
Selecting inside highlighted text removes the highlight from all matches
Double clicking is currently disabled as there are a few niggles. Very odd in firefox, if you select inside an inline element, window.getSelection actually selects the previous sibling. If you give that inline element a display of inline-block it selects correctly.
Hi all, thanks for all of your input. I have been swamp with work and did not have time to do much else. I will analyze the code and come back with questions.