eLeXeM
eLeXeM

Reputation: 1

can't get exclusion of elements in document.querySelectorAll to work

hoping to avoid repeatedly typing references to anchors by hand, I tried to come up with a way to make any occurrence of given terms into automagically linked references to a same-named anchor, like turn 'foo' into <a href="#foo">foo</a>, 'bar' into <a href="#bar">bar</a>, and so on.

I can't, however, seem to get my clunky approach to skip already linked occurrences (or other elements like style, script, etc). Positive selection (like nodeList = document.querySelectorAll(".entry-content a"); ) works just fine, but exclusion (like e.g. nodeList = document.querySelectorAll(".entry-content:not(a, style, script)"); eludes me. I sifted thru quite a lot of questions that looked similar, already, but could adapt none for my stubborn problem :/ so, I must definitely be doing something wrong.

Your help figuring this out is much appreciated. Here's where I'm at right now:

function rep() {
        
        const nodeList = document.querySelectorAll(".entry-content:not(a, style, script)");

            for (let i = 0; i < nodeList.length; i++) {

                nodeList[i].innerHTML = nodeList[i].innerHTML.replace(/foo/g, '<a href="#foo" style="background: lime;">REPLACED FOO</a>');
                }
        
        }

But this just blatantly replaces every occurrence of 'foo' inside my class="entry-content", regardless of element type it appears in (instead of disregarding a and style and script elements).

Thank you for your look at this. Cheers -

Upvotes: 0

Views: 217

Answers (2)

CertainPerformance
CertainPerformance

Reputation: 370979

:not only accepts "simple selectors" - selectors with only one component to them. a, style, script is not a simple selector, so :not(a, style, script) doesn't produce the desired results. You could put the logic to exclude those tags in the JavaScript instead of the selector.

But that's not enough. Some of the elements you don't want to match are descendants of the .entry-content elements. For example, a <p class="entry-content"> will not be excluded from being a match just because it has an <a> descendant. So just matching .entry-content elements and replacing won't be enough. You'll need some different logic to identify text which has a .entry-content ancestor and also doesn't have a blacklisted tag as an ancestor.

One possibility would be to iterate over text nodes and check if their parent element has a .closest element that matches, and doesn't have a .closest element in the blacklist. The replacement of text nodes with a varying number of possibly non-text nodes is pretty cumbersome, unfortunately. Better to avoid assigning to .innerHTML - that'll corrupt references that JavaScript scripts may have to elements inside the container that gets altered.

// https://stackoverflow.com/q/2579666
function nativeTreeWalker() {
    const walker = document.createTreeWalker(
        document.body, 
        NodeFilter.SHOW_TEXT, 
        null, 
        false
    );

    let node;
    const textNodes = [];
    while(node = walker.nextNode()) {
        textNodes.push(node);
    }
    return textNodes;
}

function rep() {
    for (const node of [...nativeTreeWalker()]) {
        const parent = node.parentElement;
        if (!node.textContent.includes('foo') || !parent.closest('.entry-content') || parent.closest('a, style, script')) {
            continue;
        }
        // Degenerate case
        if (parent.matches('textarea')) {
            // In modern environments, use `.replaceAll` instead of regex
            parent.textContent = parent.textContent.replaceAll('foo', '<a href="#foo" style="background: lime;">REPLACED FOO</a>');
            continue;
        }
        // At this point, we know this node needs to be replaced.
        // Can't just use parent.innerHTML = parent.innerHTML.replace
        // because other children of the parent (siblings of this node) may be on the blacklist

        // Use a DocumentFragment
        // so the new nodes can be inserted at the right position
        // all at once at the end
        const newNodesFragment = new DocumentFragment();
        for (const match of node.textContent.match(/foo|((?!foo).)+/g)) {
            if (match !== 'foo') {
                newNodesFragment.append(match);
            } else {
                newNodesFragment.append(
                    Object.assign(
                        document.createElement('a'),
                        {
                            href: '#foo',
                            style: 'background: lime',
                            textContent: 'REPLACED FOO'
                        }
                    )
                );
            }
        }
        // Insert the new nodes
        parent.insertBefore(newNodesFragment, node);
        // Remove the original text node
        parent.removeChild(node);
    }
}
.red {
  background: red;
}
<h1>A Foo is not a foo and a Bar is not a bar</h1>
<p style="color:gray;"> this p has <b>no class</b>, nothing should happen here. Hi, I'm a foo. I'm a bar. Hi, I'm <a href="some link.htm">an already <b>linked foo</b></a>. I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm <a href="some link.htm"
    target="_blank">an already <b>linked bar</b></a>. Hi, I'm a foo. I'm a bar.</p>

<p class="entry-content">This p <b>has class .entry-content</b>. Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm <a href="some link.htm" target="_blank">.ean already <b>linked foo - should not be altered</b></a>.
  I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar. <span class="red">Hi, I'm a foo</span>. I'm a bar. Hi, I'm a <span><b>foo in a span</b></span>. I'm a bar. Hi, I'm <a href="some link.htm">an already <b>linked bar</b></a>.
  Hi, I'm a foo. I'm a bar. <textarea>Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar.</textarea> </p>

<button onclick="rep()">do rep()</button>

Upvotes: 2

Ronnie Smith
Ronnie Smith

Reputation: 18585

You want to create a class to make new anchor elements.

class anchor {
    constructor(text) {
    let a = document.createElement("a");
    let t = document.createTextNode(text);
    a.href = text;
    a.appendChild(t);
    return a;
  }
}

document.body.appendChild(new anchor("foo"));
document.body.appendChild(new anchor("bar"));
a {
  margin:24px;
}

Upvotes: 0

Related Questions