Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(devexp): highlight components by group #2540

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import type {
import type {
PlaceholderMode,
} from '../stores';
import {
GroupInfo,
} from './highlight/models';
import {
OtterLikeComponentInfo,
} from './inspector';
Expand All @@ -32,6 +35,40 @@ export interface ToggleInspectorMessage extends OtterMessageContent<'toggleInspe
isRunning: boolean;
}

/**
* Message to toggle the highlight
*/
export interface ToggleHighlightMessage extends OtterMessageContent<'toggleHighlight'> {
/** Is the highlight displayed */
isRunning: boolean;
}

/**
* Message the change the configuration of the `HighlightService`
*/
export interface ChangeHighlightConfiguration extends OtterMessageContent<'changeHighlightConfiguration'> {
/**
* Minimum width of HTMLElement to be considered
*/
elementMinWidth?: number;
/**
* Minimum height of HTMLElement to be considered
*/
elementMinHeight?: number;
/**
* Throttle interval
*/
throttleInterval?: number;
/**
* Group information to detect elements
*/
groupsInfo?: Record<string, GroupInfo>;
/**
* Maximum number of ancestors
*/
maxDepth?: number;
}

/**
* Message to toggle the placeholder mode
*/
Expand All @@ -51,6 +88,8 @@ type ComponentsMessageContents =
| IsComponentSelectionAvailableMessage
| SelectedComponentInfoMessage
| ToggleInspectorMessage
| ToggleHighlightMessage
| ChangeHighlightConfiguration
| PlaceholderModeMessage;

