-1

I saw somewhere that this question was asked in a faang interview and I cannot come up with an optimized solution or find it anywhere. So the question basically wants us to write a function that receives an input like this:

Input: findAllEle('color', '#fff');

and produces an output like this:

Output: Array of elements matching this color

by going through the DOM tree! The solution probably is using a BFS or DFS but then the edge case is what if the color selector is white or #ffffff instead of #fff for which I think we should use a Trie!

Can anyone implement Trie solution using javascript for those edge cases (having multiple different selector)?

seyet
  • 1,080
  • 4
  • 23
  • 42

5 Answers5

0

I've managed to do it, but, although the trie works in my version, it's kinda useless because it seems getComputedStyle always returns the color's rgb representation (which is why I had to include it in the trie).

I tried to make it as generic as possible so that you can use this with other dfs problems

class Trie {
  value = ''
  children = []

  constructor(...keys) {
    const firstLetters = Array.from(new Set(keys.map(k => k[0])))
    this.children = firstLetters.map(l => {
      return {
                ...new Trie(...keys.filter(k => k.startsWith(l)).map(k => k.substring(1))),
                value: l,
            }
    })
  }
}

const whiteColorTrie = new Trie('white', '#fff', '#ffffff', 'rgb(255, 255, 255)')

const existsInTrie = (trie, search, initialValue = '') => {
    const { value } = trie

    const acc = initialValue + (value ?? '')

    if (!search.startsWith(acc)) return false

    if (search === acc && acc.length !== 0) return true

    return trie.children.some(node => existsInTrie(node, search, acc))
}

const dfs = (root, getChildNodes, predicate) => {
    const children = getChildNodes(root)
    const matches = []

    if (predicate(root)) matches.push(root)

    children.forEach(child =>
        matches.push(...dfs(child, getChildNodes, predicate))
    )

    return matches
}

const elements = dfs(
    document.body,
    node => [...node.children],
    node => existsInTrie(whiteColorTrie, getComputedStyle(node).color)
)

Since the trie isn't really doing anything, here's a version of the code that doesn't include it, making it a lot simpler:

const dfs = (root, getChildNodes, predicate) => {
    const children = getChildNodes(root)
    const matches = []

    if (predicate(root)) matches.push(root)

    children.forEach(child =>
        matches.push(...dfs(child, getChildNodes, predicate))
    )

    return matches
}

const elements = dfs(
    document.body,
    node => [...node.children],
    node => getComputedStyle(node).color === 'rgb(255, 255, 255)'
)
Gustavo Shigueo
  • 399
  • 3
  • 11
0

Visiting all descendants of a node can be handled straight-forwardly with recursive use of .children

Searching for a style color (where the catch is that color is represented in a variety of ways) can be done by coercing them into a standard format with getComputedStyle (see here).

Here, search recursively, always comparing a "normalized" representation of the colors being compared...

// turn any color into a standard-formatted rgb string
const normalizeColor = color => {
  const d = document.createElement("div");
  d.style.color = color;
  document.body.appendChild(d)
  const result = window.getComputedStyle(d).color;
  d.remove();
  return result;
}

// recursive search for descendant nodes with color
function childrenWithColor(el, color) {
  let result = [];
  let targetColor = normalizeColor(color);
  let elColor = normalizeColor(el.style.getPropertyValue('color'));
  if (elColor === targetColor) result.push(el);

  for (let i = 0; i < el.children.length; i++) {
    let child = el.children[i];
    result = [...result, ...childrenWithColor(child, color)];
  }
  return result;
}

