Skip to content

Commit

Permalink
feat(devexp): highlight components by group
Browse files Browse the repository at this point in the history
  • Loading branch information
matthieu-crouzet committed Dec 2, 2024
1 parent 78718fe commit f923307
Show file tree
Hide file tree
Showing 2 changed files with 215 additions and 0 deletions.
5 changes: 5 additions & 0 deletions apps/showcase/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import {
import {
SideNavLinksGroup,
} from '../components/index';
import {
HighlightService,
} from '../services/highlight';

@O3rComponent({ componentType: 'Component' })
@Component({
Expand All @@ -36,6 +39,8 @@ import {
export class AppComponent implements OnDestroy {
public title = 'showcase';

public readonly service = inject(HighlightService);

public linksGroups: SideNavLinksGroup[] = [
{
label: '',
Expand Down
210 changes: 210 additions & 0 deletions apps/showcase/src/services/highlight/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import {
Injectable,
OnDestroy,
} from '@angular/core';

const HIGHLIGHT_WRAPPER_CLASS = 'highlight-wrapper';
const HIGHLIGHT_OVERLAY_CLASS = 'highlight-overlay';
const HIGHLIGHT_CHIP_CLASS = 'highlight-chip';
// Should we set it customizable (if yes, chrome extension view or options)
const ELEMENT_MIN_HEIGHT = 30;
// Should we set it customizable (if yes, chrome extension view or options)
const ELEMENT_MIN_WIDTH = 60;
// Should we set it customizable (if yes, chrome extension view or options)
const REFRESH_INTERVAL = 2000;

interface ElementInfo {
color?: string;
backgroundColor: string;
displayName: string;
regexp: string;
}

interface ElementWithSelectorInfo {
element: HTMLElement;
info: ElementInfo;
}

interface ElementWithSelectorInfoAndDepth extends ElementWithSelectorInfo {
depth: number;
}

function getIdentifier(element: HTMLElement, info: ElementInfo): string {
const tagName = element.tagName.toLowerCase();
const regexp = new RegExp(info.regexp, 'i');

Check warning on line 34 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L32-L34

Added lines #L32 - L34 were not covered by tests
if (!regexp.test(element.tagName)) {
const attribute = Array.from(element.attributes).find((att) => regexp.test(att.name));

Check warning on line 36 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L36

Added line #L36 was not covered by tests
if (attribute) {
return `${attribute.name}${attribute.value ? `="${attribute.value}"` : ''}`;
}
const className = Array.from(element.classList).find((cName) => regexp.test(cName));

Check warning on line 40 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L40

Added line #L40 was not covered by tests
if (className) {
return className;

Check warning on line 42 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L42

Added line #L42 was not covered by tests
}
}
return tagName;

Check warning on line 45 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L45

Added line #L45 was not covered by tests
}

/**
* Compute the number of ancestors of a given element based on a list of elements
* @param element
* @param elementList
*/
function computeNumberOfAncestors(element: HTMLElement, elementList: HTMLElement[]) {
return elementList.filter((el: HTMLElement) => el.contains(element)).length;

Check warning on line 54 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L53-L54

Added lines #L53 - L54 were not covered by tests
}

@Injectable({
providedIn: 'root'
})
export class HighlightService implements OnDestroy {
// Should be customizable from the chrome extension view
public maxDepth = 10;

// Should be customizable from the chrome extension options
public elementsInfo: Record<string, ElementInfo> = {
otter: {
backgroundColor: '#f4dac6',
color: 'black',
regexp: '^o3r',
displayName: 'o3r'
},
designFactory: {
backgroundColor: '#000835',
regexp: '^df',
displayName: 'df'
},
ngBootstrap: {
backgroundColor: '#0d6efd',
regexp: '^ngb',
displayName: 'ngb'
}
};

private interval: ReturnType<typeof setInterval> | null = null;

constructor() {
this.start();
}

public start() {
if (!this.interval) {
this.interval = setInterval(this.run.bind(this), REFRESH_INTERVAL);
}
}

public stop() {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
}

public run() {
let wrapper = document.querySelector(`.${HIGHLIGHT_WRAPPER_CLASS}`);

Check warning on line 104 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L103-L104

Added lines #L103 - L104 were not covered by tests
if (wrapper) {
wrapper.childNodes.forEach((node) => node.remove());

Check warning on line 106 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L106

Added line #L106 was not covered by tests
} else {
wrapper = document.createElement('div');
wrapper.classList.add(HIGHLIGHT_WRAPPER_CLASS);
document.body.append(wrapper);

Check warning on line 110 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L108-L110

Added lines #L108 - L110 were not covered by tests
}

// We have to select all elements from document because
// with CSSSelector it's impossible to select element by regex on their `tagName`, attribute name or attribute value
const elementsWithInfo = Array.from(document.querySelectorAll<HTMLElement>('*'))
.reduce((acc: ElementWithSelectorInfo[], element) => {
const rect = element.getBoundingClientRect();

Check warning on line 117 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L115-L117

Added lines #L115 - L117 were not covered by tests
if (rect.height < ELEMENT_MIN_HEIGHT || rect.width < ELEMENT_MIN_WIDTH) {
return acc;

Check warning on line 119 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L119

Added line #L119 was not covered by tests
}
const elementInfo = Object.values(this.elementsInfo).find((info) => {
const regexp = new RegExp(`^${info.regexp}`, 'i');

Check warning on line 122 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L121-L122

Added lines #L121 - L122 were not covered by tests

return regexp.test(element.tagName)
|| Array.from(element.attributes).some((attr) => regexp.test(attr.name))
|| Array.from(element.classList).some((cName) => regexp.test(cName));

Check warning on line 126 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L125-L126

Added lines #L125 - L126 were not covered by tests
});
if (elementInfo) {
return acc.concat({ element, info: elementInfo });

Check warning on line 129 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L129

Added line #L129 was not covered by tests
}
return acc;

Check warning on line 131 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L131

Added line #L131 was not covered by tests
}, [])
.reduce((acc: ElementWithSelectorInfoAndDepth[], elementWithInfo, _, array) => {
const depth = computeNumberOfAncestors(elementWithInfo.element, array.map((e) => e.element));

Check warning on line 134 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L133-L134

Added lines #L133 - L134 were not covered by tests
if (depth <= this.maxDepth) {
return acc.concat({

Check warning on line 136 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L136

Added line #L136 was not covered by tests
...elementWithInfo,
depth
});
}
return acc;

Check warning on line 141 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L141

Added line #L141 was not covered by tests
}, []);

const overlayData: Record<string, { chip: HTMLElement; overlay: HTMLElement; depth: number }[]> = {};
elementsWithInfo.forEach(({ element, info, depth }) => {
const { backgroundColor, color, displayName } = info;
const rect = element.getBoundingClientRect();
const overlay = document.createElement('div');
const chip = document.createElement('div');

Check warning on line 149 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L144-L149

Added lines #L144 - L149 were not covered by tests
const position = element.computedStyleMap().get('position')?.toString() === 'fixed' ? 'fixed' : 'absolute';
const top = `${position === 'fixed' ? rect.top : (rect.top + window.scrollY)}px`;
const left = `${position === 'fixed' ? rect.left : (rect.left + window.scrollX)}px`;
overlay.classList.add(HIGHLIGHT_OVERLAY_CLASS);

Check warning on line 153 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L153

Added line #L153 was not covered by tests
// All static style could be moved in a <style>
overlay.style.top = top;
overlay.style.left = left;
overlay.style.width = `${rect.width}px`;
overlay.style.height = `${rect.height}px`;
overlay.style.border = `1px solid ${backgroundColor}`;
overlay.style.zIndex = '10000';
overlay.style.position = position;
overlay.style.pointerEvents = 'none';
wrapper.append(overlay);
chip.classList.add(HIGHLIGHT_CHIP_CLASS);
chip.textContent = `${displayName} ${depth}`;

Check warning on line 165 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L155-L165

Added lines #L155 - L165 were not covered by tests
// All static style could be moved in a <style>
chip.style.top = top;
chip.style.left = left;
chip.style.backgroundColor = backgroundColor;

Check warning on line 169 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L167-L169

Added lines #L167 - L169 were not covered by tests
chip.style.color = color ?? '#FFF';
chip.style.position = position;
chip.style.display = 'inline-block';
chip.style.padding = '2px 4px';
chip.style.borderRadius = '0 0 4px';
chip.style.cursor = 'pointer';
chip.style.zIndex = '10000';
chip.style.textWrap = 'no-wrap';
const name = getIdentifier(element, info);
chip.title = name;
wrapper.append(chip);
chip.addEventListener('click', () => {

Check warning on line 181 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L171-L181

Added lines #L171 - L181 were not covered by tests
// Should we log in the console as well ?
void navigator.clipboard.writeText(name);

Check warning on line 183 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L183

Added line #L183 was not covered by tests
});
const positionKey = `${top};${left}`;

Check warning on line 185 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L185

Added line #L185 was not covered by tests
if (!overlayData[positionKey]) {
overlayData[positionKey] = [];

Check warning on line 187 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L187

Added line #L187 was not covered by tests
}
overlayData[positionKey].push({ chip, overlay, depth });

Check warning on line 189 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L189

Added line #L189 was not covered by tests
});
Object.values(overlayData).forEach((chips) => {
chips
.sort(({ depth: depthA }, { depth: depthB }) => depthA - depthB)
.forEach(({ chip, overlay }, index, array) => {

Check warning on line 194 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L191-L194

Added lines #L191 - L194 were not covered by tests
if (index !== 0) {
const translateX = array.slice(0, index).reduce((sum, e) => sum + e.chip.getBoundingClientRect().width, 0);
chip.style.transform = `translateX(${translateX}px)`;
overlay.style.margin = `${index}px 0 0 ${index}px`;
overlay.style.width = `${+overlay.style.width.replace('px', '') - 2 * index}px`;
overlay.style.height = `${+overlay.style.height.replace('px', '') - 2 * index}px`;
overlay.style.zIndex = `${+overlay.style.zIndex - index}`;

Check warning on line 201 in apps/showcase/src/services/highlight/index.ts

View check run for this annotation

Codecov / codecov/patch

apps/showcase/src/services/highlight/index.ts#L196-L201

Added lines #L196 - L201 were not covered by tests
}
});
});
}

public ngOnDestroy() {
this.stop();
}
}

0 comments on commit f923307

Please sign in to comment.