UNPKG

@dcloudio/uni-debugger

Version:

uni-app debugger

445 lines (380 loc) 13.4 kB
// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @unrestricted */ Elements.ElementsBreadcrumbs = class extends UI.HBox { constructor() { super(true); this.registerRequiredCSS('elements/breadcrumbs.css'); this.crumbsElement = this.contentElement.createChild('div', 'crumbs'); this.crumbsElement.addEventListener('mousemove', this._mouseMovedInCrumbs.bind(this), false); this.crumbsElement.addEventListener('mouseleave', this._mouseMovedOutOfCrumbs.bind(this), false); this._nodeSymbol = Symbol('node'); } /** * @override */ wasShown() { this.update(); } /** * @param {!Array.<!SDK.DOMNode>} nodes */ updateNodes(nodes) { if (!nodes.length) return; const crumbs = this.crumbsElement; for (let crumb = crumbs.firstChild; crumb; crumb = crumb.nextSibling) { if (nodes.indexOf(crumb[this._nodeSymbol]) !== -1) { this.update(true); return; } } } /** * @param {?SDK.DOMNode} node */ setSelectedNode(node) { this._currentDOMNode = node; this.crumbsElement.window().requestAnimationFrame(() => this.update()); } _mouseMovedInCrumbs(event) { const nodeUnderMouse = event.target; const crumbElement = nodeUnderMouse.enclosingNodeOrSelfWithClass('crumb'); const node = /** @type {?SDK.DOMNode} */ (crumbElement ? crumbElement[this._nodeSymbol] : null); if (node) node.highlight(); } _mouseMovedOutOfCrumbs(event) { if (this._currentDOMNode) SDK.OverlayModel.hideDOMNodeHighlight(); } /** * @param {!Event} event * @this {Elements.ElementsBreadcrumbs} */ _onClickCrumb(event) { event.preventDefault(); let crumb = /** @type {!Element} */ (event.currentTarget); if (!crumb.classList.contains('collapsed')) { this.dispatchEventToListeners(Elements.ElementsBreadcrumbs.Events.NodeSelected, crumb[this._nodeSymbol]); return; } // Clicking a collapsed crumb will expose the hidden crumbs. if (crumb === this.crumbsElement.firstChild) { // If the clicked crumb is the first child, pick the farthest crumb // that is still hidden. This allows the user to expose every crumb. let currentCrumb = crumb; while (currentCrumb) { const hidden = currentCrumb.classList.contains('hidden'); const collapsed = currentCrumb.classList.contains('collapsed'); if (!hidden && !collapsed) break; crumb = currentCrumb; currentCrumb = currentCrumb.nextSiblingElement; } } this.updateSizes(crumb); } /** * @param {!SDK.DOMNode} domNode * @return {?string} */ _determineElementTitle(domNode) { switch (domNode.nodeType()) { case Node.ELEMENT_NODE: if (domNode.pseudoType()) return '::' + domNode.pseudoType(); return null; case Node.TEXT_NODE: return Common.UIString('(text)'); case Node.COMMENT_NODE: return '<!-->'; case Node.DOCUMENT_TYPE_NODE: return '<!doctype>'; case Node.DOCUMENT_FRAGMENT_NODE: return domNode.shadowRootType() ? '#shadow-root' : domNode.nodeNameInCorrectCase(); default: return domNode.nodeNameInCorrectCase(); } } /** * @param {boolean=} force */ update(force) { if (!this.isShowing()) return; const currentDOMNode = this._currentDOMNode; const crumbs = this.crumbsElement; let handled = false; let crumb = crumbs.firstChild; while (crumb) { if (crumb[this._nodeSymbol] === currentDOMNode) { crumb.classList.add('selected'); handled = true; } else { crumb.classList.remove('selected'); } crumb = crumb.nextSibling; } if (handled && !force) { // We don't need to rebuild the crumbs, but we need to adjust sizes // to reflect the new focused or root node. this.updateSizes(); return; } crumbs.removeChildren(); for (let current = currentDOMNode; current; current = current.parentNode) { if (current.nodeType() === Node.DOCUMENT_NODE) continue; crumb = createElementWithClass('span', 'crumb'); crumb[this._nodeSymbol] = current; crumb.addEventListener('mousedown', this._onClickCrumb.bind(this), false); const crumbTitle = this._determineElementTitle(current); if (crumbTitle) { const nameElement = createElement('span'); nameElement.textContent = crumbTitle; crumb.appendChild(nameElement); crumb.title = crumbTitle; } else { Elements.DOMLinkifier.decorateNodeLabel(current, crumb); } if (current === currentDOMNode) crumb.classList.add('selected'); crumbs.insertBefore(crumb, crumbs.firstChild); } this.updateSizes(); } /** * @param {!Element=} focusedCrumb * @return {{selectedIndex: number, focusedIndex: number, selectedCrumb: ?Element}} */ _resetCrumbStylesAndFindSelections(focusedCrumb) { const crumbs = this.crumbsElement; let selectedIndex = 0; let focusedIndex = 0; let selectedCrumb = null; // Reset crumb styles. for (let i = 0; i < crumbs.childNodes.length; ++i) { const crumb = crumbs.children[i]; // Find the selected crumb and index. if (!selectedCrumb && crumb.classList.contains('selected')) { selectedCrumb = crumb; selectedIndex = i; } // Find the focused crumb index. if (crumb === focusedCrumb) focusedIndex = i; crumb.classList.remove('compact', 'collapsed', 'hidden'); } return {selectedIndex: selectedIndex, focusedIndex: focusedIndex, selectedCrumb: selectedCrumb}; } /** * @return {{normal: !Array.<number>, compact: !Array.<number>, collapsed: number, available: number}} */ _measureElementSizes() { const crumbs = this.crumbsElement; // Layout 1: Measure total and normal crumb sizes at the same time as a // dummy element for the collapsed size. const collapsedElement = createElementWithClass('span', 'crumb collapsed'); crumbs.insertBefore(collapsedElement, crumbs.firstChild); const available = crumbs.offsetWidth; const collapsed = collapsedElement.offsetWidth; const normalSizes = []; for (let i = 1; i < crumbs.childNodes.length; ++i) { const crumb = crumbs.childNodes[i]; normalSizes[i - 1] = crumb.offsetWidth; } crumbs.removeChild(collapsedElement); // Layout 2: Measure collapsed crumb sizes const compactSizes = []; for (let i = 0; i < crumbs.childNodes.length; ++i) { const crumb = crumbs.childNodes[i]; crumb.classList.add('compact'); } for (let i = 0; i < crumbs.childNodes.length; ++i) { const crumb = crumbs.childNodes[i]; compactSizes[i] = crumb.offsetWidth; } // Clean up. for (let i = 0; i < crumbs.childNodes.length; ++i) { const crumb = crumbs.childNodes[i]; crumb.classList.remove('compact', 'collapsed'); } return {normal: normalSizes, compact: compactSizes, collapsed: collapsed, available: available}; } /** * @param {!Element=} focusedCrumb */ updateSizes(focusedCrumb) { if (!this.isShowing()) return; const crumbs = this.crumbsElement; if (!crumbs.firstChild) return; const selections = this._resetCrumbStylesAndFindSelections(focusedCrumb); const sizes = this._measureElementSizes(); const selectedIndex = selections.selectedIndex; const focusedIndex = selections.focusedIndex; const selectedCrumb = selections.selectedCrumb; function crumbsAreSmallerThanContainer() { let totalSize = 0; for (let i = 0; i < crumbs.childNodes.length; ++i) { const crumb = crumbs.childNodes[i]; if (crumb.classList.contains('hidden')) continue; if (crumb.classList.contains('collapsed')) { totalSize += sizes.collapsed; continue; } totalSize += crumb.classList.contains('compact') ? sizes.compact[i] : sizes.normal[i]; } const rightPadding = 10; return totalSize + rightPadding < sizes.available; } if (crumbsAreSmallerThanContainer()) return; // No need to compact the crumbs, they all fit at full size. const BothSides = 0; const AncestorSide = -1; const ChildSide = 1; /** * @param {function(!Element)} shrinkingFunction * @param {number} direction */ function makeCrumbsSmaller(shrinkingFunction, direction) { const significantCrumb = focusedCrumb || selectedCrumb; const significantIndex = significantCrumb === selectedCrumb ? selectedIndex : focusedIndex; function shrinkCrumbAtIndex(index) { const shrinkCrumb = crumbs.children[index]; if (shrinkCrumb && shrinkCrumb !== significantCrumb) shrinkingFunction(shrinkCrumb); if (crumbsAreSmallerThanContainer()) return true; // No need to compact the crumbs more. return false; } // Shrink crumbs one at a time by applying the shrinkingFunction until the crumbs // fit in the container or we run out of crumbs to shrink. if (direction) { // Crumbs are shrunk on only one side (based on direction) of the signifcant crumb. let index = (direction > 0 ? 0 : crumbs.childNodes.length - 1); while (index !== significantIndex) { if (shrinkCrumbAtIndex(index)) return true; index += (direction > 0 ? 1 : -1); } } else { // Crumbs are shrunk in order of descending distance from the signifcant crumb, // with a tie going to child crumbs. let startIndex = 0; let endIndex = crumbs.childNodes.length - 1; while (startIndex !== significantIndex || endIndex !== significantIndex) { const startDistance = significantIndex - startIndex; const endDistance = endIndex - significantIndex; let index; if (startDistance >= endDistance) index = startIndex++; else index = endIndex--; if (shrinkCrumbAtIndex(index)) return true; } } // We are not small enough yet, return false so the caller knows. return false; } function coalesceCollapsedCrumbs() { let crumb = crumbs.firstChild; let collapsedRun = false; let newStartNeeded = false; let newEndNeeded = false; while (crumb) { const hidden = crumb.classList.contains('hidden'); if (!hidden) { const collapsed = crumb.classList.contains('collapsed'); if (collapsedRun && collapsed) { crumb.classList.add('hidden'); crumb.classList.remove('compact'); crumb.classList.remove('collapsed'); if (crumb.classList.contains('start')) { crumb.classList.remove('start'); newStartNeeded = true; } if (crumb.classList.contains('end')) { crumb.classList.remove('end'); newEndNeeded = true; } continue; } collapsedRun = collapsed; if (newEndNeeded) { newEndNeeded = false; crumb.classList.add('end'); } } else { collapsedRun = true; } crumb = crumb.nextSibling; } if (newStartNeeded) { crumb = crumbs.lastChild; while (crumb) { if (!crumb.classList.contains('hidden')) { crumb.classList.add('start'); break; } crumb = crumb.previousSibling; } } } /** * @param {!Element} crumb */ function compact(crumb) { if (crumb.classList.contains('hidden')) return; crumb.classList.add('compact'); } /** * @param {!Element} crumb * @param {boolean=} dontCoalesce */ function collapse(crumb, dontCoalesce) { if (crumb.classList.contains('hidden')) return; crumb.classList.add('collapsed'); crumb.classList.remove('compact'); if (!dontCoalesce) coalesceCollapsedCrumbs(); } if (!focusedCrumb) { // When not focused on a crumb we can be biased and collapse less important // crumbs that the user might not care much about. // Compact child crumbs. if (makeCrumbsSmaller(compact, ChildSide)) return; // Collapse child crumbs. if (makeCrumbsSmaller(collapse, ChildSide)) return; } // Compact ancestor crumbs, or from both sides if focused. if (makeCrumbsSmaller(compact, focusedCrumb ? BothSides : AncestorSide)) return; // Collapse ancestor crumbs, or from both sides if focused. if (makeCrumbsSmaller(collapse, focusedCrumb ? BothSides : AncestorSide)) return; if (!selectedCrumb) return; // Compact the selected crumb. compact(selectedCrumb); if (crumbsAreSmallerThanContainer()) return; // Collapse the selected crumb as a last resort. Pass true to prevent coalescing. collapse(selectedCrumb, true); } }; /** @enum {symbol} */ Elements.ElementsBreadcrumbs.Events = { NodeSelected: Symbol('NodeSelected') };