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:
Tao Zhou
2020-06-10 15:48:38 +02:00
parent 177fdf46b8
commit 7b7d16ba16
11 changed files with 333 additions and 248 deletions

View File

@@ -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);
},
};

View File

@@ -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

View File

@@ -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';

View File

@@ -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 => {

View File

@@ -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', () => {

View File

@@ -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) {

View File

@@ -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;
}, '');
},
};

View File

@@ -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>

View File

@@ -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) {

View 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;
}

View 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')));
});
});
});