Commit 26b56ba6 authored by Camillo Bruni's avatar Camillo Bruni Committed by V8 LUCI CQ

[tools][system-analyzer] Fix linked events

- Open and focus separate views for each log entry
- Map.prototype.parent is now a getter
- Fix SharedLibLogEntry tooltips
- Store codeEntry in IcLogEntry for linking back to code objects
- New property-link-table which is used in tooltip and code-panel
- Ignore right-click events in the timeline-tracks

Bug: v8:10644, v8:11835
Change-Id: Id2fe5002b776adf362b1580b96082c84790a6ef0
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2960804Reviewed-by: 's avatarPatrick Thier <pthier@chromium.org>
Commit-Queue: Camillo Bruni <cbruni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#75150}
parent 68a43849
......@@ -36,6 +36,7 @@ export class SourcePosition {
this.line = line;
this.column = column;
this.entries = [];
this.isFunction = false;
}
addEntry(entry) {
......@@ -45,6 +46,20 @@ export class SourcePosition {
toString() {
return `${this.script.name}:${this.line}:${this.column}`;
}
get functionPosition() {
// TODO(cbruni)
return undefined;
}
get toolTipDict() {
return {
title: this.toString(),
__this__: this,
script: this.script,
entries: this.entries.length,
}
}
}
export class Script {
......@@ -74,6 +89,11 @@ export class Script {
return this._entries;
}
findFunctionSourcePosition(sourcePosition) {
// TODO(cbruni) implmenent
return undefined;
}
addSourcePosition(line, column, entry) {
let sourcePosition = this.lineToColumn.get(line)?.get(column);
if (sourcePosition === undefined) {
......
......@@ -64,6 +64,8 @@ found in the LICENSE file. -->
</timeline-panel>
<div class="panels">
<script-panel id="script-panel"></script-panel>
<code-panel id="code-panel"></code-panel>
<map-panel id="map-panel"></map-panel>
<list-panel id="ic-list" title="IC List">
<div class="legend">
......@@ -90,8 +92,6 @@ found in the LICENSE file. -->
<list-panel id="deopt-list" title="Deopt Events"></list-panel>
<list-panel id="code-list" title="Code Events"></list-panel>
<list-panel id="api-list" title="API Events"></list-panel>
<script-panel id="script-panel"></script-panel>
<code-panel id="code-panel"></code-panel>
</div>
</section>
......
......@@ -72,6 +72,7 @@ class App {
import('./view/map-panel.mjs'),
import('./view/script-panel.mjs'),
import('./view/code-panel.mjs'),
import('./view/property-link-table.mjs'),
import('./view/tool-tip.mjs'),
]);
document.addEventListener(
......@@ -104,14 +105,6 @@ class App {
case IcLogEntry:
if (entry.map) entries.push(entry.map);
break;
case ApiLogEntry:
break;
case CodeLogEntry:
break;
case TickLogEntry:
break;
case SharedLibLogEntry:
break;
case DeoptLogEntry:
// TODO select map + code entries
if (entry.fileSourcePosition) entries.push(entry.fileSourcePosition);
......@@ -119,6 +112,11 @@ class App {
case Script:
entries = entry.entries.concat(entry.sourcePositions);
break;
case ApiLogEntry:
case CodeLogEntry:
case TickLogEntry:
case SharedLibLogEntry:
break;
default:
throw new Error(`Unknown selection type: ${entry.constructor?.name}`);
}
......@@ -178,34 +176,40 @@ class App {
}
}
showMapEntries(entries) {
showMapEntries(entries, focusView = true) {
this._view.mapPanel.selectedLogEntries = entries;
this._view.mapList.selectedLogEntries = entries;
if (focusView) this._view.mapPanel.show();
}
showIcEntries(entries) {
showIcEntries(entries, focusView = true) {
this._view.icList.selectedLogEntries = entries;
if (focusView) this._view.icList.show();
}
showDeoptEntries(entries) {
showDeoptEntries(entries, focusView = true) {
this._view.deoptList.selectedLogEntries = entries;
if (focusView) this._view.deoptList.show();
}
showSharedLibEntries(entries) {}
showSharedLibEntries(entries, focusView = true) {}
showCodeEntries(entries) {
showCodeEntries(entries, focusView = true) {
this._view.codePanel.selectedEntries = entries;
this._view.codeList.selectedLogEntries = entries;
if (focusView) this._view.codePanel.show();
}
showApiEntries(entries) {
showApiEntries(entries, focusView = true) {
this._view.apiList.selectedLogEntries = entries;
if (focusView) this._view.apiList.show();
}
showTickEntries(entries) {}
showTickEntries(entries, focusView = true) {}
showSourcePositions(entries) {
showSourcePositions(entries, focusView = true) {
this._view.scriptPanel.selectedSourcePositions = entries
if (focusView) this._view.scriptPanel.show();
}
handleTimeRangeSelect(e) {
......@@ -215,12 +219,12 @@ class App {
selectTimeRange(start, end) {
this._state.selectTimeRange(start, end);
this.showMapEntries(this._state.mapTimeline.selectionOrSelf);
this.showIcEntries(this._state.icTimeline.selectionOrSelf);
this.showDeoptEntries(this._state.deoptTimeline.selectionOrSelf);
this.showCodeEntries(this._state.codeTimeline.selectionOrSelf);
this.showApiEntries(this._state.apiTimeline.selectionOrSelf);
this.showTickEntries(this._state.tickTimeline.selectionOrSelf);
this.showMapEntries(this._state.mapTimeline.selectionOrSelf, false);
this.showIcEntries(this._state.icTimeline.selectionOrSelf, false);
this.showDeoptEntries(this._state.deoptTimeline.selectionOrSelf, false);
this.showCodeEntries(this._state.codeTimeline.selectionOrSelf, false);
this.showApiEntries(this._state.apiTimeline.selectionOrSelf, false);
this.showTickEntries(this._state.tickTimeline.selectionOrSelf, false);
this._view.timelinePanel.timeSelection = {start, end};
}
......@@ -254,25 +258,32 @@ class App {
}
}
focusMapLogEntry(entry) {
focusMapLogEntry(entry, focusSourcePosition = true) {
this._state.map = entry;
this._view.mapTrack.focusedEntry = entry;
this._view.mapPanel.map = entry;
this._view.mapPanel.show();
if (focusSourcePosition) this.focusSourcePosition(entry.sourcePosition);
}
focusIcLogEntry(entry) {
this._state.ic = entry;
this.focusMapLogEntry(entry.map, false);
this.focusCodeLogEntry(entry.codeLogEntry, false);
this.focusSourcePosition(entry.sourcePosition);
}
focusCodeLogEntry(entry) {
focusCodeLogEntry(entry, focusSourcePosition = true) {
this._state.code = entry;
this._view.codePanel.entry = entry;
if (focusSourcePosition) this.focusSourcePosition(entry.sourcePosition);
this._view.codePanel.show();
}
focusDeoptLogEntry(entry) {
this._state.deoptLogEntry = entry;
this.focusCodeLogEntry(entry.codeLogEntry, false);
this.focusSourcePosition(entry.sourcePosition);
}
focusSharedLibLogEntry(entry) {
......@@ -282,6 +293,7 @@ class App {
focusApiLogEntry(entry) {
this._state.apiLogEntry = entry;
this._view.apiTrack.focusedEntry = entry;
this.focusSourcePosition(entry.sourcePosition);
}
focusTickLogEntry(entry) {
......@@ -297,23 +309,12 @@ class App {
handleToolTip(event) {
let content = event.content;
switch (content.constructor) {
case String:
break;
case Script:
case SourcePosition:
case MapLogEntry:
case IcLogEntry:
case ApiLogEntry:
case CodeLogEntry:
case DeoptLogEntry:
case SharedLibLogEntry:
case TickLogEntry:
content = content.toolTipDict;
break;
default:
throw new Error(
`Unknown tooltip content type: ${entry.constructor?.name}`);
if (typeof content !== 'string') {
content = content?.toolTipDict;
}
if (!content) {
throw new Error(
`Unknown tooltip content type: ${content.constructor?.name}`);
}
this.setToolTip(content, event.positionOrTargetNode);
}
......@@ -416,8 +417,8 @@ class Navigation {
}
selectPrevEdge() {
if (!this.map) return;
if (!this.map.parent()) return;
this.map = this.map.parent();
if (!this.map.parent) return;
this.map = this.map.parent;
this._view.mapTrack.selectedEntry = this.map;
this.updateUrl();
this._view.mapPanel.map = this.map;
......
......@@ -32,6 +32,10 @@ export class DeoptLogEntry extends LogEntry {
return this._entry;
}
get codeLogEntry() {
return this._entry?.logEntry;
}
get functionName() {
return this._entry.functionName;
}
......@@ -100,13 +104,17 @@ export class CodeLogEntry extends LogEntry {
}
export class SharedLibLogEntry extends LogEntry {
constructor(name) {
constructor(entry) {
super('SHARED_LIB', 0);
this._name = name;
this._entry = entry;
}
get name() {
return this._name;
return this._entry.name;
}
get entry() {
return this._entry;
}
toString() {
......
......@@ -6,7 +6,7 @@ import {LogEntry} from './log.mjs';
export class IcLogEntry extends LogEntry {
constructor(
type, fn_file, time, line, column, key, oldState, newState, map, reason,
modifier, additional) {
modifier, codeEntry) {
super(type, time);
this.category = 'other';
if (this.type.indexOf('Store') !== -1) {
......@@ -17,15 +17,21 @@ export class IcLogEntry extends LogEntry {
const parts = fn_file.split(' ');
this.functionName = parts[0];
this.file = parts[1];
let position = line + ':' + column;
this.oldState = oldState;
this.newState = newState;
this.state = this.oldState + ' → ' + this.newState;
this.key = key;
this.map = map;
this.reason = reason;
this.additional = additional;
this.modifier = modifier;
this.codeEntry = codeEntry;
}
get state() {
return this.oldState + ' → ' + this.newState;
}
get codeLogEntry() {
return this.codeEntry?.logEntry;
}
parseMapProperties(parts, offset) {
......
......@@ -83,7 +83,7 @@ class MapLogEntry extends LogEntry {
this.leftId = currentId
}
parent() {
get parent() {
return this.edge?.from;
}
......@@ -134,7 +134,7 @@ class MapLogEntry extends LogEntry {
if (edge && edge.isTransition()) {
transitions[edge.name] = edge;
}
current = current.parent()
current = current.parent;
}
return transitions;
}
......@@ -157,10 +157,10 @@ class MapLogEntry extends LogEntry {
getParents() {
let parents = [];
let current = this.parent();
let current = this.parent;
while (current) {
parents.push(current);
current = current.parent();
current = current.parent;
}
return parents;
}
......
......@@ -221,7 +221,7 @@ export class Processor extends LogReader {
processSharedLibrary(name, start, end, aslr_slide) {
const entry = this._profile.addLibrary(name, start, end);
entry.logEntry = new SharedLibLogEntry(name);
entry.logEntry = new SharedLibLogEntry(entry);
}
processCodeCreation(type, kind, timestamp, start, size, name, maybe_func) {
......@@ -341,14 +341,14 @@ export class Processor extends LogReader {
type, pc, time, line, column, old_state, new_state, mapId, key, modifier,
slow_reason) {
this._lastTimestamp = time;
const profileEntry = this._profile.findEntry(pc);
const fnName = this.formatProfileEntry(profileEntry);
const script = this.getProfileEntryScript(profileEntry);
const codeEntry = this._profile.findEntry(pc);
const fnName = this.formatProfileEntry(codeEntry);
const script = this.getProfileEntryScript(codeEntry);
const map = this.getOrCreateMapEntry(mapId, time);
// TODO: Use SourcePosition here directly
let entry = new IcLogEntry(
type, fnName, time, line, column, key, old_state, new_state, map,
slow_reason, modifier);
slow_reason, modifier, codeEntry);
if (script) {
entry.sourcePosition = script.addSourcePosition(line, column, entry);
}
......@@ -371,10 +371,15 @@ export class Processor extends LogReader {
if (profileEntry.type === 'Builtin') return undefined;
const script = profileEntry.source?.script;
if (script !== undefined) return script;
// Slow path, try to get the script from the url:
const fnName = this.formatProfileEntry(profileEntry);
let parts = fnName.split(' ');
let fileName = parts[parts.length - 1];
let fileName;
if (profileEntry.type = 'SHARED_LIB') {
fileName = profileEntry.name;
} else {
// Slow path, try to get the script from the url:
const fnName = this.formatProfileEntry(profileEntry);
let parts = fnName.split(' ');
fileName = parts[parts.length - 1];
}
return this.getScript(fileName);
}
......@@ -390,7 +395,7 @@ export class Processor extends LogReader {
if (type === 'Normalize') {
// Fix a bug where we log "Normalize" transitions for maps created from
// the NormalizedMapCache.
if (to_.parent()?.id === from || to_.time < from_.time || to_.depth > 0) {
if (to_.parent?.id === from || to_.time < from_.time || to_.depth > 0) {
console.log(`Skipping transition to cached normalized map`);
return;
}
......@@ -403,7 +408,7 @@ export class Processor extends LogReader {
if (script) {
to_.sourcePosition = script.addSourcePosition(line, column, to_)
}
if (to_.parent() !== undefined && to_.parent() === from_) {
if (to_.parent !== undefined && to_.parent === from_) {
// Fix bug where we double log transitions.
console.warn('Fixing up double transition');
to_.edge.updateFrom(edge);
......
......@@ -278,7 +278,7 @@ class Chunk {
}
filter() {
return this.items.filter(map => !map.parent() || !this.has(map.parent()));
return this.items.filter(map => !map.parent || !this.has(map.parent));
}
}
......
<!-- Copyright 2020 the V8 project authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<head>
<link href="./index.css" rel="stylesheet">
</head>
......@@ -19,6 +18,8 @@ found in the LICENSE file. -->
<button id="selectedRelatedButton">Select Related Events</button>
</div>
<div class="panelBody">
<h3>Properties</h3>
<property-link-table id="properties"></property-link-table>
<h3>Disassembly</h3>
<pre id="disassembly"></pre>
<h3>Source Code</h3>
......
......@@ -31,6 +31,19 @@ DOM.defineCustomElement('view/code-panel',
set entry(entry) {
this._entry = entry;
if (entry !== undefined) {
this.$('#properties').propertyDict = {
'__this__': entry,
functionName: entry.functionName,
size: formatBytes(entry.size),
sourcePosition: entry.sourcePosition,
script: entry.script,
type: entry.type,
kind: entry.kindName,
};
} else {
this.$('#properties').propertyDict = {};
}
this.requestUpdate();
}
......
......@@ -284,15 +284,7 @@ export class CollapsableElement extends V8CustomElement {
constructor(templateText) {
super(templateText);
this._hasPendingUpdate = false;
this._closer.onclick = _ => this.tryUpdateOnVisibilityChange();
}
hide() {
if (this._contentIsVisible) this._closer.click();
}
show() {
if (!this._contentIsVisible) this._closer.click();
this._closer.onclick = _ => this._requestUpdateIfVisible();
}
get _closer() {
......@@ -303,19 +295,30 @@ export class CollapsableElement extends V8CustomElement {
return !this._closer.checked;
}
hide() {
if (this._contentIsVisible) {
this._closer.checked = true;
this._requestUpdateIfVisible();
}
this.scrollIntoView();
}
show() {
if (!this._contentIsVisible) {
this._closer.checked = false;
this._requestUpdateIfVisible();
}
this.scrollIntoView();
}
requestUpdate(useAnimation = false) {
// A pending update will be resolved later, no need to try again.
if (this._hasPendingUpdate) return;
this._hasPendingUpdate = true;
this.requestUpdateIfVisible(useAnimation);
}
tryUpdateOnVisibilityChange() {
if (!this._hasPendingUpdate) return;
this.requestUpdateIfVisible(true);
this._requestUpdateIfVisible(useAnimation);
}
requestUpdateIfVisible(useAnimation) {
_requestUpdateIfVisible(useAnimation = true) {
if (!this._contentIsVisible) return;
return super.requestUpdate(useAnimation);
}
......
<!-- Copyright 2020 the V8 project authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file. -->
<head>
<link href="./index.css" rel="stylesheet">
</head>
<style>
.properties td {
vertical-align: top;
}
.properties > tbody > tr > td:nth-child(2n+1):after {
content: ':';
}
.properties {
min-width: 350px;
border-collapse: collapse;
}
</style>
<div id="body">
<div id="content"></div>
</div>
\ No newline at end of file
// Copyright 2021 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {App} from '../index.mjs'
import {FocusEvent} from './events.mjs';
import {DOM, ExpandableText, V8CustomElement} from './helper.mjs';
DOM.defineCustomElement(
'view/property-link-table',
template => class PropertyLinkTable extends V8CustomElement {
_instance;
_propertyDict;
_instanceLinkButtons = true;
_logEntryClickHandler = this._handleLogEntryClick.bind(this);
_logEntryRelatedHandler = this._handleLogEntryRelated.bind(this);
constructor() {
super(template);
}
static get observedAttributes() {
return ['noButtons'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name == 'instanceLinkButtons') this._instanceLinkButtons = true;
}
set propertyDict(propertyDict) {
if (this._propertyDict === propertyDict) return;
this._propertyDict = propertyDict;
this.requestUpdate();
}
_update() {
this._fragment = new DocumentFragment();
this._table = DOM.table('properties');
for (let key in this._propertyDict) {
const value = this._propertyDict[key];
this._addKeyValue(key, value);
}
this._addFooter();
this._fragment.appendChild(this._table);
const newContent = DOM.div();
newContent.appendChild(this._fragment);
this.$('#content').replaceWith(newContent);
newContent.id = 'content';
this._fragment = undefined;
}
_addKeyValue(key, value) {
if (key == 'title') {
this._addTitle(value);
return;
}
if (key == '__this__') {
this._instance = value;
return;
}
const row = this._table.insertRow();
row.insertCell().innerText = key;
const cell = row.insertCell();
if (value == undefined) return;
if (App.isClickable(value)) {
cell.className = 'clickable';
cell.onclick = this._logEntryClickHandler;
cell.data = value;
}
new ExpandableText(cell, value.toString());
}
_addTitle(value) {
const title = DOM.element('h3');
title.innerText = value;
this._fragment.appendChild(title);
}
_addFooter() {
if (this._instance === undefined) return;
if (!this._instanceLinkButtons) return;
const td = this._table.createTFoot().insertRow().insertCell();
td.colSpan = 2;
let showButton =
td.appendChild(DOM.button('Show', this._logEntryClickHandler));
showButton.data = this._instance;
let showRelatedButton = td.appendChild(
DOM.button('Show Related', this._logEntryRelatedClickHandler));
showRelatedButton.data = this._instance;
}
_handleLogEntryClick(e) {
this.dispatchEvent(new FocusEvent(e.currentTarget.data));
}
_handleLogEntryRelated(e) {
this.dispatchEvent(new SelectRelatedEvent(e.currentTarget.data));
}
});
......@@ -34,8 +34,11 @@ DOM.defineCustomElement('view/script-panel',
set script(script) {
if (this._script === script) return;
this._script = script;
this._renderSourcePanel();
this._updateScriptDropdownSelection();
this.requestUpdate();
}
set focusedSourcePositions(sourcePositions) {
this.selectedSourcePositions = sourcePositions;
}
set selectedSourcePositions(sourcePositions) {
......@@ -45,10 +48,6 @@ DOM.defineCustomElement('view/script-panel',
this._focusSelectedMarkers();
}
set focusedSourcePositions(sourcePositions) {
this.selectedSourcePositions = sourcePositions;
}
set scripts(scripts) {
this._scripts = scripts;
this._initializeScriptDropdown();
......@@ -58,6 +57,11 @@ DOM.defineCustomElement('view/script-panel',
return this.$('#script-dropdown');
}
_update() {
this._renderSourcePanel();
this._updateScriptDropdownSelection();
}
_initializeScriptDropdown() {
this._scripts.sort((a, b) => a.name.localeCompare(b.name));
let select = this.scriptDropdown;
......@@ -70,6 +74,7 @@ DOM.defineCustomElement('view/script-panel',
select.add(option);
}
}
_updateScriptDropdownSelection() {
this.scriptDropdown.selectedIndex =
this._script ? this._scripts.indexOf(this._script) : -1;
......
......@@ -309,6 +309,7 @@ export class TimelineTrackBase extends V8CustomElement {
}
_handleClick(event) {
if (event.button !== 0) return;
if (event.target === this.timelineChunks) return;
this.isLocked = !this.isLocked;
event.stopImmediatePropagation();
......@@ -317,6 +318,7 @@ export class TimelineTrackBase extends V8CustomElement {
}
_handleDoubleClick(event) {
if (event.button !== 0) return;
this._selectionHandler.clearSelection();
const time = this.positionToTime(event.pageX);
const chunk = this._getChunkForEvent(event)
......@@ -327,11 +329,12 @@ export class TimelineTrackBase extends V8CustomElement {
}
_handleMouseMove(event) {
if (this.isLocked) return false;
if (event.button !== 0) return;
if (this._selectionHandler.isSelecting) return false;
const logEntry = this._getEntryForEvent(event);
if (!logEntry) return false;
this.dispatchEvent(new ToolTipEvent(logEntry, this.toolTipTargetNode));
if (this.isLocked) return false;
const time = this.positionToTime(event.pageX);
this._drawAnnotations(logEntry, time);
}
......@@ -458,8 +461,9 @@ class SelectionHandler {
SelectionHandler.SELECTION_OFFSET;
}
_handleTimeSelectionMouseDown(e) {
let xPosition = e.clientX
_handleTimeSelectionMouseDown(event) {
if (event.button !== 0) return;
let xPosition = event.clientX
// Update origin time in case we click on a handle.
if (this._isOnLeftHandle(xPosition)) {
xPosition = this._rightHandlePosX;
......@@ -470,16 +474,19 @@ class SelectionHandler {
this._selectionOriginTime = this.positionToTime(xPosition);
}
_handleTimeSelectionMouseMove(e) {
_handleTimeSelectionMouseMove(event) {
if (event.button !== 0) return;
if (!this.isSelecting) return;
const currentTime = this.positionToTime(e.clientX);
const currentTime = this.positionToTime(event.clientX);
this._timeline.dispatchEvent(new SynchronizeSelectionEvent(
Math.min(this._selectionOriginTime, currentTime),
Math.max(this._selectionOriginTime, currentTime)));
}
_handleTimeSelectionMouseUp(e) {
_handleTimeSelectionMouseUp(event) {
if (event.button !== 0) return;
this._selectionOriginTime = -1;
if (this._timeSelection.start === -1) return;
const delta = this._timeSelection.end - this._timeSelection.start;
if (delta <= 1 || isNaN(delta)) return;
this._timeline.dispatchEvent(new SelectTimeEvent(
......
......@@ -46,7 +46,7 @@ DOM.defineCustomElement('view/timeline/timeline-track', 'timeline-track-map',
let current = logEntry;
while (current !== undefined) {
stack.push(current);
current = current.parent();
current = current.parent;
}
// Draw outgoing refs as fuzzy background. Skip the last map entry.
......@@ -54,7 +54,7 @@ DOM.defineCustomElement('view/timeline/timeline-track', 'timeline-track-map',
let nofEdges = 0;
const kMaxOutgoingEdges = 100;
for (let i = stack.length - 2; i >= 0; i--) {
const map = stack[i].parent();
const map = stack[i].parent;
nofEdges += map.children.length;
if (nofEdges > kMaxOutgoingEdges) break;
buffer += this.drawOutgoingEdges(map, 0.4, 1);
......
......@@ -77,6 +77,7 @@ DOM.defineCustomElement('view/timeline/timeline-track', 'timeline-track-tick',
}
_handleDoubleClick(event) {
if (event.button !== 0) return;
this._selectionHandler.clearSelection();
const flame = this._getFlameForEvent(event);
if (flame === undefined) return;
......
......@@ -101,22 +101,11 @@ found in the LICENSE file. -->
.right > .tipThin {
left: var(--tip-offset);
}
.properties td {
vertical-align: top;
}
.properties > tbody > tr > td:nth-child(2n+1):after {
content: ':';
}
.properties {
min-width: 350px;
}
</style>
<div id="body">
<div id="content">
<property-link-table id="properties" showButtons></property-link-table>
</div>
<div class="tip">
<div class="tipThin"></div>
......
......@@ -2,18 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {App} from '../index.mjs'
import {FocusEvent} from './events.mjs';
import {DOM, ExpandableText, V8CustomElement} from './helper.mjs';
import {DOM, V8CustomElement} from './helper.mjs';
DOM.defineCustomElement(
'view/tool-tip', (templateText) => class Tooltip extends V8CustomElement {
_targetNode;
_content;
_isHidden = true;
_logEntryClickHandler = this._handleLogEntryClick.bind(this);
_logEntryRelatedHandler = this._handleLogEntryRelated.bind(this);
constructor() {
super(templateText);
......@@ -90,7 +85,13 @@ DOM.defineCustomElement(
} else if (content?.nodeType && nodeType?.nodeName) {
this._setContentNode(content);
} else {
this._setContentNode(new TableBuilder(this, content).fragment);
if (this.contentNode.firstChild?.localName == 'property-link-table') {
this.contentNode.firstChild.propertyDict = content;
} else {
const node = DOM.element('property-link-table');
node.propertyDict = content;
this._setContentNode(node);
}
}
}
......@@ -101,14 +102,6 @@ DOM.defineCustomElement(
newContent.id = 'content';
}
_handleLogEntryClick(e) {
this.dispatchEvent(new FocusEvent(e.currentTarget.data));
}
_handleLogEntryRelated(e) {
this.dispatchEvent(new SelectRelatedEvent(e.currentTarget.data));
}
hide() {
this._isHidden = true;
this.bodyNode.style.display = 'none';
......@@ -128,59 +121,3 @@ DOM.defineCustomElement(
return this.$('#content');
}
});
class TableBuilder {
_instance;
constructor(tooltip, descriptor) {
this._fragment = new DocumentFragment();
this._table = DOM.table('properties');
this._tooltip = tooltip;
for (let key in descriptor) {
const value = descriptor[key];
this._addKeyValue(key, value);
}
this._addFooter();
this._fragment.appendChild(this._table);
}
_addKeyValue(key, value) {
if (key == 'title') return this._addTitle(value);
if (key == '__this__') {
this._instance = value;
return;
}
const row = this._table.insertRow();
row.insertCell().innerText = key;
const cell = row.insertCell();
if (value == undefined) return;
if (App.isClickable(value)) {
cell.className = 'clickable';
cell.onclick = this._logEntryClickHandler;
cell.data = value;
}
new ExpandableText(cell, value.toString());
}
_addTitle(value) {
const title = DOM.element('h3');
title.innerText = value;
this._fragment.appendChild(title);
}
_addFooter() {
if (this._instance === undefined) return;
const td = this._table.createTFoot().insertRow().insertCell();
td.colSpan = 2;
let button =
td.appendChild(DOM.button('Show', this._tooltip._logEntryClickHandler));
button.data = this._instance;
button = td.appendChild(
DOM.button('Show Related', this._tooltip._logEntryRelatedClickHandler));
button.data = this._instance;
}
get fragment() {
return this._fragment;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment