Skip to content

Commit 5e2bf4a

Browse files
feat(devexp): highlight components by group
1 parent f4e354c commit 5e2bf4a

File tree

6 files changed

+450
-0
lines changed

6 files changed

+450
-0
lines changed

packages/@o3r/components/src/devkit/components-devkit.interface.ts

+41
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import type {
88
import type {
99
PlaceholderMode,
1010
} from '../stores';
11+
import {
12+
GroupInfo,
13+
} from './highlight/models';
1114
import {
1215
OtterLikeComponentInfo,
1316
} from './inspector';
@@ -32,6 +35,40 @@ export interface ToggleInspectorMessage extends OtterMessageContent<'toggleInspe
3235
isRunning: boolean;
3336
}
3437

38+
/**
39+
* Message to toggle the highlight
40+
*/
41+
export interface ToggleHighlightMessage extends OtterMessageContent<'toggleHighlight'> {
42+
/** Is the highlight displayed */
43+
isRunning: boolean;
44+
}
45+
46+
/**
47+
* Message the change the configuration of the `HighlightService`
48+
*/
49+
export interface ChangeHighlightConfiguration extends OtterMessageContent<'changeHighlightConfiguration'> {
50+
/**
51+
* Minimum width of HTMLElement to be considered
52+
*/
53+
elementMinWidth?: number;
54+
/**
55+
* Minimum height of HTMLElement to be considered
56+
*/
57+
elementMinHeight?: number;
58+
/**
59+
* Throttle interval
60+
*/
61+
throttleInterval?: number;
62+
/**
63+
* Group information to detect elements
64+
*/
65+
groupsInfo?: Record<string, GroupInfo>;
66+
/**
67+
* Maximum number of ancestors
68+
*/
69+
maxDepth?: number;
70+
}
71+
3572
/**
3673
* Message to toggle the placeholder mode
3774
*/
@@ -51,6 +88,8 @@ type ComponentsMessageContents =
5188
| IsComponentSelectionAvailableMessage
5289
| SelectedComponentInfoMessage
5390
| ToggleInspectorMessage
91+
| ToggleHighlightMessage
92+
| ChangeHighlightConfiguration
5493
| PlaceholderModeMessage;
5594

5695
/** List of possible DataTypes for Components messages */
@@ -74,5 +113,7 @@ export const isComponentsMessage = (message: any): message is AvailableComponent
74113
|| message.dataType === 'isComponentSelectionAvailable'
75114
|| message.dataType === 'placeholderMode'
76115
|| message.dataType === 'toggleInspector'
116+
|| message.dataType === 'toggleHighlight'
117+
|| message.dataType === 'changeHighlightConfiguration'
77118
);
78119
};

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