/** List of possible DataTypes for Components messages */
Expand All @@ -74,5 +113,7 @@ export const isComponentsMessage = (message: any): message is AvailableComponent
|| message.dataType === 'isComponentSelectionAvailable'
|| message.dataType === 'placeholderMode'
|| message.dataType === 'toggleInspector'
|| message.dataType === 'toggleHighlight'
|| message.dataType === 'changeHighlightConfiguration'
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
OTTER_COMPONENTS_DEVTOOLS_DEFAULT_OPTIONS,
OTTER_COMPONENTS_DEVTOOLS_OPTIONS,
} from './components-devtools.token';
import {
HighlightService,
} from './highlight/highlight.service';
import {
OtterInspectorService,
OtterLikeComponentInfo,
Expand All @@ -51,6 +54,7 @@
export class ComponentsDevtoolsMessageService implements DevtoolsServiceInterface {
private readonly options: ComponentsDevtoolsServiceOptions;
private readonly inspectorService: OtterInspectorService;
private readonly highlightService: HighlightService;
private readonly sendMessage = sendOtterMessage<AvailableComponentsMessageContents>;
private readonly destroyRef = inject(DestroyRef);

Expand All @@ -65,6 +69,8 @@
};

this.inspectorService = new OtterInspectorService();
this.highlightService = new HighlightService();

Check warning on line 72 in packages/@o3r/components/src/devkit/components-devtools.message.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/components-devtools.message.service.ts#L72

Added line #L72 was not covered by tests

if (this.options.isActivatedOnBootstrap) {
this.activate();
}
Expand Down Expand Up @@ -130,6 +136,36 @@
this.inspectorService.toggleInspector(message.isRunning);
break;
}
case 'toggleHighlight': {
if (message.isRunning) {
this.highlightService.start();

Check warning on line 141 in packages/@o3r/components/src/devkit/components-devtools.message.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/components-devtools.message.service.ts#L141

Added line #L141 was not covered by tests
} else {
this.highlightService.stop();

Check warning on line 143 in packages/@o3r/components/src/devkit/components-devtools.message.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/components-devtools.message.service.ts#L143

Added line #L143 was not covered by tests
}
break;

Check warning on line 145 in packages/@o3r/components/src/devkit/components-devtools.message.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/components-devtools.message.service.ts#L145

Added line #L145 was not covered by tests
}
case 'changeHighlightConfiguration': {
if (message.elementMinWidth) {
this.highlightService.elementMinWidth = message.elementMinWidth;

Check warning on line 149 in packages/@o3r/components/src/devkit/components-devtools.message.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/components-devtools.message.service.ts#L149

Added line #L149 was not covered by tests
}
if (message.elementMinHeight) {
this.highlightService.elementMinHeight = message.elementMinHeight;

Check warning on line 152 in packages/@o3r/components/src/devkit/components-devtools.message.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/components-devtools.message.service.ts#L152

Added line #L152 was not covered by tests
}
if (message.throttleInterval) {
this.highlightService.throttleInterval = message.throttleInterval;

Check warning on line 155 in packages/@o3r/components/src/devkit/components-devtools.message.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/components-devtools.message.service.ts#L155

Added line #L155 was not covered by tests
}
if (message.groupsInfo) {
this.highlightService.groupsInfo = message.groupsInfo;

Check warning on line 158 in packages/@o3r/components/src/devkit/components-devtools.message.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/components-devtools.message.service.ts#L158

Added line #L158 was not covered by tests
}
if (message.maxDepth) {
this.highlightService.maxDepth = message.maxDepth;

Check warning on line 161 in packages/@o3r/components/src/devkit/components-devtools.message.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/components-devtools.message.service.ts#L161

Added line #L161 was not covered by tests
}
if (this.highlightService.isRunning()) {
// Re-start to recompute the highlight with the new configuration
this.highlightService.start();

Check warning on line 165 in packages/@o3r/components/src/devkit/components-devtools.message.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/components-devtools.message.service.ts#L165

Added line #L165 was not covered by tests
}
break;

Check warning on line 167 in packages/@o3r/components/src/devkit/components-devtools.message.service.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/components-devtools.message.service.ts#L167

Added line #L167 was not covered by tests
}
case 'placeholderMode': {
this.store.dispatch(togglePlaceholderModeTemplate({ mode: message.mode }));
break;
Expand Down
32 changes: 32 additions & 0 deletions packages/@o3r/components/src/devkit/highlight/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Class applied on the wrapper of highlight elements
*/
export const HIGHLIGHT_WRAPPER_CLASS = 'highlight-wrapper';

/**
* Class applied on the overlay elements
*/
export const HIGHLIGHT_OVERLAY_CLASS = 'highlight-overlay';

/**
* Class applied on the chip elements
*/
export const HIGHLIGHT_CHIP_CLASS = 'highlight-chip';

/**
* Default value for maximum number of ancestors
*/
export const DEFAULT_MAX_DEPTH = 10;

/**
* Default value for element min height
*/
export const DEFAULT_ELEMENT_MIN_HEIGHT = 30;
/**
* Default value for element min width
*/
export const DEFAULT_ELEMENT_MIN_WIDTH = 60;
/**
* Default value for throttle interval
*/
export const DEFAULT_THROTTLE_INTERVAL = 500;
66 changes: 66 additions & 0 deletions packages/@o3r/components/src/devkit/highlight/helpers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {
getIdentifier,
} from './helpers';
import {
ElementWithGroupInfo,
} from './models';

describe('Highlight helpers', () => {
describe('getIdentifier', () => {
it('should return the tagName', () => {
const element = {
htmlElement: {
tagName: 'prefix-selector'
} as HTMLElement,
regexp: '^prefix',
backgroundColor: 'blue',
displayName: 'prefix'
} satisfies ElementWithGroupInfo;
expect(getIdentifier(element)).toBe('prefix-selector');
});

it('should return the first attributeName matching', () => {
const element = {
htmlElement: {
tagName: 'custom-selector',
attributes: [
{ name: 'custom-attribute' },
{ name: 'prefix-attribute' },
{ name: 'prefix-attribute-2' }
] as any as NamedNodeMap
} as HTMLElement,
regexp: '^prefix',
backgroundColor: 'blue',
displayName: 'prefix'
} satisfies ElementWithGroupInfo;
expect(getIdentifier(element)).toBe('prefix-attribute');
});

it('should return the first attributeName matching with its value', () => {
const element = {
htmlElement: {
tagName: 'custom-selector',
attributes: [{ name: 'prefix-attribute', value: 'value' }] as any as NamedNodeMap
} as HTMLElement,
regexp: '^prefix',
backgroundColor: 'blue',
displayName: 'prefix'
} satisfies ElementWithGroupInfo;
expect(getIdentifier(element)).toBe('prefix-attribute="value"');
});

it('should return the first className matching', () => {
const element = {
htmlElement: {
tagName: 'custom-selector',
attributes: [] as any as NamedNodeMap,
classList: ['custom-class', 'prefix-class', 'prefix-class-2'] as any as DOMTokenList
} as HTMLElement,
regexp: '^prefix',
backgroundColor: 'blue',
displayName: 'prefix'
} satisfies ElementWithGroupInfo;
expect(getIdentifier(element)).toBe('prefix-class');
});
});
});
154 changes: 154 additions & 0 deletions packages/@o3r/components/src/devkit/highlight/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import {
HIGHLIGHT_CHIP_CLASS,
HIGHLIGHT_OVERLAY_CLASS,
HIGHLIGHT_WRAPPER_CLASS,
} from './constants';
import type {
ElementWithGroupInfo,
} from './models';

/**
* Retrieve the identifier of the element
* @param element
*/
export function getIdentifier(element: ElementWithGroupInfo): string {
const { tagName, attributes, classList } = element.htmlElement;
const regexp = new RegExp(element.regexp, 'i');
if (!regexp.test(tagName)) {
const attribute = Array.from(attributes).find((attr) => regexp.test(attr.name));
if (attribute) {
return `${attribute.name}${attribute.value ? `="${attribute.value}"` : ''}`;
}
const className = Array.from(classList).find((cName) => regexp.test(cName));
if (className) {
return className;
}
}
return tagName;
}

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

Check warning on line 36 in packages/@o3r/components/src/devkit/highlight/helpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/highlight/helpers.ts#L36

Added line #L36 was not covered by tests
}

