Move dom related utils from scripts/util.js to utils/dom-util.js
				
					
				
			Also replace existing usages and add more tests on all util methods. Change-Id: I89e0d9413153bfc115cd989ca7c66893b9709cc2
This commit is contained in:
		@@ -15,6 +15,8 @@
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import {descendedFromClass} from '../../utils/dom-util.js';
 | 
			
		||||
 | 
			
		||||
export const DomUtilBehavior = {
 | 
			
		||||
  /**
 | 
			
		||||
   * Are any ancestors of the element (or the element itself) members of the
 | 
			
		||||
@@ -28,13 +30,9 @@ export const DomUtilBehavior = {
 | 
			
		||||
   * @return {boolean}
 | 
			
		||||
   */
 | 
			
		||||
  descendedFromClass(element, className, opt_stopElement) {
 | 
			
		||||
    let isDescendant = element.classList.contains(className);
 | 
			
		||||
    while (!isDescendant && element.parentElement &&
 | 
			
		||||
        (!opt_stopElement || element.parentElement !== opt_stopElement)) {
 | 
			
		||||
      isDescendant = element.classList.contains(className);
 | 
			
		||||
      element = element.parentElement;
 | 
			
		||||
    }
 | 
			
		||||
    return isDescendant;
 | 
			
		||||
    console.warn('DomUtilBehavior is deprecated.' +
 | 
			
		||||
     'Use descendedFromClass from utils directly.');
 | 
			
		||||
    return descendedFromClass(element, className, opt_stopElement);
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -57,7 +57,7 @@ import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-beh
 | 
			
		||||
import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
 | 
			
		||||
import {GrEditConstants} from '../../edit/gr-edit-constants.js';
 | 
			
		||||
import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter.js';
 | 
			
		||||
import {util} from '../../../scripts/util.js';
 | 
			
		||||
import {getComputedStyleValue} from '../../../utils/dom-util.js';
 | 
			
		||||
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 | 
			
		||||
import {pluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
 | 
			
		||||
import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 | 
			
		||||
@@ -2003,7 +2003,7 @@ class GrChangeView extends mixinBehaviors( [
 | 
			
		||||
  _computeShowRelatedToggle() {
 | 
			
		||||
    // Make sure the max height has been applied, since there is now content
 | 
			
		||||
    // to populate.
 | 
			
		||||
    if (!util.getComputedStyleValue('--relation-chain-max-height', this)) {
 | 
			
		||||
    if (!getComputedStyleValue('--relation-chain-max-height', this)) {
 | 
			
		||||
      this._updateRelatedChangeMaxHeight();
 | 
			
		||||
    }
 | 
			
		||||
    // Prevents showMore from showing when click on related change, since the
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ import {dom} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 | 
			
		||||
import {KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 | 
			
		||||
import {GrEditConstants} from '../../edit/gr-edit-constants.js';
 | 
			
		||||
import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.js';
 | 
			
		||||
import {util} from '../../../scripts/util.js';
 | 
			
		||||
import {getComputedStyleValue} from '../../../utils/dom-util.js';
 | 
			
		||||
import {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
 | 
			
		||||
import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
 | 
			
		||||
import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
 | 
			
		||||
@@ -320,7 +320,7 @@ suite('gr-change-view tests', () => {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const getCustomCssValue =
 | 
			
		||||
      cssParam => util.getComputedStyleValue(cssParam, element);
 | 
			
		||||
      cssParam => getComputedStyleValue(cssParam, element);
 | 
			
		||||
 | 
			
		||||
  test('_handleMessageAnchorTap', () => {
 | 
			
		||||
    element._changeNum = '1';
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 | 
			
		||||
import {htmlTemplate} from './gr-diff-selection_html.js';
 | 
			
		||||
import {DomUtilBehavior} from '../../../behaviors/dom-util-behavior/dom-util-behavior.js';
 | 
			
		||||
import {GrRangeNormalizer} from '../gr-diff-highlight/gr-range-normalizer.js';
 | 
			
		||||
import {util} from '../../../scripts/util.js';
 | 
			
		||||
import {querySelectorAll} from '../../../utils/dom-util.js';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Possible CSS classes indicating the state of selection. Dynamically added/
 | 
			
		||||
@@ -203,7 +203,7 @@ class GrDiffSelection extends mixinBehaviors( [
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _getSelection() {
 | 
			
		||||
    const diffHosts = util.querySelectorAll(document.body, 'gr-diff');
 | 
			
		||||
    const diffHosts = querySelectorAll(document.body, 'gr-diff');
 | 
			
		||||
    if (!diffHosts.length) return window.getSelection();
 | 
			
		||||
 | 
			
		||||
    const curDiffHost = diffHosts.find(diffHost => {
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
 | 
			
		||||
import './gr-diff.js';
 | 
			
		||||
import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
 | 
			
		||||
import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image.js';
 | 
			
		||||
import {util} from '../../../scripts/util.js';
 | 
			
		||||
import {getComputedStyleValue} from '../../../utils/dom-util.js';
 | 
			
		||||
import {_setHiddenScroll} from '../../../scripts/hiddenscroll.js';
 | 
			
		||||
import {runA11yAudit} from '../../../test/a11y-test-utils.js';
 | 
			
		||||
import '@polymer/paper-button/paper-button.js';
 | 
			
		||||
@@ -82,14 +82,14 @@ suite('gr-diff tests', () => {
 | 
			
		||||
    element = basicFixture.instantiate();
 | 
			
		||||
    element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: true});
 | 
			
		||||
    flushAsynchronousOperations();
 | 
			
		||||
    assert.equal(util.getComputedStyleValue('--line-limit', element), '80ch');
 | 
			
		||||
    assert.equal(getComputedStyleValue('--line-limit', element), '80ch');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test('line limit without line_wrapping', () => {
 | 
			
		||||
    element = basicFixture.instantiate();
 | 
			
		||||
    element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: false});
 | 
			
		||||
    flushAsynchronousOperations();
 | 
			
		||||
    assert.isNotOk(util.getComputedStyleValue('--line-limit', element));
 | 
			
		||||
    assert.isNotOk(getComputedStyleValue('--line-limit', element));
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  suite('_get{PatchNum|IsParentComment}ByLineAndContent', () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 | 
			
		||||
import {htmlTemplate} from './gr-button_html.js';
 | 
			
		||||
import {TooltipBehavior} from '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
 | 
			
		||||
import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
 | 
			
		||||
import {util} from '../../../scripts/util.js';
 | 
			
		||||
import {getEventPath} from '../../../utils/dom-util.js';
 | 
			
		||||
import {appContext} from '../../../services/app-context.js';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
@@ -113,7 +113,7 @@ class GrButton extends mixinBehaviors( [
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.reporting.reportInteraction('button-click',
 | 
			
		||||
        {path: util.getEventPath(e)});
 | 
			
		||||
        {path: getEventPath(e)});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _disabledChanged(disabled) {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,17 +15,6 @@
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
function getPathFromNode(el) {
 | 
			
		||||
  if (!el.tagName || el.tagName === 'GR-APP'
 | 
			
		||||
      || el instanceof DocumentFragment
 | 
			
		||||
      || el instanceof HTMLSlotElement) {
 | 
			
		||||
    return '';
 | 
			
		||||
  }
 | 
			
		||||
  let path = el.tagName.toLowerCase();
 | 
			
		||||
  if (el.id) path += `#${el.id}`;
 | 
			
		||||
  if (el.className) path += `.${el.className.replace(/ /g, '.')}`;
 | 
			
		||||
  return path;
 | 
			
		||||
}
 | 
			
		||||
// TODO (dmfilippov): Each function must be exported separately. According to
 | 
			
		||||
// the code style guide, a namespacing is not allowed.
 | 
			
		||||
export const util = {
 | 
			
		||||
@@ -76,133 +65,4 @@ export const util = {
 | 
			
		||||
    };
 | 
			
		||||
    return wrappedPromise;
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get computed style value.
 | 
			
		||||
   *
 | 
			
		||||
   * If ShadyCSS is provided, use ShadyCSS api.
 | 
			
		||||
   * If `getComputedStyleValue` is provided on the element, use it.
 | 
			
		||||
   * Otherwise fallback to native method (in polymer 2).
 | 
			
		||||
   *
 | 
			
		||||
   */
 | 
			
		||||
  getComputedStyleValue: (name, el) => {
 | 
			
		||||
    let style;
 | 
			
		||||
    if (window.ShadyCSS) {
 | 
			
		||||
      style = ShadyCSS.getComputedStyleValue(el, name);
 | 
			
		||||
    } else if (el.getComputedStyleValue) {
 | 
			
		||||
      style = el.getComputedStyleValue(name);
 | 
			
		||||
    } else {
 | 
			
		||||
      style = getComputedStyle(el).getPropertyValue(name);
 | 
			
		||||
    }
 | 
			
		||||
    return style;
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Query selector on a dom element.
 | 
			
		||||
   *
 | 
			
		||||
   * This is shadow DOM compatible, but only works when selector is within
 | 
			
		||||
   * one shadow host, won't work if your selector is crossing
 | 
			
		||||
   * multiple shadow hosts.
 | 
			
		||||
   *
 | 
			
		||||
   */
 | 
			
		||||
  querySelector: (el, selector) => {
 | 
			
		||||
    let nodes = [el];
 | 
			
		||||
    let result = null;
 | 
			
		||||
    while (nodes.length) {
 | 
			
		||||
      const node = nodes.pop();
 | 
			
		||||
 | 
			
		||||
      // Skip if it's an invalid node.
 | 
			
		||||
      if (!node || !node.querySelector) continue;
 | 
			
		||||
 | 
			
		||||
      // Try find it with native querySelector directly
 | 
			
		||||
      result = node.querySelector(selector);
 | 
			
		||||
 | 
			
		||||
      if (result) {
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Add all nodes with shadowRoot and loop through
 | 
			
		||||
      const allShadowNodes = [...node.querySelectorAll('*')]
 | 
			
		||||
          .filter(child => !!child.shadowRoot)
 | 
			
		||||
          .map(child => child.shadowRoot);
 | 
			
		||||
      nodes = nodes.concat(allShadowNodes);
 | 
			
		||||
 | 
			
		||||
      // Add shadowRoot of current node if has one
 | 
			
		||||
      // as its not included in node.querySelectorAll('*')
 | 
			
		||||
      if (node.shadowRoot) {
 | 
			
		||||
        nodes.push(node.shadowRoot);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return result;
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Query selector all dom elements matching with certain selector.
 | 
			
		||||
   *
 | 
			
		||||
   * This is shadow DOM compatible, but only works when selector is within
 | 
			
		||||
   * one shadow host, won't work if your selector is crossing
 | 
			
		||||
   * multiple shadow hosts.
 | 
			
		||||
   *
 | 
			
		||||
   * Note: this can be very expensive, only use when have to.
 | 
			
		||||
   */
 | 
			
		||||
  querySelectorAll: (el, selector) => {
 | 
			
		||||
    let nodes = [el];
 | 
			
		||||
    const results = new Set();
 | 
			
		||||
    while (nodes.length) {
 | 
			
		||||
      const node = nodes.pop();
 | 
			
		||||
 | 
			
		||||
      if (!node || !node.querySelectorAll) continue;
 | 
			
		||||
 | 
			
		||||
      // Try find all from regular children
 | 
			
		||||
      [...node.querySelectorAll(selector)]
 | 
			
		||||
          .forEach(el => results.add(el));
 | 
			
		||||
 | 
			
		||||
      // Add all nodes with shadowRoot and loop through
 | 
			
		||||
      const allShadowNodes = [...node.querySelectorAll('*')]
 | 
			
		||||
          .filter(child => !!child.shadowRoot)
 | 
			
		||||
          .map(child => child.shadowRoot);
 | 
			
		||||
      nodes = nodes.concat(allShadowNodes);
 | 
			
		||||
 | 
			
		||||
      // Add shadowRoot of current node if has one
 | 
			
		||||
      // as its not included in node.querySelectorAll('*')
 | 
			
		||||
      if (node.shadowRoot) {
 | 
			
		||||
        nodes.push(node.shadowRoot);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return [...results];
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Retrieves the dom path of the current event.
 | 
			
		||||
   *
 | 
			
		||||
   * If the event object contains a `path` property, then use it,
 | 
			
		||||
   * otherwise, construct the dom path based on the event target.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {!Event} e
 | 
			
		||||
   * @return {string}
 | 
			
		||||
   * @example
 | 
			
		||||
   *
 | 
			
		||||
   * domNode.onclick = e => {
 | 
			
		||||
   *  getEventPath(e); // eg: div.class1>p#pid.class2
 | 
			
		||||
   * }
 | 
			
		||||
   */
 | 
			
		||||
  getEventPath: e => {
 | 
			
		||||
    if (!e) return '';
 | 
			
		||||
 | 
			
		||||
    let path = e.path;
 | 
			
		||||
    if (!path || !path.length) {
 | 
			
		||||
      path = [];
 | 
			
		||||
      let el = e.target;
 | 
			
		||||
      while (el) {
 | 
			
		||||
        path.push(el);
 | 
			
		||||
        el = el.parentNode || el.host;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return path.reduce((domPath, curEl) => {
 | 
			
		||||
      const pathForEl = getPathFromNode(curEl);
 | 
			
		||||
      if (!pathForEl) return domPath;
 | 
			
		||||
      return domPath ? `${pathForEl}>${domPath}` : pathForEl;
 | 
			
		||||
    }, '');
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,89 +0,0 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<!--
 | 
			
		||||
@license
 | 
			
		||||
Copyright (C) 2020 The Android Open Source Project
 | 
			
		||||
 | 
			
		||||
Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
you may not use this file except in compliance with the License.
 | 
			
		||||
You may obtain a copy of the License at
 | 
			
		||||
 | 
			
		||||
http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 | 
			
		||||
Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
See the License for the specific language governing permissions and
 | 
			
		||||
limitations under the License.
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
 | 
			
		||||
<meta charset="utf-8">
 | 
			
		||||
<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
 | 
			
		||||
<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
 | 
			
		||||
<script src="/components/wct-browser-legacy/browser.js"></script>
 | 
			
		||||
 | 
			
		||||
<test-fixture id="basic">
 | 
			
		||||
  <template>
 | 
			
		||||
    <div id="test" class="a b c">
 | 
			
		||||
      <a class="testBtn"></a>
 | 
			
		||||
    </div>
 | 
			
		||||
  </template>
 | 
			
		||||
</test-fixture>
 | 
			
		||||
 | 
			
		||||
<script type="module">
 | 
			
		||||
  import '../test/common-test-setup.js';
 | 
			
		||||
  import {util} from './util.js';
 | 
			
		||||
  suite('util tests', () => {
 | 
			
		||||
    suite('getEventPath', () => {
 | 
			
		||||
      test('empty event', () => {
 | 
			
		||||
        assert.equal(util.getEventPath(), '');
 | 
			
		||||
        assert.equal(util.getEventPath(null), '');
 | 
			
		||||
        assert.equal(util.getEventPath(undefined), '');
 | 
			
		||||
        assert.equal(util.getEventPath({}), '');
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      test('event with fake path', () => {
 | 
			
		||||
        assert.equal(util.getEventPath({path: []}), '');
 | 
			
		||||
        assert.equal(util.getEventPath({path: [
 | 
			
		||||
          {tagName: 'dd'},
 | 
			
		||||
        ]}), 'dd');
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      test('event with fake complicated path', () => {
 | 
			
		||||
        assert.equal(util.getEventPath({path: [
 | 
			
		||||
          {tagName: 'dd', id: 'test', className: 'a b'},
 | 
			
		||||
          {tagName: 'DIV', id: 'test2', className: 'a b c'},
 | 
			
		||||
        ]}), 'div#test2.a.b.c>dd#test.a.b');
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      test('event with fake target', () => {
 | 
			
		||||
        const fakeTargetParent2 = {
 | 
			
		||||
          tagName: 'DIV', id: 'test2', className: 'a b c',
 | 
			
		||||
        };
 | 
			
		||||
        const fakeTargetParent1 = {
 | 
			
		||||
          parentNode: fakeTargetParent2,
 | 
			
		||||
          tagName: 'dd',
 | 
			
		||||
          id: 'test',
 | 
			
		||||
          className: 'a b',
 | 
			
		||||
        };
 | 
			
		||||
        const fakeTarget = {tagName: 'SPAN', parentNode: fakeTargetParent1};
 | 
			
		||||
        assert.equal(
 | 
			
		||||
            util.getEventPath({target: fakeTarget}),
 | 
			
		||||
            'div#test2.a.b.c>dd#test.a.b>span'
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      test('event with real click', () => {
 | 
			
		||||
        const element = fixture('basic');
 | 
			
		||||
        const aLink = element.querySelector('a');
 | 
			
		||||
        let path;
 | 
			
		||||
        aLink.onclick = e => path = util.getEventPath(e);
 | 
			
		||||
        MockInteractions.click(aLink);
 | 
			
		||||
        assert.equal(
 | 
			
		||||
            path,
 | 
			
		||||
            'html>body>test-fixture#basic>div#test.a.b.c>a.testBtn'
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
@@ -245,7 +245,6 @@ const scripts = [
 | 
			
		||||
  'gr-group-suggestions-provider/gr-group-suggestions-provider_test.html',
 | 
			
		||||
  'gr-display-name-utils/gr-display-name-utils_test.html',
 | 
			
		||||
  'gr-email-suggestions-provider/gr-email-suggestions-provider_test.html',
 | 
			
		||||
  'util_test.html',
 | 
			
		||||
];
 | 
			
		||||
/* eslint-enable max-len */
 | 
			
		||||
for (let file of scripts) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										178
									
								
								polygerrit-ui/app/utils/dom-util.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								polygerrit-ui/app/utils/dom-util.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,178 @@
 | 
			
		||||
/**
 | 
			
		||||
 * @license
 | 
			
		||||
 * Copyright (C) 2020 The Android Open Source Project
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
function getPathFromNode(el) {
 | 
			
		||||
  if (!el.tagName || el.tagName === 'GR-APP'
 | 
			
		||||
        || el instanceof DocumentFragment
 | 
			
		||||
        || el instanceof HTMLSlotElement) {
 | 
			
		||||
    return '';
 | 
			
		||||
  }
 | 
			
		||||
  let path = el.tagName.toLowerCase();
 | 
			
		||||
  if (el.id) path += `#${el.id}`;
 | 
			
		||||
  if (el.className) path += `.${el.className.replace(/ /g, '.')}`;
 | 
			
		||||
  return path;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Get computed style value.
 | 
			
		||||
 *
 | 
			
		||||
 * If ShadyCSS is provided, use ShadyCSS api.
 | 
			
		||||
 * If `getComputedStyleValue` is provided on the element, use it.
 | 
			
		||||
 * Otherwise fallback to native method (in polymer 2).
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
export function getComputedStyleValue(name, el) {
 | 
			
		||||
  let style;
 | 
			
		||||
  if (window.ShadyCSS) {
 | 
			
		||||
    style = ShadyCSS.getComputedStyleValue(el, name);
 | 
			
		||||
  } else if (el.getComputedStyleValue) {
 | 
			
		||||
    style = el.getComputedStyleValue(name);
 | 
			
		||||
  } else {
 | 
			
		||||
    style = getComputedStyle(el).getPropertyValue(name);
 | 
			
		||||
  }
 | 
			
		||||
  return style;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Query selector on a dom element.
 | 
			
		||||
 *
 | 
			
		||||
 * This is shadow DOM compatible, but only works when selector is within
 | 
			
		||||
 * one shadow host, won't work if your selector is crossing
 | 
			
		||||
 * multiple shadow hosts.
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
export function querySelector(el, selector) {
 | 
			
		||||
  let nodes = [el];
 | 
			
		||||
  let result = null;
 | 
			
		||||
  while (nodes.length) {
 | 
			
		||||
    const node = nodes.pop();
 | 
			
		||||
 | 
			
		||||
    // Skip if it's an invalid node.
 | 
			
		||||
    if (!node || !node.querySelector) continue;
 | 
			
		||||
 | 
			
		||||
    // Try find it with native querySelector directly
 | 
			
		||||
    result = node.querySelector(selector);
 | 
			
		||||
 | 
			
		||||
    if (result) {
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Add all nodes with shadowRoot and loop through
 | 
			
		||||
    const allShadowNodes = [...node.querySelectorAll('*')]
 | 
			
		||||
        .filter(child => !!child.shadowRoot)
 | 
			
		||||
        .map(child => child.shadowRoot);
 | 
			
		||||
    nodes = nodes.concat(allShadowNodes);
 | 
			
		||||
 | 
			
		||||
    // Add shadowRoot of current node if has one
 | 
			
		||||
    // as its not included in node.querySelectorAll('*')
 | 
			
		||||
    if (node.shadowRoot) {
 | 
			
		||||
      nodes.push(node.shadowRoot);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Query selector all dom elements matching with certain selector.
 | 
			
		||||
 *
 | 
			
		||||
 * This is shadow DOM compatible, but only works when selector is within
 | 
			
		||||
 * one shadow host, won't work if your selector is crossing
 | 
			
		||||
 * multiple shadow hosts.
 | 
			
		||||
 *
 | 
			
		||||
 * Note: this can be very expensive, only use when have to.
 | 
			
		||||
 */
 | 
			
		||||
export function querySelectorAll(el, selector) {
 | 
			
		||||
  let nodes = [el];
 | 
			
		||||
  const results = new Set();
 | 
			
		||||
  while (nodes.length) {
 | 
			
		||||
    const node = nodes.pop();
 | 
			
		||||
 | 
			
		||||
    if (!node || !node.querySelectorAll) continue;
 | 
			
		||||
 | 
			
		||||
    // Try find all from regular children
 | 
			
		||||
    [...node.querySelectorAll(selector)]
 | 
			
		||||
        .forEach(el => results.add(el));
 | 
			
		||||
 | 
			
		||||
    // Add all nodes with shadowRoot and loop through
 | 
			
		||||
    const allShadowNodes = [...node.querySelectorAll('*')]
 | 
			
		||||
        .filter(child => !!child.shadowRoot)
 | 
			
		||||
        .map(child => child.shadowRoot);
 | 
			
		||||
    nodes = nodes.concat(allShadowNodes);
 | 
			
		||||
 | 
			
		||||
    // Add shadowRoot of current node if has one
 | 
			
		||||
    // as its not included in node.querySelectorAll('*')
 | 
			
		||||
    if (node.shadowRoot) {
 | 
			
		||||
      nodes.push(node.shadowRoot);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return [...results];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Retrieves the dom path of the current event.
 | 
			
		||||
 *
 | 
			
		||||
 * If the event object contains a `path` property, then use it,
 | 
			
		||||
 * otherwise, construct the dom path based on the event target.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {!Event} e
 | 
			
		||||
 * @return {string}
 | 
			
		||||
 * @example
 | 
			
		||||
 *
 | 
			
		||||
 * domNode.onclick = e => {
 | 
			
		||||
 *  getEventPath(e); // eg: div.class1>p#pid.class2
 | 
			
		||||
 * }
 | 
			
		||||
 */
 | 
			
		||||
export function getEventPath(e) {
 | 
			
		||||
  if (!e) return '';
 | 
			
		||||
 | 
			
		||||
  let path = e.path;
 | 
			
		||||
  if (!path || !path.length) {
 | 
			
		||||
    path = [];
 | 
			
		||||
    let el = e.target;
 | 
			
		||||
    while (el) {
 | 
			
		||||
      path.push(el);
 | 
			
		||||
      el = el.parentNode || el.host;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return path.reduce((domPath, curEl) => {
 | 
			
		||||
    const pathForEl = getPathFromNode(curEl);
 | 
			
		||||
    if (!pathForEl) return domPath;
 | 
			
		||||
    return domPath ? `${pathForEl}>${domPath}` : pathForEl;
 | 
			
		||||
  }, '');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Are any ancestors of the element (or the element itself) members of the
 | 
			
		||||
 * given class.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {!Element} element
 | 
			
		||||
 * @param {string} className
 | 
			
		||||
 * @param {Element=} opt_stopElement If provided, stop traversing the
 | 
			
		||||
 *     ancestry when the stop element is reached. The stop element's class
 | 
			
		||||
 *     is not checked.
 | 
			
		||||
 * @return {boolean}
 | 
			
		||||
 */
 | 
			
		||||
export function descendedFromClass(element, className, opt_stopElement) {
 | 
			
		||||
  let isDescendant = element.classList.contains(className);
 | 
			
		||||
  while (!isDescendant && element.parentElement &&
 | 
			
		||||
        (!opt_stopElement || element.parentElement !== opt_stopElement)) {
 | 
			
		||||
    isDescendant = element.classList.contains(className);
 | 
			
		||||
    element = element.parentElement;
 | 
			
		||||
  }
 | 
			
		||||
  return isDescendant;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										139
									
								
								polygerrit-ui/app/utils/dom-util_test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								polygerrit-ui/app/utils/dom-util_test.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,139 @@
 | 
			
		||||
/**
 | 
			
		||||
 * @license
 | 
			
		||||
 * Copyright (C) 2020 The Android Open Source Project
 | 
			
		||||
 *
 | 
			
		||||
 * Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
 * you may not use this file except in compliance with the License.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 * http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
import '../test/common-test-setup-karma.js';
 | 
			
		||||
import {getComputedStyleValue, querySelector, querySelectorAll, descendedFromClass, getEventPath} from './dom-util.js';
 | 
			
		||||
import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 | 
			
		||||
import {html} from '@polymer/polymer/lib/utils/html-tag.js';
 | 
			
		||||
 | 
			
		||||
class TestEle extends PolymerElement {
 | 
			
		||||
  static get is() {
 | 
			
		||||
    return 'dom-util-test-element';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static get template() {
 | 
			
		||||
    return html`
 | 
			
		||||
    <div>
 | 
			
		||||
      <div class="a">
 | 
			
		||||
        <div class="b">
 | 
			
		||||
          <div class="c"></div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <span class="ss"></span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <span class="ss"></span>
 | 
			
		||||
    </div>
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
customElements.define(TestEle.is, TestEle);
 | 
			
		||||
 | 
			
		||||
const basicFixture = fixtureFromTemplate(html`
 | 
			
		||||
  <div id="test" class="a b c">
 | 
			
		||||
    <a class="testBtn" style="color:red;"></a>
 | 
			
		||||
    <dom-util-test-element></dom-util-test-element>
 | 
			
		||||
    <span class="ss"></span>
 | 
			
		||||
  </div>
 | 
			
		||||
`);
 | 
			
		||||
 | 
			
		||||
suite('dom-util tests', () => {
 | 
			
		||||
  suite('getEventPath', () => {
 | 
			
		||||
    test('empty event', () => {
 | 
			
		||||
      assert.equal(getEventPath(), '');
 | 
			
		||||
      assert.equal(getEventPath(null), '');
 | 
			
		||||
      assert.equal(getEventPath(undefined), '');
 | 
			
		||||
      assert.equal(getEventPath({}), '');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('event with fake path', () => {
 | 
			
		||||
      assert.equal(getEventPath({path: []}), '');
 | 
			
		||||
      assert.equal(getEventPath({path: [
 | 
			
		||||
        {tagName: 'dd'},
 | 
			
		||||
      ]}), 'dd');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('event with fake complicated path', () => {
 | 
			
		||||
      assert.equal(getEventPath({path: [
 | 
			
		||||
        {tagName: 'dd', id: 'test', className: 'a b'},
 | 
			
		||||
        {tagName: 'DIV', id: 'test2', className: 'a b c'},
 | 
			
		||||
      ]}), 'div#test2.a.b.c>dd#test.a.b');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('event with fake target', () => {
 | 
			
		||||
      const fakeTargetParent2 = {
 | 
			
		||||
        tagName: 'DIV', id: 'test2', className: 'a b c',
 | 
			
		||||
      };
 | 
			
		||||
      const fakeTargetParent1 = {
 | 
			
		||||
        parentNode: fakeTargetParent2,
 | 
			
		||||
        tagName: 'dd',
 | 
			
		||||
        id: 'test',
 | 
			
		||||
        className: 'a b',
 | 
			
		||||
      };
 | 
			
		||||
      const fakeTarget = {tagName: 'SPAN', parentNode: fakeTargetParent1};
 | 
			
		||||
      assert.equal(
 | 
			
		||||
          getEventPath({target: fakeTarget}),
 | 
			
		||||
          'div#test2.a.b.c>dd#test.a.b>span'
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('event with real click', () => {
 | 
			
		||||
      const element = basicFixture.instantiate();
 | 
			
		||||
      const aLink = element.querySelector('a');
 | 
			
		||||
      let path;
 | 
			
		||||
      aLink.onclick = e => path = getEventPath(e);
 | 
			
		||||
      MockInteractions.click(aLink);
 | 
			
		||||
      assert.equal(
 | 
			
		||||
          path,
 | 
			
		||||
          `html>body>test-fixture#${basicFixture.fixtureId}>` +
 | 
			
		||||
          'div#test.a.b.c>a.testBtn'
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  suite('querySelector and querySelectorAll', () => {
 | 
			
		||||
    test('query cross shadow dom', () => {
 | 
			
		||||
      const element = basicFixture.instantiate();
 | 
			
		||||
      const theFirstEl = querySelector(element, '.ss');
 | 
			
		||||
      const allEls = querySelectorAll(element, '.ss');
 | 
			
		||||
      assert.equal(allEls.length, 3);
 | 
			
		||||
      assert.equal(theFirstEl, allEls[0]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  suite('getComputedStyleValue', () => {
 | 
			
		||||
    test('color style', () => {
 | 
			
		||||
      const element = basicFixture.instantiate();
 | 
			
		||||
      const testBtn = querySelector(element, '.testBtn');
 | 
			
		||||
      assert.equal(
 | 
			
		||||
          getComputedStyleValue('color', testBtn), 'rgb(255, 0, 0)'
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  suite('descendedFromClass', () => {
 | 
			
		||||
    test('basic tests', () => {
 | 
			
		||||
      const element = basicFixture.instantiate();
 | 
			
		||||
      const testEl = querySelector(element, 'dom-util-test-element');
 | 
			
		||||
      // .c is a child of .a and not vice versa.
 | 
			
		||||
      assert.isTrue(descendedFromClass(querySelector(testEl, '.c'), 'a'));
 | 
			
		||||
      assert.isFalse(descendedFromClass(querySelector(testEl, '.a'), 'c'));
 | 
			
		||||
 | 
			
		||||
      // Stops at stop element.
 | 
			
		||||
      assert.isFalse(descendedFromClass(querySelector(testEl, '.c'), 'a',
 | 
			
		||||
          querySelector(testEl, '.b')));
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
		Reference in New Issue
	
	Block a user