+36
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ import {
4040
OTTER_COMPONENTS_DEVTOOLS_DEFAULT_OPTIONS,
4141
OTTER_COMPONENTS_DEVTOOLS_OPTIONS,
4242
} from './components-devtools.token';
43+
import {
44+
HighlightService,
45+
} from './highlight/highlight.service';
4346
import {
4447
OtterInspectorService,
4548
OtterLikeComponentInfo,
@@ -51,6 +54,7 @@ import {
5154
export class ComponentsDevtoolsMessageService implements DevtoolsServiceInterface {
5255
private readonly options: ComponentsDevtoolsServiceOptions;
5356
private readonly inspectorService: OtterInspectorService;
57+
private readonly highlightService: HighlightService;
5458
private readonly sendMessage = sendOtterMessage<AvailableComponentsMessageContents>;
5559
private readonly destroyRef = inject(DestroyRef);
5660

@@ -65,6 +69,8 @@ export class ComponentsDevtoolsMessageService implements DevtoolsServiceInterfac
6569
};
6670

6771
this.inspectorService = new OtterInspectorService();
72+
this.highlightService = new HighlightService();
73+
6874
if (this.options.isActivatedOnBootstrap) {
6975
this.activate();
7076
}
@@ -130,6 +136,36 @@ export class ComponentsDevtoolsMessageService implements DevtoolsServiceInterfac
130136
this.inspectorService.toggleInspector(message.isRunning);
131137
break;
132138
}
139+
case 'toggleHighlight': {
140+
if (message.isRunning) {
141+
this.highlightService.start();
142+
} else {
143+
this.highlightService.stop();
144+
}
145+
break;
146+
}
147+
case 'changeHighlightConfiguration': {
148+
if (message.elementMinWidth) {
149+
this.highlightService.elementMinWidth = message.elementMinWidth;
150+
}
151+
if (message.elementMinHeight) {
152+
this.highlightService.elementMinHeight = message.elementMinHeight;
153+
}
154+
if (message.throttleInterval) {
155+
this.highlightService.throttleInterval = message.throttleInterval;
156+
}
157+
if (message.groupsInfo) {
158+
this.highlightService.groupsInfo = message.groupsInfo;
159+
}
160+
if (message.maxDepth) {
161+
this.highlightService.maxDepth = message.maxDepth;
162+
}
163+
if (this.highlightService.isRunning()) {
164+
// Re-start to recompute the highlight with the new configuration
165+
this.highlightService.start();
166+
}
167+
break;
168+
}
133169
case 'placeholderMode': {
134170
this.store.dispatch(togglePlaceholderModeTemplate({ mode: message.mode }));
135171
break;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Class applied on the wrapper of highlight elements
3+
*/
4+
export const HIGHLIGHT_WRAPPER_CLASS = 'highlight-wrapper';
5+
6+
/**
7+
* Class applied on the overlay elements
8+
*/
9+
export const HIGHLIGHT_OVERLAY_CLASS = 'highlight-overlay';
10+
11+
/**
12+
* Class applied on the chip elements
13+
*/
14+
export const HIGHLIGHT_CHIP_CLASS = 'highlight-chip';
15+
16+
/**
17+
* Default value for maximum number of ancestors
18+
*/
19+
export const DEFAULT_MAX_DEPTH = 10;
20+
21+
/**
22+
* Default value for element min height
23+
*/
24+
export const DEFAULT_ELEMENT_MIN_HEIGHT = 30;
25+
/**
26+
* Default value for element min width
27+
*/
28+
export const DEFAULT_ELEMENT_MIN_WIDTH = 60;
29+
/**
30+
* Default value for throttle interval
31+
*/
32+
export const DEFAULT_THROTTLE_INTERVAL = 500;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import {
2+
HIGHLIGHT_WRAPPER_CLASS,
3+
} from './constants';
4+
import {
5+
ElementWithGroupInfo,
6+
} from './models';
7+
8+
/**
9+
* Retrieve the identifier of the element
10+
* @param element
11+
*/
12+
export function getIdentifier(element: ElementWithGroupInfo): string {
13+
const { tagName, attributes, classList } = element.htmlElement;
14+
const regexp = new RegExp(element.regexp, 'i');
15+
if (!regexp.test(tagName)) {
16+
const attribute = Array.from(attributes).find((att) => regexp.test(att.name));
17+
if (attribute) {
18+
return `${attribute.name}${attribute.value ? `="${attribute.value}"` : ''}`;
19+
}
20+
const className = Array.from(classList).find((cName) => regexp.test(cName));
21+
if (className) {
22+
return className;
23+
}
24+
}
25+
return tagName;
26+
}
27+
28+
/**
29+
* Compute the number of ancestors of a given element based on a list of elements
30+
* @param element
31+
* @param elementList
32+
*/
33+
export function computeNumberOfAncestors(element: HTMLElement, elementList: HTMLElement[]) {
34+
return elementList.filter((el: HTMLElement) => el.contains(element)).length;
35+
}
36+
37+
/**
38+
* Throttle {@link fn} with a {@link delay}
39+
* @param fn method to run
40+
* @param delay given in ms
41+
*/
42+
export function throttle<T extends (...args: any[]) => any>(fn: T, delay: number): (...args: Parameters<T>) => void {
43+
let timerFlag: ReturnType<typeof setTimeout> | null = null;
44+
45+
const throttleFn = (...args: Parameters<T>) => {
46+
if (timerFlag === null) {
47+
fn(...args);
48+
timerFlag = setTimeout(() => {
49+
fn(...args);
50+
timerFlag = null;
51+
}, delay);
52+
}
53+
};
54+
return throttleFn;
55+
}
56+
57+
/**
58+
* Run {@link refreshFn} if {@link mutations} implies to refresh elements inside {@link highlightWrapper}
59+
* @param mutations
60+
* @param highlightWrapper
61+
* @param refreshFn
62+
*/
63+
export function runRefreshIfNeeded(mutations: MutationRecord[], highlightWrapper: Element | null, refreshFn: () => void) {
64+
if (
65+
mutations.some((mutation) =>
66+
mutation.target !== highlightWrapper
67+
|| (
68+
mutation.target === document.body
69+
&& Array.from<HTMLElement>(mutation.addedNodes.values() as any)
70+
.concat(...mutation.removedNodes.values() as any)
71+
.some((node) => !node.classList.contains(HIGHLIGHT_WRAPPER_CLASS))
72+
)
73+
)
74+
) {
75+
refreshFn();
76+
}
77+
}

0 commit comments

Comments
 (0)