// chose "red" for this example
let el = document.getElementById('parent')
let results = childrenWithColor(el, 'red');
// expect: grandchild-a, grandchild-b, child-b, grandchild-d
console.log('red ones:', results.map(el => el.id).join(', '));
.child {
  margin-left: 2em;
}
<div id="parent">
  parent has no style at all

  <div class="child" id="child-a" style="border: 1px solid black;">
    child-a has a style that isn't a color

    <div class="child" id="grandchild-a" style="color: rgb(255,0,0);">
      grandchild-a has rgb red
    </div>

    <div class="child" id="grandchild-b" style="color: rgb(255, 0, 0);">
      grandchild-b has rgb red formatted with whitespace
    </div>
  </div>

  <div class="child" id="child-b" style="color: #f00;">
    child-b has single digit hex red

    <div class="child" id="grandchild-c" style="color: rgb(0,0,255);">
      grandchild-c isn't red
    </div>

    <div class="child" id="grandchild-d" style="color: #ff0000;">
      grandchild-d has double digit hex red
    </div>
  </div>
</div>
danh
  • 62,181
  • 10
  • 95
  • 136
0

You can create a wrapper function called findAllEle that accepts two arguments:

const findAllEle = (attr, value) => {
    return document.querySelectorAll(`[${attr}=${value}]`)
}

This function will return a list NodeList of all elements that each element has a structure like: <element attr="value">some text</element> or self closing <element attr="value" /> Element can be a div, p, h1, etc.

Dean Gite
  • 1,658
  • 1
  • 15
  • 17
0

This is the simplest way I found of doing it.

Thanks to this SO answer - https://stackoverflow.com/a/47355187/10276412 - for knowledge on the efficient handling of color names.

I'm only handling the color attribute's conversion as the question has required, but I'm making the findAllEle to accept and process any attribute, provided the function is extended to handle/process each attribute accordingly.

// Helper function
const toRgb = (color) => {
    let channels = [];
    // Handle color name input and convert to hex first
    if(color[0] !== '#') {
        var ctx = document.createElement('canvas').getContext('2d');
        ctx.fillStyle = color;
        color = ctx.fillStyle;
    }
    // Convert hex to rgb, because that's what the browsers seem to return
    const values = color.substring(1);
    channels = channels.length == 6 ? values.match(/.{1}/g) : values.match(/.{2}/g);
    channels = channels.map(channel => parseInt(channel, 16));
    console.log(channels)
    return `rgb(${channels.join(", ")})`;
}

// Main function
const findAllEle = (attr, value) => {
        let finalValue = '';
        if (attr == 'color') {
                finalValue = toRgb(value);
        }
        const allDOMElements = document.getRootNode().all;
        const elementsArr = Array.from(allDOMElements);
        const matches = elementsArr.filter(el => getComputedStyle(el)[attr] == finalValue);
        return matches;
}

findAllEle('color', 'black');

Please feel free to correct if there's anything incorrect or less optimal.

[update]

Editing to add explanation around some decisions/potential caveats.

  • Given that browsers return colors in RGB, it makes more sense to convert the input color into RGB right away than trying to match it with every type of color string of every DOM element.
  • Drilling down the DOM recursively is a fine approach to find all elements, but it is my assumption that when the browser itself offers a property that returns all the elements out of the box, it must be optimized to the best extent possible. I came across getRootNode().all by chance, and didn't find any documentation around the property. I have tested the property with a few different web pages, and it does seem to return all DOM elements. If anyone finds its behavior strange/incorrect in any way, I'd be glad to hear about it.
0

To find all the element based on attribute and it's value two cases should be consider here

  1. attr - value pair
  2. style -> prop - value pair

so in generic function we need to first check whether attr is related to some style prop then we will fetch style all elements style, loop though it and filter out elements which contains style prop with exact value. Here in case of color prop we need to take help of some lib or function which can conver string(white), hex(#fff or #ffffff), rgb(255,255,255) into some generic value which we can compare (I think this is out of scope for this answer, yet you can found some function as part of some stackoverflow questions).

taken the help from this post: https://stackoverflow.com/a/64550463/11953446

As part of this answer and considering large picture here is the final solution:

const getAllEle = (attr, value) => {
    // in case of color we need to create some generic function to convert current value and comparision value to some common value
    return CSS.supports(attr,value)) ? 
            [...document.querySelectorAll("*")].filter(ele => ele.style[attr] === value) // use some function to convert color values
     :
      [...document.querySelectorAll(`[${attr}=${value}]`)]
}
Prashant Shah
  • 226
  • 1
  • 8