1// Copyright 2020 the V8 project authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4import {FocusEvent, SelectRelatedEvent, ToolTipEvent} from '../events.mjs'; 5import {CSSColor} from '../helper.mjs'; 6import {DOM, V8CustomElement} from '../helper.mjs'; 7 8DOM.defineCustomElement('./view/map-panel/map-transitions', 9 (templateText) => 10 class MapTransitions extends V8CustomElement { 11 _timeline; 12 _map; 13 _edgeToColor = new Map(); 14 _selectedLogEntries; 15 _displayedMapsInTree; 16 _toggleSubtreeHandler = this._handleToggleSubtree.bind(this); 17 _mapClickHandler = this._handleMapClick.bind(this); 18 _mapDoubleClickHandler = this._handleMapDoubleClick.bind(this); 19 _mouseoverMapHandler = this._handleMouseoverMap.bind(this); 20 21 constructor() { 22 super(templateText); 23 this.currentNode = this.transitionView; 24 } 25 26 get transitionView() { 27 return this.$('#transitionView'); 28 } 29 30 set timeline(timeline) { 31 this._timeline = timeline; 32 this._edgeToColor.clear(); 33 timeline.getBreakdown().forEach(breakdown => { 34 this._edgeToColor.set(breakdown.key, CSSColor.at(breakdown.id)); 35 }); 36 } 37 38 set selectedLogEntries(list) { 39 this._selectedLogEntries = list; 40 this.requestUpdate(); 41 } 42 43 _update() { 44 this.transitionView.style.display = 'none'; 45 DOM.removeAllChildren(this.transitionView); 46 if (this._selectedLogEntries.length == 0) return; 47 this._displayedMapsInTree = new Set(); 48 // Limit view to 200 maps for performance reasons. 49 this._selectedLogEntries.slice(0, 200).forEach( 50 (map) => this._addMapAndParentTransitions(map)); 51 this._displayedMapsInTree = undefined; 52 this.transitionView.style.display = ''; 53 } 54 55 _addMapAndParentTransitions(map) { 56 if (map === undefined) return; 57 if (this._displayedMapsInTree.has(map)) return; 58 this._displayedMapsInTree.add(map); 59 this.currentNode = this.transitionView; 60 let parents = map.getParents(); 61 if (parents.length > 0) { 62 this._addTransitionTo(parents.pop()); 63 parents.reverse().forEach((each) => this._addTransitionTo(each)); 64 } 65 let mapNode = this._addSubtransitions(map); 66 // Mark and show the selected map. 67 mapNode.classList.add('selected'); 68 if (this.selectedMap == map) { 69 setTimeout( 70 () => mapNode.scrollIntoView({ 71 behavior: 'smooth', 72 block: 'nearest', 73 inline: 'nearest', 74 }), 75 1); 76 } 77 } 78 79 _addSubtransitions(map) { 80 let mapNode = this._addTransitionTo(map); 81 // Draw outgoing linear transition line. 82 let current = map; 83 while (current.children.length == 1) { 84 current = current.children[0].to; 85 this._addTransitionTo(current); 86 } 87 return mapNode; 88 } 89 90 _addTransitionEdge(map) { 91 let classes = ['transitionEdge']; 92 let edge = DOM.div(classes); 93 edge.style.backgroundColor = this._edgeToColor.get(map.edge.type); 94 let labelNode = DOM.div('transitionLabel'); 95 labelNode.innerText = map.edge.toString(); 96 edge.appendChild(labelNode); 97 return edge; 98 } 99 100 _addTransitionTo(map) { 101 // transition[ transitions[ transition[...], transition[...], ...]]; 102 this._displayedMapsInTree?.add(map); 103 let transition = DOM.div('transition'); 104 if (map.isDeprecated()) transition.classList.add('deprecated'); 105 if (map.edge) { 106 transition.appendChild(this._addTransitionEdge(map)); 107 } 108 let mapNode = this._addMapNode(map); 109 transition.appendChild(mapNode); 110 111 let subtree = DOM.div('transitions'); 112 transition.appendChild(subtree); 113 114 this.currentNode.appendChild(transition); 115 this.currentNode = subtree; 116 117 return mapNode; 118 } 119 120 _addMapNode(map) { 121 let node = DOM.div('map'); 122 if (map.edge) 123 node.style.backgroundColor = this._edgeToColor.get(map.edge.type); 124 node.map = map; 125 node.onclick = this._mapClickHandler 126 node.ondblclick = this._mapDoubleClickHandler 127 node.onmouseover = this._mouseoverMapHandler 128 if (map.children.length > 1) { 129 node.innerText = map.children.length; 130 const showSubtree = DOM.div('showSubtransitions'); 131 showSubtree.onclick = this._toggleSubtreeHandler 132 node.appendChild(showSubtree); 133 } 134 else if (map.children.length == 0) { 135 node.innerHTML = '●'; 136 } 137 this.currentNode.appendChild(node); 138 return node; 139 } 140 141 _handleMapClick(event) { 142 const map = event.currentTarget.map; 143 this.dispatchEvent(new FocusEvent(map)); 144 } 145 146 _handleMapDoubleClick(event) { 147 this.dispatchEvent(new SelectRelatedEvent(event.currentTarget.map)); 148 } 149 150 _handleMouseoverMap(event) { 151 this.dispatchEvent( 152 new ToolTipEvent(event.currentTarget.map, event.currentTarget)); 153 } 154 155 _handleToggleSubtree(event) { 156 event.stopImmediatePropagation(); 157 const node = event.currentTarget.parentElement; 158 const map = node.map; 159 event.target.classList.toggle('opened'); 160 const transitionsNode = node.parentElement.querySelector('.transitions'); 161 const subtransitionNodes = transitionsNode.children; 162 if (subtransitionNodes.length <= 1) { 163 // Add subtransitions except the one that's already shown. 164 let visibleTransitionMap = subtransitionNodes.length == 1 ? 165 transitionsNode.querySelector('.map').map : 166 undefined; 167 map.children.forEach((edge) => { 168 if (edge.to != visibleTransitionMap) { 169 this.currentNode = transitionsNode; 170 this._addSubtransitions(edge.to); 171 } 172 }); 173 } else { 174 // remove all but the first (currently selected) subtransition 175 for (let i = subtransitionNodes.length - 1; i > 0; i--) { 176 transitionsNode.removeChild(subtransitionNodes[i]); 177 } 178 } 179 return false; 180 } 181}); 182