/**
* Throttle {@link fn} with a {@link delay}
* @param fn method to run
* @param delay given in ms
*/
export function throttle<T extends (...args: any[]) => any>(fn: T, delay: number): (...args: Parameters<T>) => void {
let timerFlag: ReturnType<typeof setTimeout> | null = null;

Check warning on line 45 in packages/@o3r/components/src/devkit/highlight/helpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/highlight/helpers.ts#L45

Added line #L45 was not covered by tests

const throttleFn = (...args: Parameters<T>) => {

Check warning on line 47 in packages/@o3r/components/src/devkit/highlight/helpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/highlight/helpers.ts#L47

Added line #L47 was not covered by tests
if (timerFlag === null) {
fn(...args);
timerFlag = setTimeout(() => {
fn(...args);
timerFlag = null;

Check warning on line 52 in packages/@o3r/components/src/devkit/highlight/helpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/highlight/helpers.ts#L49-L52

Added lines #L49 - L52 were not covered by tests
}, delay);
}
};
return throttleFn;

Check warning on line 56 in packages/@o3r/components/src/devkit/highlight/helpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/highlight/helpers.ts#L56

Added line #L56 was not covered by tests
}

/**
* Run {@link refreshFn} if {@link mutations} implies to refresh elements inside {@link highlightWrapper}
* @param mutations
* @param highlightWrapper
* @param refreshFn
*/
export function runRefreshIfNeeded(mutations: MutationRecord[], highlightWrapper: Element | null, refreshFn: () => void) {
if (
mutations.some((mutation) =>

Check warning on line 67 in packages/@o3r/components/src/devkit/highlight/helpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/highlight/helpers.ts#L67

Added line #L67 was not covered by tests

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there is some issue with the refresh
image

mutation.target !== highlightWrapper
|| (
mutation.target === document.body
&& Array.from<HTMLElement>(mutation.addedNodes.values() as any)
.concat(...mutation.removedNodes.values() as any)
.some((node) => !node.classList.contains(HIGHLIGHT_WRAPPER_CLASS))

Check warning on line 73 in packages/@o3r/components/src/devkit/highlight/helpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/highlight/helpers.ts#L73

Added line #L73 was not covered by tests
)
)
) {
refreshFn();

Check warning on line 77 in packages/@o3r/components/src/devkit/highlight/helpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/highlight/helpers.ts#L77

Added line #L77 was not covered by tests
}
}

/**
* Options to create an overlay element
*/
export interface CreateOverlayOptions {
top: string;
left: string;
position: string;
width: string;
height: string;
backgroundColor: string;
}

/**
* Create an overlay element
* @param doc HTML Document
* @param opts
*/
export function createOverlay(doc: Document, opts: CreateOverlayOptions) {
const overlay = doc.createElement('div');
overlay.classList.add(HIGHLIGHT_OVERLAY_CLASS);

Check warning on line 100 in packages/@o3r/components/src/devkit/highlight/helpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/highlight/helpers.ts#L99-L100

Added lines #L99 - L100 were not covered by tests
// All static style could be moved in a <style>
overlay.style.top = opts.top;
overlay.style.left = opts.left;
overlay.style.width = opts.width;
overlay.style.height = opts.height;
overlay.style.border = `1px solid ${opts.backgroundColor}`;
overlay.style.zIndex = '10000';
overlay.style.position = opts.position;
overlay.style.pointerEvents = 'none';
return overlay;

Check warning on line 110 in packages/@o3r/components/src/devkit/highlight/helpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/highlight/helpers.ts#L102-L110

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

/**
* Options to create a chip element
*/
export interface CreateChipOptions {
displayName: string;
depth: number;
top: string;
left: string;
position: string;
backgroundColor: string;
color?: string;
name: string;
}

/**
* Create a chip element
* @param doc HTML Document
* @param opts
*/
export function createChip(doc: Document, opts: CreateChipOptions) {
const chip = doc.createElement('div');
chip.classList.add(HIGHLIGHT_CHIP_CLASS);
chip.textContent = `${opts.displayName} ${opts.depth}`;

Check warning on line 135 in packages/@o3r/components/src/devkit/highlight/helpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/highlight/helpers.ts#L133-L135

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

Check warning on line 139 in packages/@o3r/components/src/devkit/highlight/helpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/highlight/helpers.ts#L137-L139

Added lines #L137 - L139 were not covered by tests
chip.style.color = opts.color ?? '#FFF';
chip.style.position = opts.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';
chip.title = opts.name;
chip.addEventListener('click', () => {

Check warning on line 149 in packages/@o3r/components/src/devkit/highlight/helpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/highlight/helpers.ts#L141-L149

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

Check warning on line 151 in packages/@o3r/components/src/devkit/highlight/helpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/highlight/helpers.ts#L151

Added line #L151 was not covered by tests
});
return chip;

Check warning on line 153 in packages/@o3r/components/src/devkit/highlight/helpers.ts

View check run for this annotation

Codecov / codecov/patch

packages/@o3r/components/src/devkit/highlight/helpers.ts#L153

Added line #L153 was not covered by tests
}
Loading
Loading