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.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import {descendedFromClass} from '../../utils/dom-util.js';
|
||||||
|
|
||||||
export const DomUtilBehavior = {
|
export const DomUtilBehavior = {
|
||||||
/**
|
/**
|
||||||
* Are any ancestors of the element (or the element itself) members of the
|
* Are any ancestors of the element (or the element itself) members of the
|
||||||
@@ -28,13 +30,9 @@ export const DomUtilBehavior = {
|
|||||||
* @return {boolean}
|
* @return {boolean}
|
||||||
*/
|
*/
|
||||||
descendedFromClass(element, className, opt_stopElement) {
|
descendedFromClass(element, className, opt_stopElement) {
|
||||||
let isDescendant = element.classList.contains(className);
|
console.warn('DomUtilBehavior is deprecated.' +
|
||||||
while (!isDescendant && element.parentElement &&
|
'Use descendedFromClass from utils directly.');
|
||||||
(!opt_stopElement || element.parentElement !== opt_stopElement)) {
|
return descendedFromClass(element, className, opt_stopElement);
|
||||||
isDescendant = element.classList.contains(className);
|
|
||||||
element = element.parentElement;
|
|
||||||
}
|
|
||||||
return isDescendant;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-beh
|
|||||||
import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
|
import {RESTClientBehavior} from '../../../behaviors/rest-client-behavior/rest-client-behavior.js';
|
||||||
import {GrEditConstants} from '../../edit/gr-edit-constants.js';
|
import {GrEditConstants} from '../../edit/gr-edit-constants.js';
|
||||||
import {GrCountStringFormatter} from '../../shared/gr-count-string-formatter/gr-count-string-formatter.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 {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
|
||||||
import {pluginEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.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';
|
import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.js';
|
||||||
@@ -2003,7 +2003,7 @@ class GrChangeView extends mixinBehaviors( [
|
|||||||
_computeShowRelatedToggle() {
|
_computeShowRelatedToggle() {
|
||||||
// Make sure the max height has been applied, since there is now content
|
// Make sure the max height has been applied, since there is now content
|
||||||
// to populate.
|
// to populate.
|
||||||
if (!util.getComputedStyleValue('--relation-chain-max-height', this)) {
|
if (!getComputedStyleValue('--relation-chain-max-height', this)) {
|
||||||
this._updateRelatedChangeMaxHeight();
|
this._updateRelatedChangeMaxHeight();
|
||||||
}
|
}
|
||||||
// Prevents showMore from showing when click on related change, since the
|
// 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 {KeyboardShortcutBinder} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.js';
|
||||||
import {GrEditConstants} from '../../edit/gr-edit-constants.js';
|
import {GrEditConstants} from '../../edit/gr-edit-constants.js';
|
||||||
import {_testOnly_resetEndpoints} from '../../shared/gr-js-api-interface/gr-plugin-endpoints.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 {GerritNav} from '../../core/gr-navigation/gr-navigation.js';
|
||||||
import {pluginLoader} from '../../shared/gr-js-api-interface/gr-plugin-loader.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';
|
import {_testOnly_initGerritPluginApi} from '../../shared/gr-js-api-interface/gr-gerrit.js';
|
||||||
@@ -320,7 +320,7 @@ suite('gr-change-view tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getCustomCssValue =
|
const getCustomCssValue =
|
||||||
cssParam => util.getComputedStyleValue(cssParam, element);
|
cssParam => getComputedStyleValue(cssParam, element);
|
||||||
|
|
||||||
test('_handleMessageAnchorTap', () => {
|
test('_handleMessageAnchorTap', () => {
|
||||||
element._changeNum = '1';
|
element._changeNum = '1';
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {PolymerElement} from '@polymer/polymer/polymer-element.js';
|
|||||||
import {htmlTemplate} from './gr-diff-selection_html.js';
|
import {htmlTemplate} from './gr-diff-selection_html.js';
|
||||||
import {DomUtilBehavior} from '../../../behaviors/dom-util-behavior/dom-util-behavior.js';
|
import {DomUtilBehavior} from '../../../behaviors/dom-util-behavior/dom-util-behavior.js';
|
||||||
import {GrRangeNormalizer} from '../gr-diff-highlight/gr-range-normalizer.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/
|
* Possible CSS classes indicating the state of selection. Dynamically added/
|
||||||
@@ -203,7 +203,7 @@ class GrDiffSelection extends mixinBehaviors( [
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getSelection() {
|
_getSelection() {
|
||||||
const diffHosts = util.querySelectorAll(document.body, 'gr-diff');
|
const diffHosts = querySelectorAll(document.body, 'gr-diff');
|
||||||
if (!diffHosts.length) return window.getSelection();
|
if (!diffHosts.length) return window.getSelection();
|
||||||
|
|
||||||
const curDiffHost = diffHosts.find(diffHost => {
|
const curDiffHost = diffHosts.find(diffHost => {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {getMockDiffResponse} from '../../../test/mocks/diff-response.js';
|
|||||||
import './gr-diff.js';
|
import './gr-diff.js';
|
||||||
import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
|
import {dom, flush} from '@polymer/polymer/lib/legacy/polymer.dom.js';
|
||||||
import {GrDiffBuilderImage} from '../gr-diff-builder/gr-diff-builder-image.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 {_setHiddenScroll} from '../../../scripts/hiddenscroll.js';
|
||||||
import {runA11yAudit} from '../../../test/a11y-test-utils.js';
|
import {runA11yAudit} from '../../../test/a11y-test-utils.js';
|
||||||
import '@polymer/paper-button/paper-button.js';
|
import '@polymer/paper-button/paper-button.js';
|
||||||
@@ -82,14 +82,14 @@ suite('gr-diff tests', () => {
|
|||||||
element = basicFixture.instantiate();
|
element = basicFixture.instantiate();
|
||||||
element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: true});
|
element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: true});
|
||||||
flushAsynchronousOperations();
|
flushAsynchronousOperations();
|
||||||
assert.equal(util.getComputedStyleValue('--line-limit', element), '80ch');
|
assert.equal(getComputedStyleValue('--line-limit', element), '80ch');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('line limit without line_wrapping', () => {
|
test('line limit without line_wrapping', () => {
|
||||||
element = basicFixture.instantiate();
|
element = basicFixture.instantiate();
|
||||||
element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: false});
|
element.prefs = Object.assign({}, MINIMAL_PREFS, {line_wrapping: false});
|
||||||
flushAsynchronousOperations();
|
flushAsynchronousOperations();
|
||||||
assert.isNotOk(util.getComputedStyleValue('--line-limit', element));
|
assert.isNotOk(getComputedStyleValue('--line-limit', element));
|
||||||
});
|
});
|
||||||
|
|
||||||
suite('_get{PatchNum|IsParentComment}ByLineAndContent', () => {
|
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 {htmlTemplate} from './gr-button_html.js';
|
||||||
import {TooltipBehavior} from '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
|
import {TooltipBehavior} from '../../../behaviors/gr-tooltip-behavior/gr-tooltip-behavior.js';
|
||||||
import {KeyboardShortcutBehavior} from '../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-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';
|
import {appContext} from '../../../services/app-context.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -113,7 +113,7 @@ class GrButton extends mixinBehaviors( [
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.reporting.reportInteraction('button-click',
|
this.reporting.reportInteraction('button-click',
|
||||||
{path: util.getEventPath(e)});
|
{path: getEventPath(e)});
|
||||||
}
|
}
|
||||||
|
|
||||||
_disabledChanged(disabled) {
|
_disabledChanged(disabled) {
|
||||||
|
|||||||
@@ -15,17 +15,6 @@
|
|||||||
* limitations under the License.
|
* 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
|
// TODO (dmfilippov): Each function must be exported separately. According to
|
||||||
// the code style guide, a namespacing is not allowed.
|
// the code style guide, a namespacing is not allowed.
|
||||||
export const util = {
|
export const util = {
|
||||||
@@ -76,133 +65,4 @@ export const util = {
|
|||||||
};
|
};
|
||||||
return wrappedPromise;
|
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-group-suggestions-provider/gr-group-suggestions-provider_test.html',
|
||||||
'gr-display-name-utils/gr-display-name-utils_test.html',
|
'gr-display-name-utils/gr-display-name-utils_test.html',
|
||||||
'gr-email-suggestions-provider/gr-email-suggestions-provider_test.html',
|
'gr-email-suggestions-provider/gr-email-suggestions-provider_test.html',
|
||||||
'util_test.html',
|
|
||||||
];
|
];
|
||||||
/* eslint-enable max-len */
|
/* eslint-enable max-len */
|
||||||
for (let file of scripts) {
|
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