javascript
exercises
exercises.js⚡javascript
/**
* Advanced DOM Traversal - Exercises
* Practice advanced DOM navigation techniques
*
* Note: These exercises are for browser environments.
* Use the solutions as reference for browser implementations.
*/
// =============================================================================
// EXERCISE 1: TreeWalker Text Finder
// Create a utility to find and manipulate text nodes
// =============================================================================
/*
* TODO: Create TextFinder class that:
* - Uses TreeWalker to find text nodes
* - Supports case-insensitive search
* - Can highlight found text
* - Can replace text content
* - Provides navigation between matches
*/
class TextFinder {
// YOUR CODE HERE:
}
// SOLUTION:
/*
class TextFinder {
constructor(root = document.body) {
this.root = root;
this.matches = [];
this.currentIndex = -1;
this.highlightClass = 'text-finder-highlight';
}
find(searchText, options = {}) {
const { caseSensitive = false, wholeWord = false } = options;
this.clearHighlights();
this.matches = [];
this.currentIndex = -1;
const walker = document.createTreeWalker(
this.root,
NodeFilter.SHOW_TEXT,
{
acceptNode: (node) => {
const text = caseSensitive
? node.textContent
: node.textContent.toLowerCase();
const search = caseSensitive
? searchText
: searchText.toLowerCase();
if (wholeWord) {
const regex = new RegExp(`\\b${search}\\b`);
return regex.test(text)
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT;
}
return text.includes(search)
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT;
}
}
);
while (walker.nextNode()) {
this.matches.push(walker.currentNode);
}
return this.matches.length;
}
highlightAll() {
this.matches.forEach((node, index) => {
this.highlightNode(node, index);
});
}
highlightNode(textNode, index) {
const parent = textNode.parentElement;
const text = textNode.textContent;
const span = document.createElement('span');
span.className = this.highlightClass;
span.dataset.matchIndex = index;
span.textContent = text;
parent.replaceChild(span, textNode);
this.matches[index] = span;
}
clearHighlights() {
document.querySelectorAll('.' + this.highlightClass).forEach(span => {
const textNode = document.createTextNode(span.textContent);
span.parentNode.replaceChild(textNode, span);
});
}
next() {
if (this.matches.length === 0) return null;
this.currentIndex = (this.currentIndex + 1) % this.matches.length;
return this.scrollToMatch(this.currentIndex);
}
previous() {
if (this.matches.length === 0) return null;
this.currentIndex = this.currentIndex <= 0
? this.matches.length - 1
: this.currentIndex - 1;
return this.scrollToMatch(this.currentIndex);
}
scrollToMatch(index) {
const match = this.matches[index];
if (match) {
match.scrollIntoView({ behavior: 'smooth', block: 'center' });
return match;
}
return null;
}
replace(replacement) {
if (this.currentIndex >= 0 && this.matches[this.currentIndex]) {
const match = this.matches[this.currentIndex];
match.textContent = replacement;
this.matches.splice(this.currentIndex, 1);
this.currentIndex--;
}
}
replaceAll(replacement) {
this.matches.forEach(match => {
if (match.nodeType === Node.TEXT_NODE) {
match.textContent = replacement;
} else {
match.textContent = replacement;
}
});
this.matches = [];
this.currentIndex = -1;
}
}
*/
// =============================================================================
// EXERCISE 2: DOM Path Generator
// Generate unique selectors for elements
// =============================================================================
/*
* TODO: Create generatePath(element) function that:
* - Creates unique CSS selector path to element
* - Uses IDs when available
* - Falls back to nth-child for ambiguous elements
* - Returns shortest possible path
*/
function generatePath(element) {
// YOUR CODE HERE:
}
// SOLUTION:
/*
function generatePath(element) {
if (!element || element === document.documentElement) {
return 'html';
}
// If element has ID, use it
if (element.id) {
return '#' + element.id;
}
const path = [];
let current = element;
while (current && current !== document.documentElement) {
let selector = current.tagName.toLowerCase();
// Check if ID exists
if (current.id) {
path.unshift('#' + current.id);
break;
}
// Add nth-child if needed for uniqueness
const parent = current.parentElement;
if (parent) {
const siblings = Array.from(parent.children)
.filter(child => child.tagName === current.tagName);
if (siblings.length > 1) {
const index = siblings.indexOf(current) + 1;
selector += ':nth-of-type(' + index + ')';
}
}
path.unshift(selector);
current = current.parentElement;
}
return path.join(' > ');
}
*/
// =============================================================================
// EXERCISE 3: Range Selection Helper
// Create a helper for working with text selections
// =============================================================================
/*
* TODO: Create SelectionHelper class that:
* - Gets selected text and range
* - Wraps selection in a specified element
* - Expands selection to word/sentence/paragraph
* - Saves and restores selections
*/
class SelectionHelper {
// YOUR CODE HERE:
}
// SOLUTION:
/*
class SelectionHelper {
constructor() {
this.savedSelections = [];
}
getSelection() {
return window.getSelection();
}
getSelectedText() {
return this.getSelection().toString();
}
getSelectedRange() {
const selection = this.getSelection();
if (selection.rangeCount > 0) {
return selection.getRangeAt(0);
}
return null;
}
wrapSelection(tagName, attributes = {}) {
const range = this.getSelectedRange();
if (!range || range.collapsed) return null;
const wrapper = document.createElement(tagName);
Object.entries(attributes).forEach(([key, value]) => {
wrapper.setAttribute(key, value);
});
try {
range.surroundContents(wrapper);
return wrapper;
} catch (e) {
// Handle partial selection across elements
const contents = range.extractContents();
wrapper.appendChild(contents);
range.insertNode(wrapper);
return wrapper;
}
}
expandToWord() {
const selection = this.getSelection();
if (selection.rangeCount === 0) return;
// Use modify method if available
if (selection.modify) {
selection.modify('extend', 'backward', 'word');
selection.modify('extend', 'forward', 'word');
}
}
expandToSentence() {
const range = this.getSelectedRange();
if (!range) return;
const text = range.startContainer.textContent;
let start = range.startOffset;
let end = range.endOffset;
// Find sentence start
while (start > 0 && !/[.!?]/.test(text[start - 1])) {
start--;
}
// Find sentence end
while (end < text.length && !/[.!?]/.test(text[end])) {
end++;
}
if (end < text.length) end++; // Include punctuation
range.setStart(range.startContainer, start);
range.setEnd(range.startContainer, end);
}
saveSelection() {
const range = this.getSelectedRange();
if (range) {
this.savedSelections.push({
startContainer: range.startContainer,
startOffset: range.startOffset,
endContainer: range.endContainer,
endOffset: range.endOffset
});
}
}
restoreSelection(index = -1) {
const saved = index === -1
? this.savedSelections.pop()
: this.savedSelections[index];
if (!saved) return false;
const range = document.createRange();
range.setStart(saved.startContainer, saved.startOffset);
range.setEnd(saved.endContainer, saved.endOffset);
const selection = this.getSelection();
selection.removeAllRanges();
selection.addRange(range);
return true;
}
clearSelection() {
this.getSelection().removeAllRanges();
}
}
*/
// =============================================================================
// EXERCISE 4: Element Finder
// Create advanced element finding utilities
// =============================================================================
/*
* TODO: Create ElementFinder class with methods:
* - byVisibility(visible: boolean) - find visible/hidden elements
* - byZIndex(minZ, maxZ) - find by z-index range
* - byOverlap(element) - find elements overlapping with given element
* - byTextContent(text, exact) - find by text content
* - byComputedStyle(property, value) - find by computed style
*/
class ElementFinder {
// YOUR CODE HERE:
}
// SOLUTION:
/*
class ElementFinder {
constructor(root = document.body) {
this.root = root;
}
getAllElements() {
return Array.from(this.root.querySelectorAll('*'));
}
byVisibility(visible = true) {
return this.getAllElements().filter(el => {
const style = getComputedStyle(el);
const isVisible =
style.display !== 'none' &&
style.visibility !== 'hidden' &&
style.opacity !== '0' &&
el.offsetParent !== null;
return visible ? isVisible : !isVisible;
});
}
byZIndex(minZ = -Infinity, maxZ = Infinity) {
return this.getAllElements().filter(el => {
const style = getComputedStyle(el);
const zIndex = parseInt(style.zIndex);
if (isNaN(zIndex)) return false;
return zIndex >= minZ && zIndex <= maxZ;
});
}
byOverlap(targetElement) {
const targetRect = targetElement.getBoundingClientRect();
return this.getAllElements().filter(el => {
if (el === targetElement) return false;
const rect = el.getBoundingClientRect();
return !(
rect.right < targetRect.left ||
rect.left > targetRect.right ||
rect.bottom < targetRect.top ||
rect.top > targetRect.bottom
);
});
}
byTextContent(text, exact = false) {
return this.getAllElements().filter(el => {
const content = el.textContent.trim();
if (exact) {
return content === text;
}
return content.toLowerCase().includes(text.toLowerCase());
});
}
byComputedStyle(property, value) {
return this.getAllElements().filter(el => {
const style = getComputedStyle(el);
return style[property] === value;
});
}
byAttribute(name, value = null) {
if (value === null) {
return this.getAllElements().filter(el => el.hasAttribute(name));
}
return this.getAllElements().filter(el =>
el.getAttribute(name) === value
);
}
inViewport() {
const viewport = {
top: 0,
left: 0,
bottom: window.innerHeight,
right: window.innerWidth
};
return this.getAllElements().filter(el => {
const rect = el.getBoundingClientRect();
return (
rect.top < viewport.bottom &&
rect.bottom > viewport.top &&
rect.left < viewport.right &&
rect.right > viewport.left
);
});
}
}
*/
// =============================================================================
// EXERCISE 5: DOM Diff Utility
// Compare two DOM structures
// =============================================================================
/*
* TODO: Create DOMDiff class that:
* - Compares two DOM elements/subtrees
* - Identifies added, removed, changed nodes
* - Tracks attribute changes
* - Returns patch operations
*/
class DOMDiff {
// YOUR CODE HERE:
}
// SOLUTION:
/*
class DOMDiff {
diff(oldNode, newNode) {
const patches = [];
this.walkAndDiff(oldNode, newNode, patches, []);
return patches;
}
walkAndDiff(oldNode, newNode, patches, path) {
// Node removed
if (!newNode) {
patches.push({
type: 'REMOVE',
path: [...path],
oldNode
});
return;
}
// Node added
if (!oldNode) {
patches.push({
type: 'ADD',
path: [...path],
newNode
});
return;
}
// Different node types
if (oldNode.nodeType !== newNode.nodeType) {
patches.push({
type: 'REPLACE',
path: [...path],
oldNode,
newNode
});
return;
}
// Text node changed
if (oldNode.nodeType === Node.TEXT_NODE) {
if (oldNode.textContent !== newNode.textContent) {
patches.push({
type: 'TEXT',
path: [...path],
oldValue: oldNode.textContent,
newValue: newNode.textContent
});
}
return;
}
// Different elements
if (oldNode.tagName !== newNode.tagName) {
patches.push({
type: 'REPLACE',
path: [...path],
oldNode,
newNode
});
return;
}
// Check attributes
this.diffAttributes(oldNode, newNode, patches, path);
// Check children
const oldChildren = Array.from(oldNode.childNodes);
const newChildren = Array.from(newNode.childNodes);
const maxLength = Math.max(oldChildren.length, newChildren.length);
for (let i = 0; i < maxLength; i++) {
this.walkAndDiff(
oldChildren[i],
newChildren[i],
patches,
[...path, i]
);
}
}
diffAttributes(oldEl, newEl, patches, path) {
const oldAttrs = this.getAttributes(oldEl);
const newAttrs = this.getAttributes(newEl);
// Check for changed or removed attributes
for (const [name, value] of Object.entries(oldAttrs)) {
if (!(name in newAttrs)) {
patches.push({
type: 'REMOVE_ATTR',
path: [...path],
name
});
} else if (newAttrs[name] !== value) {
patches.push({
type: 'SET_ATTR',
path: [...path],
name,
oldValue: value,
newValue: newAttrs[name]
});
}
}
// Check for new attributes
for (const [name, value] of Object.entries(newAttrs)) {
if (!(name in oldAttrs)) {
patches.push({
type: 'SET_ATTR',
path: [...path],
name,
newValue: value
});
}
}
}
getAttributes(element) {
const attrs = {};
for (const attr of element.attributes || []) {
attrs[attr.name] = attr.value;
}
return attrs;
}
apply(root, patches) {
for (const patch of patches) {
const target = this.getNodeByPath(root, patch.path);
switch (patch.type) {
case 'REMOVE':
target.parentNode.removeChild(target);
break;
case 'ADD':
// Handle add
break;
case 'REPLACE':
target.parentNode.replaceChild(
patch.newNode.cloneNode(true),
target
);
break;
case 'TEXT':
target.textContent = patch.newValue;
break;
case 'SET_ATTR':
target.setAttribute(patch.name, patch.newValue);
break;
case 'REMOVE_ATTR':
target.removeAttribute(patch.name);
break;
}
}
}
getNodeByPath(root, path) {
let node = root;
for (const index of path) {
node = node.childNodes[index];
}
return node;
}
}
*/
// =============================================================================
// EXERCISE 6: Shadow DOM Query
// Create utilities for querying across shadow boundaries
// =============================================================================
/*
* TODO: Create ShadowQuery class that:
* - Queries elements across shadow DOM boundaries
* - Supports CSS selectors
* - Can traverse into open shadow roots
* - Returns flat list of matching elements
*/
class ShadowQuery {
// YOUR CODE HERE:
}
// SOLUTION:
/*
class ShadowQuery {
static query(selector, root = document) {
const results = [];
// Query current context
results.push(...root.querySelectorAll(selector));
// Find all elements with shadow roots
const walker = document.createTreeWalker(
root,
NodeFilter.SHOW_ELEMENT,
null
);
while (walker.nextNode()) {
const node = walker.currentNode;
if (node.shadowRoot) {
results.push(...ShadowQuery.query(selector, node.shadowRoot));
}
}
return results;
}
static queryOne(selector, root = document) {
// Check current context first
const result = root.querySelector(selector);
if (result) return result;
// Search in shadow roots
const walker = document.createTreeWalker(
root,
NodeFilter.SHOW_ELEMENT,
null
);
while (walker.nextNode()) {
const node = walker.currentNode;
if (node.shadowRoot) {
const shadowResult = ShadowQuery.queryOne(selector, node.shadowRoot);
if (shadowResult) return shadowResult;
}
}
return null;
}
static closest(element, selector) {
let current = element;
while (current) {
if (current.matches && current.matches(selector)) {
return current;
}
if (current.parentElement) {
current = current.parentElement;
} else if (current.host) {
// Cross shadow boundary
current = current.host;
} else {
current = null;
}
}
return null;
}
static getPath(element) {
const path = [];
let current = element;
while (current) {
path.unshift(current);
if (current.parentElement) {
current = current.parentElement;
} else if (current.host) {
path.unshift(current.host.shadowRoot);
current = current.host;
} else {
current = null;
}
}
return path;
}
}
*/
// =============================================================================
// TEST YOUR IMPLEMENTATIONS
// =============================================================================
function runTests() {
console.log('Advanced DOM Traversal - Exercises');
console.log('==================================');
console.log('These exercises require a browser environment.');
console.log('Use the solutions as reference for implementation.');
// Test structure
console.log('\nTest your implementations in a browser with:');
console.log('1. Create an HTML file with test elements');
console.log('2. Include this script');
console.log('3. Test each class/function in console');
}
// Run tests
runTests();