mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(devtools): Implement Component/Directive Search (rangle/angular-devtools#25)
This commit is contained in:
committed by
GitHub
parent
6aa03cf63e
commit
da690ca84a
-15
@@ -1,15 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
import { ComponentExplorerComponent } from './component-explorer.component';
|
||||
import { ComponentTreeComponent } from './component-tree/component-tree.component';
|
||||
import { PropertyViewModule } from './property-view/property-view.module';
|
||||
import { MatTreeModule } from '@angular/material/tree';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ComponentExplorerComponent, ComponentTreeComponent],
|
||||
exports: [ComponentExplorerComponent],
|
||||
imports: [MatTreeModule, MatIconModule, CommonModule, PropertyViewModule],
|
||||
})
|
||||
export class ComponentExplorerModule {}
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
:host /deep/ .mat-tree-node .mat-icon {
|
||||
font-size: 12px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
:host /deep/ .mat-tree-node {
|
||||
min-height: 20px !important;
|
||||
cursor: default;
|
||||
font-family: Menlo, monospace;
|
||||
border-radius: 5px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
:host /deep/ .mat-tree-node,
|
||||
.mat-nested-tree-node {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
:host /deep/ mat-tree-node > button {
|
||||
outline: none;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
:host /deep/ mat-tree-node:hover {
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
|
||||
:host /deep/ mat-tree-node.selected {
|
||||
background-color: #dadada;
|
||||
}
|
||||
|
||||
.dir-names {
|
||||
opacity: 0.4;
|
||||
}
|
||||
-133
@@ -1,133 +0,0 @@
|
||||
import { Component, Input, EventEmitter, HostListener, Output, ViewChild, ElementRef } from '@angular/core';
|
||||
import { Node } from 'protocol';
|
||||
import { FlatTreeControl, CdkTree } from '@angular/cdk/tree';
|
||||
import { FlatNode, ComponentDataSource } from './component-data-source';
|
||||
|
||||
import scrollIntoViewIfNeeded from 'scroll-into-view-if-needed';
|
||||
import { isChildOf, parentCollapsed } from './component-tree-utils';
|
||||
|
||||
@Component({
|
||||
selector: 'ng-component-tree',
|
||||
templateUrl: './component-tree.component.html',
|
||||
styleUrls: ['./component-tree.component.css'],
|
||||
})
|
||||
export class ComponentTreeComponent {
|
||||
@Input() set forest(forest: Node[]) {
|
||||
this.dataSource.update(forest);
|
||||
if (!this._initialized && forest && forest.length) {
|
||||
this.treeControl.expandAll();
|
||||
this._initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Output() selectNode = new EventEmitter();
|
||||
|
||||
@ViewChild(CdkTree) tree: CdkTree<any>;
|
||||
|
||||
|
||||
selectedNode: FlatNode | null = null;
|
||||
treeControl = new FlatTreeControl<FlatNode>(
|
||||
node => node.level,
|
||||
node => node.expandable
|
||||
);
|
||||
dataSource = new ComponentDataSource(this.treeControl);
|
||||
|
||||
private _initialized = false;
|
||||
|
||||
hasChild = (_: number, node: FlatNode) => node.expandable;
|
||||
|
||||
constructor(private _host: ElementRef) {}
|
||||
|
||||
select(node: FlatNode): void {
|
||||
this.selectNode.emit(node.original);
|
||||
this.selectedNode = node;
|
||||
|
||||
// We wait for the CD to run.
|
||||
setTimeout(() => {
|
||||
const el = this._host.nativeElement.querySelector('.selected');
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
scrollIntoViewIfNeeded(el, {
|
||||
scrollMode: 'if-needed',
|
||||
block: 'nearest',
|
||||
inline: 'nearest',
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.ArrowUp', ['$event'])
|
||||
navigateUp(evnt: KeyboardEvent): void {
|
||||
if (!this.selectedNode) {
|
||||
return;
|
||||
}
|
||||
evnt.preventDefault();
|
||||
const data = this.dataSource.data;
|
||||
let prevIdx = data.findIndex(e => e.id === this.selectedNode.id) - 1;
|
||||
if (prevIdx < 0) {
|
||||
return;
|
||||
}
|
||||
let prevNode = data[prevIdx];
|
||||
const currentNode = data[prevIdx + 1];
|
||||
if (prevNode.id.length <= currentNode.id.length) {
|
||||
return this.select(data[prevIdx]);
|
||||
}
|
||||
while (prevIdx >= 0 && parentCollapsed(prevIdx, data, this.treeControl)) {
|
||||
prevIdx--;
|
||||
prevNode = data[prevIdx];
|
||||
}
|
||||
this.select(data[prevIdx]);
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.ArrowDown', ['$event'])
|
||||
navigateDown(evnt: KeyboardEvent): void {
|
||||
if (!this.selectedNode) {
|
||||
return;
|
||||
}
|
||||
const data = this.dataSource.data;
|
||||
let idx = data.findIndex(e => e.id === this.selectedNode.id);
|
||||
const currentNode = data[idx];
|
||||
if (!this.treeControl.isExpanded(currentNode) && currentNode.expandable) {
|
||||
for (let i = idx + 1; i < data.length; i++) {
|
||||
const node = data[i];
|
||||
if (!isChildOf(node.id, currentNode.id)) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
idx++;
|
||||
}
|
||||
if (idx >= data.length) {
|
||||
return;
|
||||
}
|
||||
this.select(data[idx]);
|
||||
evnt.preventDefault();
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.ArrowLeft', ['$event'])
|
||||
collapseCurrent(evnt: KeyboardEvent): void {
|
||||
if (!this.selectedNode) {
|
||||
return;
|
||||
}
|
||||
this.treeControl.collapse(this.selectedNode);
|
||||
evnt.preventDefault();
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.ArrowRight', ['$event'])
|
||||
expandCurrent(evnt: KeyboardEvent): void {
|
||||
if (!this.selectedNode) {
|
||||
return;
|
||||
}
|
||||
this.treeControl.expand(this.selectedNode);
|
||||
evnt.preventDefault();
|
||||
}
|
||||
|
||||
isSelected(node: FlatNode): boolean {
|
||||
if (!this.selectedNode) {
|
||||
return false;
|
||||
}
|
||||
return this.selectedNode.id.join(',') === node.id.join(',');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<mat-tab-group animationDuration="0ms">
|
||||
<mat-tab label="Components">
|
||||
<ng-component-explorer [messageBus]="messageBus"></ng-component-explorer>
|
||||
<ng-directive-explorer [messageBus]="messageBus"></ng-directive-explorer>
|
||||
</mat-tab>
|
||||
<mat-tab label="Profiler">
|
||||
<ng-profiler [messageBus]="messageBus"></ng-profiler>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, Input, ViewChild } from '@angular/core';
|
||||
import { Events, MessageBus } from 'protocol';
|
||||
import { MatTabGroup } from '@angular/material/tabs';
|
||||
import { ComponentExplorerComponent } from './component-explorer/component-explorer.component';
|
||||
import { DirectiveExplorerComponent } from './directive-explorer/directive-explorer.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ng-devtools-tabs',
|
||||
@@ -12,7 +12,7 @@ export class DevToolsTabsComponent {
|
||||
@Input() messageBus: MessageBus<Events>;
|
||||
@Input() angularVersion: string | undefined = undefined;
|
||||
@ViewChild(MatTabGroup) tabGroup: MatTabGroup;
|
||||
@ViewChild(ComponentExplorerComponent) componentExplorer: ComponentExplorerComponent;
|
||||
@ViewChild(DirectiveExplorerComponent) componentExplorer: DirectiveExplorerComponent;
|
||||
|
||||
inspectorRunning = false;
|
||||
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { DevToolsTabsComponent } from './devtools-tabs.component';
|
||||
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { ComponentExplorerModule } from './component-explorer/component-explorer.module';
|
||||
import { ProfilerModule } from './component-explorer/profiler/profiler.module';
|
||||
import { DirectiveExplorerModule } from './directive-explorer/directive-explorer.module';
|
||||
import { ProfilerModule } from './directive-explorer/profiler/profiler.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [DevToolsTabsComponent],
|
||||
imports: [MatTabsModule, ComponentExplorerModule, ProfilerModule, CommonModule],
|
||||
imports: [MatTabsModule, DirectiveExplorerModule, ProfilerModule, CommonModule],
|
||||
exports: [DevToolsTabsComponent]
|
||||
})
|
||||
export class DevToolsTabModule {}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<ng-component-tree (selectNode)="handleNodeSelection($event)" [forest]="forest"> </ng-component-tree>
|
||||
<ng-directive-forest (selectNode)="handleNodeSelection($event)" [forest]="forest"></ng-directive-forest>
|
||||
<div *ngIf="currentSelectedElement" class="property-wrapper">
|
||||
<div *ngFor="let data of directivesData | keyvalue; trackBy: nameTracking" class="explorer-panel">
|
||||
<header>Properties of {{ data.key }}</header>
|
||||
+5
-5
@@ -8,15 +8,15 @@ import {
|
||||
ComponentExplorerViewQuery,
|
||||
ComponentExplorerView, ComponentExplorerViewProperties,
|
||||
} from 'protocol';
|
||||
import { IndexedNode } from './component-tree/index-forest';
|
||||
import { IndexedNode } from './directive-forest/index-forest';
|
||||
import { PropertyViewComponent } from './property-view/property-view.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ng-component-explorer',
|
||||
templateUrl: './component-explorer.component.html',
|
||||
styleUrls: ['./component-explorer.component.css'],
|
||||
selector: 'ng-directive-explorer',
|
||||
templateUrl: './directive-explorer.component.html',
|
||||
styleUrls: ['./directive-explorer.component.css'],
|
||||
})
|
||||
export class ComponentExplorerComponent implements OnInit {
|
||||
export class DirectiveExplorerComponent implements OnInit {
|
||||
@Input() messageBus: MessageBus<Events>;
|
||||
|
||||
@ViewChildren(PropertyViewComponent) propertyViews: QueryList<PropertyViewComponent>;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
import { DirectiveExplorerComponent } from './directive-explorer.component';
|
||||
import { DirectiveForestComponent } from './directive-forest/directive-forest.component';
|
||||
import { FilterComponent } from './directive-forest/filter/filter.component';
|
||||
import { PropertyViewModule } from './property-view/property-view.module';
|
||||
import { MatTreeModule } from '@angular/material/tree';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
|
||||
@NgModule({
|
||||
declarations: [DirectiveExplorerComponent, DirectiveForestComponent, FilterComponent],
|
||||
exports: [DirectiveExplorerComponent],
|
||||
imports: [MatTreeModule, MatIconModule, CommonModule, PropertyViewModule, MatButtonModule],
|
||||
})
|
||||
export class DirectiveExplorerModule {}
|
||||
+4
@@ -65,6 +65,10 @@ export class ComponentDataSource extends DataSource<FlatNode> {
|
||||
return this._data;
|
||||
}
|
||||
|
||||
get expandedDataValues() {
|
||||
return this._expandedData.value;
|
||||
}
|
||||
|
||||
update(forest: Node[]): void {
|
||||
if (!forest) {
|
||||
return;
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
:host /deep/ .mat-tree-node .mat-icon {
|
||||
font-size: 12px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
:host /deep/ .mat-tree-node {
|
||||
min-height: 20px !important;
|
||||
cursor: default;
|
||||
font-family: Menlo, monospace;
|
||||
border-radius: 5px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
:host /deep/ .mat-tree-node,
|
||||
.mat-nested-tree-node {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
:host /deep/ mat-tree-node > button {
|
||||
outline: none;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
:host /deep/ mat-tree-node:hover {
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
|
||||
:host /deep/ mat-tree-node.selected {
|
||||
background-color: #dadada;
|
||||
}
|
||||
|
||||
:host /deep/ mat-tree-node.matched {
|
||||
background-color: #b298e4;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:host /deep/ mat-tree-node.matched:hover {
|
||||
background-color: #8463be;
|
||||
}
|
||||
|
||||
:host /deep/ mat-tree-node.matched.selected {
|
||||
background-color: #6f56bb;
|
||||
color: #fcd736;
|
||||
}
|
||||
|
||||
:host /deep/ mat-tree-node.matched .dir-names {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
:host /deep/ mat-tree-node {
|
||||
transition: background-color 100ms;
|
||||
}
|
||||
|
||||
.dir-names {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: flex
|
||||
}
|
||||
|
||||
.filter .icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: auto 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.filter .filter-input {
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.003);
|
||||
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
line-height: 1.4em;
|
||||
padding: 12px 6px;
|
||||
}
|
||||
|
||||
.up-down-buttons {
|
||||
width: 20%;
|
||||
display: flex;
|
||||
}
|
||||
+15
-6
@@ -1,23 +1,32 @@
|
||||
<ng-filter
|
||||
[hasMatched]="hasMatched()"
|
||||
(filter)="handleFilter($event)"
|
||||
(nextMatched)="nextMatched()"
|
||||
(prevMatched)="prevMatched()"
|
||||
>
|
||||
</ng-filter>
|
||||
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
|
||||
<mat-tree-node
|
||||
[class.selected]="isSelected(node)"
|
||||
(click)="select(node)"
|
||||
[ngClass]="{ matched: isMatched(node), selected: isSelected(node) }"
|
||||
(click)="handleSelect(node)"
|
||||
*matTreeNodeDef="let node"
|
||||
matTreeNodePaddingIndent="20"
|
||||
matTreeNodePadding
|
||||
[@simpleFade]="node.level?1:0"
|
||||
>
|
||||
<button mat-icon-button disabled></button>
|
||||
<button disabled></button>
|
||||
<span class="element-name">{{ node.name }}</span>
|
||||
<span *ngIf="node.directives" class="dir-names">[{{ node.directives }}]</span>
|
||||
</mat-tree-node>
|
||||
<mat-tree-node
|
||||
[class.selected]="isSelected(node)"
|
||||
(click)="select(node)"
|
||||
[ngClass]="{ matched: isMatched(node), selected: isSelected(node) }"
|
||||
(click)="handleSelect(node)"
|
||||
*matTreeNodeDef="let node; when: hasChild"
|
||||
matTreeNodePaddingIndent="20"
|
||||
matTreeNodePadding
|
||||
[@simpleFade]="node.level?1:0"
|
||||
>
|
||||
<button mat-icon-button matTreeNodeToggle [attr.aria-label]="'toggle ' + node.name">
|
||||
<button matTreeNodeToggle [attr.aria-label]="'toggle ' + node.name">
|
||||
<mat-icon class="mat-icon-rtl-mirror">
|
||||
{{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}
|
||||
</mat-icon>
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Input,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { Node } from 'protocol';
|
||||
import { CdkTree, FlatTreeControl } from '@angular/cdk/tree';
|
||||
import { ComponentDataSource, FlatNode } from './component-data-source';
|
||||
|
||||
import scrollIntoViewIfNeeded from 'scroll-into-view-if-needed';
|
||||
import { isChildOf, parentCollapsed } from './directive-forest-utils';
|
||||
import { animate, style, transition, trigger } from '@angular/animations';
|
||||
import { FilterComponent } from './filter/filter.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ng-directive-forest',
|
||||
templateUrl: './directive-forest.component.html',
|
||||
styleUrls: ['./directive-forest.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [
|
||||
trigger('simpleFade', [
|
||||
transition(':enter', [
|
||||
style({ opacity: 0 }),
|
||||
animate(75, style({ opacity: 1 }))
|
||||
]),
|
||||
transition(':leave', [
|
||||
animate(75, style({ opacity: 0 }))
|
||||
])])]
|
||||
})
|
||||
export class DirectiveForestComponent {
|
||||
@Input() set forest(forest: Node[]) {
|
||||
this.dataSource.update(forest);
|
||||
if (!this._initialized && forest && forest.length) {
|
||||
this.treeControl.expandAll();
|
||||
this._initialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Output() selectNode = new EventEmitter();
|
||||
|
||||
@ViewChild(CdkTree) tree: CdkTree<any>;
|
||||
|
||||
@ViewChild(FilterComponent) filterComponent;
|
||||
|
||||
filterRegex = new RegExp('.^');
|
||||
currentlyMatchedIndex = -1;
|
||||
|
||||
selectedNode: FlatNode | null = null;
|
||||
treeControl = new FlatTreeControl<FlatNode>(
|
||||
node => node.level,
|
||||
node => node.expandable
|
||||
);
|
||||
dataSource = new ComponentDataSource(this.treeControl);
|
||||
|
||||
private _initialized = false;
|
||||
|
||||
hasChild = (_: number, node: FlatNode) => node.expandable;
|
||||
|
||||
constructor(private _host: ElementRef) {}
|
||||
|
||||
handleSelect(node: FlatNode): void {
|
||||
const matchedTree: FlatNode[] = this._findMatchedNodes();
|
||||
this.currentlyMatchedIndex = matchedTree.findIndex(matchedNode => matchedNode.id === node.id);
|
||||
this.select(node);
|
||||
}
|
||||
|
||||
select(node: FlatNode): void {
|
||||
this.selectNode.emit(node.original);
|
||||
this.selectedNode = node;
|
||||
|
||||
// We wait for the CD to run.
|
||||
setTimeout(() => {
|
||||
const el = this._host.nativeElement.querySelector('.selected');
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
scrollIntoViewIfNeeded(el, {
|
||||
scrollMode: 'if-needed',
|
||||
block: 'nearest',
|
||||
inline: 'nearest',
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.ArrowUp', ['$event'])
|
||||
navigateUp(evnt: KeyboardEvent): void {
|
||||
if (this.invalidArrowEvent(evnt)) {
|
||||
return;
|
||||
}
|
||||
evnt.preventDefault();
|
||||
const data = this.dataSource.data;
|
||||
let prevIdx = data.findIndex(e => e.id === this.selectedNode.id) - 1;
|
||||
if (prevIdx < 0) {
|
||||
return;
|
||||
}
|
||||
let prevNode = data[prevIdx];
|
||||
const currentNode = data[prevIdx + 1];
|
||||
if (prevNode.id.length <= currentNode.id.length) {
|
||||
return this.select(data[prevIdx]);
|
||||
}
|
||||
while (prevIdx >= 0 && parentCollapsed(prevIdx, data, this.treeControl)) {
|
||||
prevIdx--;
|
||||
prevNode = data[prevIdx];
|
||||
}
|
||||
this.select(data[prevIdx]);
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.ArrowDown', ['$event'])
|
||||
navigateDown(evnt: KeyboardEvent): void {
|
||||
if (this.invalidArrowEvent(evnt)) {
|
||||
return;
|
||||
}
|
||||
const data = this.dataSource.data;
|
||||
let idx = data.findIndex(e => e.id === this.selectedNode.id);
|
||||
const currentNode = data[idx];
|
||||
if (!this.treeControl.isExpanded(currentNode) && currentNode.expandable) {
|
||||
for (let i = idx + 1; i < data.length; i++) {
|
||||
const node = data[i];
|
||||
if (!isChildOf(node.id, currentNode.id)) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
idx++;
|
||||
}
|
||||
if (idx >= data.length) {
|
||||
return;
|
||||
}
|
||||
this.select(data[idx]);
|
||||
evnt.preventDefault();
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.ArrowLeft', ['$event'])
|
||||
collapseCurrent(evnt: KeyboardEvent): void {
|
||||
if (this.invalidArrowEvent(evnt)) {
|
||||
return;
|
||||
}
|
||||
this.treeControl.collapse(this.selectedNode);
|
||||
evnt.preventDefault();
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.ArrowRight', ['$event'])
|
||||
expandCurrent(evnt: KeyboardEvent): void {
|
||||
if (this.invalidArrowEvent(evnt)) {
|
||||
return;
|
||||
}
|
||||
this.treeControl.expand(this.selectedNode);
|
||||
evnt.preventDefault();
|
||||
}
|
||||
|
||||
invalidArrowEvent(event: KeyboardEvent): boolean {
|
||||
return event.target === this.filterComponent.filterElement.nativeElement || !this.selectedNode;
|
||||
}
|
||||
|
||||
isSelected(node: FlatNode): boolean {
|
||||
return !!this.selectedNode && this.selectedNode.id.join(',') === node.id.join(',');
|
||||
}
|
||||
|
||||
isMatched(node: FlatNode): boolean {
|
||||
return this.filterRegex.test(node.name.toLowerCase()) || this.filterRegex.test(node.directives.toLowerCase());
|
||||
}
|
||||
|
||||
handleFilter(filterText: string): void {
|
||||
this.resetCurrentlyMatchedIndex();
|
||||
this.setFilterRegex(filterText);
|
||||
}
|
||||
|
||||
resetCurrentlyMatchedIndex(): void {
|
||||
this.currentlyMatchedIndex = -1;
|
||||
}
|
||||
|
||||
setFilterRegex(filterText: string): void {
|
||||
try {
|
||||
this.filterRegex = new RegExp(filterText.toLowerCase() || '.^');
|
||||
} catch {
|
||||
this.filterRegex = new RegExp('.^');
|
||||
}
|
||||
}
|
||||
|
||||
_findMatchedNodes(): FlatNode[] {
|
||||
return this.dataSource.data.filter(node => this.isMatched(node));
|
||||
}
|
||||
|
||||
hasMatched(): boolean {
|
||||
return this._findMatchedNodes().length > 0;
|
||||
}
|
||||
|
||||
nextMatched(): void {
|
||||
const matchedTree: FlatNode[] = this._findMatchedNodes();
|
||||
this.currentlyMatchedIndex = (this.currentlyMatchedIndex + 1) % matchedTree.length;
|
||||
const nodeToSelect = matchedTree[this.currentlyMatchedIndex];
|
||||
const nodeIsVisible = this.dataSource.expandedDataValues.find((node) => node === nodeToSelect);
|
||||
if (!nodeIsVisible) {
|
||||
this.expandParents(nodeToSelect);
|
||||
}
|
||||
if (nodeToSelect) {
|
||||
this.select(nodeToSelect);
|
||||
}
|
||||
}
|
||||
|
||||
prevMatched(): void {
|
||||
const matchedTree: FlatNode[] = this._findMatchedNodes();
|
||||
this.currentlyMatchedIndex = (this.currentlyMatchedIndex - 1 + matchedTree.length) % matchedTree.length;
|
||||
const nodeToSelect = matchedTree[this.currentlyMatchedIndex];
|
||||
const nodeIsVisible = this.dataSource.expandedDataValues.find((node) => node === nodeToSelect);
|
||||
if (!nodeIsVisible) {
|
||||
this.expandParents(nodeToSelect);
|
||||
}
|
||||
if (nodeToSelect) {
|
||||
this.select(nodeToSelect);
|
||||
}
|
||||
}
|
||||
|
||||
expandParents(nodeToExpand: FlatNode): void {
|
||||
if (nodeToExpand) {
|
||||
// Todo: implement optimized array equals method
|
||||
const parentNode = this.dataSource.data.find(node =>
|
||||
node.id.toString() === nodeToExpand.id.slice(0, nodeToExpand.id.length - 1).toString());
|
||||
this.treeControl.expand(parentNode);
|
||||
this.expandParents(parentNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
.filter {
|
||||
display: flex
|
||||
}
|
||||
|
||||
.filter .icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: auto 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.filter .filter-input {
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.003);
|
||||
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
|
||||
width: 100%;
|
||||
font-size: 20px;
|
||||
font-family: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
line-height: 1.4em;
|
||||
padding: 12px 6px;
|
||||
}
|
||||
|
||||
.up-down-buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.up-down-buttons button {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
:host /deep/ .up-down-buttons button mat-icon {
|
||||
opacity: 0.5;
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<div class="filter">
|
||||
<svg class="icon" viewBox="0 0 24 24">
|
||||
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
|
||||
<path d="M0 0h24v24H0z" fill="none"/>
|
||||
</svg>
|
||||
<input #filterInput (input)="emitFilter($event)" (keydown.enter)="emitNextMatched()" (keydown.shift.enter)="emitPrevMatched()" class="filter-input" placeholder="Filter components" autofocus>
|
||||
<div class="up-down-buttons" *ngIf="hasMatched">
|
||||
<button mat-icon-button (click)="emitPrevMatched()">
|
||||
<mat-icon class="mat-icon-rtl-mirror">
|
||||
expand_less
|
||||
</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button (click)="emitNextMatched()">
|
||||
<mat-icon class="">
|
||||
expand_more
|
||||
</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ng-filter',
|
||||
templateUrl: './filter.component.html',
|
||||
styleUrls: ['./filter.component.css']
|
||||
})
|
||||
export class FilterComponent {
|
||||
@Output() filter: EventEmitter<string> = new EventEmitter<string>();
|
||||
@Output() nextMatched: EventEmitter<void> = new EventEmitter();
|
||||
@Output() prevMatched: EventEmitter<void> = new EventEmitter();
|
||||
|
||||
@Input() hasMatched = false;
|
||||
|
||||
@ViewChild('filterInput') filterElement: ElementRef;
|
||||
|
||||
emitFilter(event: InputEvent) {
|
||||
this.filter.emit((event.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
emitNextMatched() {
|
||||
this.nextMatched.emit();
|
||||
}
|
||||
|
||||
emitPrevMatched() {
|
||||
this.prevMatched.emit();
|
||||
}
|
||||
}
|
||||
-1
@@ -28,7 +28,6 @@ export class TimelineComponent {
|
||||
currentView = 1;
|
||||
|
||||
get recordsView(): AppEntry {
|
||||
console.log('hit');
|
||||
if (this.view === 'timeline') {
|
||||
// null coalesce to aggregated if no data was recorded since aggregated will be empty, whereas
|
||||
// timeline will not even exist
|
||||
@@ -6,3 +6,31 @@
|
||||
.noselect {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.initializing {
|
||||
margin: auto;
|
||||
width: 125px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.initializing .loading {
|
||||
background: url("./images/angular.svg");
|
||||
border-radius: 50%;
|
||||
width: 125px;
|
||||
height: 125px;
|
||||
float: left;
|
||||
text-align: center;
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse
|
||||
{
|
||||
0% {
|
||||
box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 35px rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<ng-container *ngIf="angularExists === true; else errorOrLoad">
|
||||
<div class="devtools-wrapper noselect">
|
||||
<div class="devtools-wrapper noselect" [@enterAnimation]>
|
||||
<ng-devtools-tabs [messageBus]="messageBus" [angularVersion]="angularVersion"></ng-devtools-tabs>
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -10,8 +10,9 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #load>
|
||||
<div *ngIf="angularExists === null">
|
||||
Initializing...
|
||||
<div class="initializing" *ngIf="angularExists === null">
|
||||
<div class="loading">
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
|
||||
import { MessageBus, Events } from 'protocol';
|
||||
import { interval } from 'rxjs';
|
||||
import { animate, style, transition, trigger } from '@angular/animations';
|
||||
|
||||
@Component({
|
||||
selector: 'ng-devtools',
|
||||
templateUrl: './devtools.component.html',
|
||||
styleUrls: ['./devtools.component.css'],
|
||||
animations: [
|
||||
trigger(
|
||||
'enterAnimation', [
|
||||
transition(':enter', [
|
||||
style({ opacity: 0 }),
|
||||
animate('200ms', style({ opacity: 1 }))
|
||||
]),
|
||||
transition(':leave', [
|
||||
style({ opacity: 1 }),
|
||||
animate('200ms', style({ opacity: 0 }))
|
||||
])
|
||||
]
|
||||
)
|
||||
]
|
||||
})
|
||||
export class DevToolsComponent implements OnInit, OnDestroy {
|
||||
angularExists: boolean | null = null;
|
||||
|
||||
@@ -3,10 +3,11 @@ import { CommonModule } from '@angular/common';
|
||||
|
||||
import { DevToolsComponent } from './devtools.component';
|
||||
import { DevToolsTabModule } from './devtools-tabs/devtools-tabs.module';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
|
||||
@NgModule({
|
||||
declarations: [DevToolsComponent],
|
||||
imports: [CommonModule, DevToolsTabModule],
|
||||
imports: [CommonModule, DevToolsTabModule, MatProgressSpinnerModule],
|
||||
exports: [DevToolsComponent],
|
||||
})
|
||||
export class DevToolsModule {}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 250 250" style="enable-background:new 0 0 250 250;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#DD0031;}
|
||||
.st1{fill:#C3002F;}
|
||||
.st2{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g>
|
||||
<polygon class="st0" points="125,30 125,30 125,30 31.9,63.2 46.1,186.3 125,230 125,230 125,230 203.9,186.3 218.1,63.2 "/>
|
||||
<polygon class="st1" points="125,30 125,52.2 125,52.1 125,153.4 125,153.4 125,230 125,230 203.9,186.3 218.1,63.2 125,30 "/>
|
||||
<path class="st2" d="M125,52.1L66.8,182.6h0h21.7h0l11.7-29.2h49.4l11.7,29.2h0h21.7h0L125,52.1L125,52.1L125,52.1L125,52.1
|
||||
L125,52.1z M142,135.4H108l17-40.9L142,135.4z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 883 B |
Reference in New Issue
Block a user