All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.dirigible.dev-tools.elements.ElementsBreadcrumbs.js Maven / Gradle / Ivy

There is a newer version: 10.6.27
Show newest version
// 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.

import * as Common from '../common/common.js';
import * as SDK from '../sdk/sdk.js';
import * as UI from '../ui/ui.js';

import {decorateNodeLabel} from './DOMLinkifier.js';

/**
 * @unrestricted
 */
export class ElementsBreadcrumbs extends UI.Widget.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');

    UI.ARIAUtils.markAsHidden(this.element);
  }

  /**
   * @override
   */
  wasShown() {
    this.update();
  }

  /**
   * @param {!Array.} 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.DOMModel.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.DOMModel.DOMNode} */ (crumbElement ? crumbElement[this._nodeSymbol] : null);
    if (node) {
      node.highlight();
    }
  }

  _mouseMovedOutOfCrumbs(event) {
    if (this._currentDOMNode) {
      SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
    }
  }


  /**
   * @param {!Event} event
   * @this {ElementsBreadcrumbs}
   */
  _onClickCrumb(event) {
    event.preventDefault();
    let crumb = /** @type {!Element} */ (event.currentTarget);
    if (!crumb.classList.contains('collapsed')) {
      this.dispatchEventToListeners(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.DOMModel.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.UIString('(text)');
      case Node.COMMENT_NODE:
        return '';
      case Node.DOCUMENT_TYPE_NODE:
        return '';
      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 {
        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., compact: !Array., 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} */
export const Events = {
  NodeSelected: Symbol('NodeSelected')
};




© 2015 - 2024 Weber Informatics LLC | Privacy Policy