Deprecate Gerrit.css() and provide a replacement

Gerrit.css doesn't work with ShadowDom, because it adds styles to a
document. This fix provides a replacement for Gerrit.css. The
replacement allows to apply the same styles to any element inside
document.

Bug: Issue 11298
Change-Id: Ide325a889a69bd267382a9664ed6f8d25ed67a3b
This commit is contained in:
Dmitrii Filippov 2019-08-22 15:52:37 +00:00
parent 54ca082f42
commit 941ee630d8
13 changed files with 398 additions and 128 deletions

View File

@ -713,10 +713,12 @@ accessed through this name.
[[Gerrit_css]]
=== Gerrit.css()
[WARNING]
This method is deprecated. It doesn't work with Shadow DOM and
will be removed in the future. Please, use link:pg-plugin-dev.html#plugin-styles[plugin.styles] instead.
Creates a new unique CSS class and injects it into the document.
The name of the class is returned and can be used by the plugin.
See link:#Gerrit_html[`Gerrit.html()`] for an easy way to use
generated class names.
Classes created with this function should be created once at install
time and reused throughout the plugin. Repeatedly creating the same
@ -814,112 +816,6 @@ If the URL passed matches `http://...`, `https://...`, or `//...`
the current browser window will navigate to the non-Gerrit URL.
The user can return to Gerrit with the back button.
[[Gerrit_html]]
=== Gerrit.html()
Parses an HTML fragment after performing template replacements. If
the HTML has a single root element or node that node is returned,
otherwise it is wrapped inside a `<div>` and the div is returned.
.Signature
[source,javascript]
----
Gerrit.html(htmlText, options, wantElements);
----
* htmlText: string of HTML to be parsed. A new unattached `<div>` is
created in the browser's document and the innerHTML property is
assigned to the passed string, after performing replacements. If
the div has exactly one child, that child will be returned instead
of the div.
* options: optional object reference supplying replacements for any
`{name}` references in htmlText. Navigation through objects is
supported permitting `{style.bar}` to be replaced with `"foo"` if
options was `{style: {bar: "foo"}}`. Value replacements are HTML
escaped before being inserted into the document fragment.
* wantElements: if options is given and wantElements is also true
an object consisting of `{root: parsedElement, elements: {...}}` is
returned instead of the parsed element. The elements object contains
a property for each element using `id={name}` in htmlText.
.Example
[source,javascript]
----
var style = {bar: Gerrit.css('background: yellow')};
Gerrit.html(
'<span class="{style.bar}">Hello {name}!</span>',
{style: style, name: "World"});
----
Event handlers can be automatically attached to elements referenced
through an attribute id. Object navigation is not supported for ids,
and the parser strips the id attribute before returning the result.
Handler functions must begin with `on` and be a function to be
installed on the element. This approach is useful for onclick and
other handlers that do not want to create circular references that
will eventually leak browser memory.
.Example
[source,javascript]
----
var options = {
link: {
onclick: function(e) { window.close() },
},
};
Gerrit.html('<a href="javascript:;" id="{link}">Close</a>', options);
----
When using options to install handlers care must be taken to not
accidentally include the returned element into the event handler's
closure. This is why options is built before calling `Gerrit.html()`
and not inline as a shown above with "Hello World".
DOM nodes can optionally be returned, allowing handlers to access the
elements identified by `id={name}` at a later point in time.
.Example
[source,javascript]
----
var w = Gerrit.html(
'<div>Name: <input type="text" id="{name}"></div>'
+ '<div>Age: <input type="text" id="{age}"></div>'
+ '<button id="{submit}"><div>Save</div></button>',
{
submit: {
onclick: function(s) {
var e = w.elements;
window.alert(e.name.value + " is " + e.age.value);
},
},
}, true);
----
To prevent memory leaks `w.root` and `w.elements` should be set to
null when the elements are no longer necessary. Screens can use
link:#screen_onUnload[screen.onUnload()] to define a callback function
to perform this cleanup:
[source,javascript]
----
var w = Gerrit.html(...);
screen.body.appendElement(w.root);
screen.onUnload(function() { w.clear() });
----
[[Gerrit_injectCss]]
=== Gerrit.injectCss()
Injects CSS rules into the document by appending onto the end of the
existing rule list. CSS rules are global to the entire application
and must be manually scoped by each plugin. For an automatic scoping
alternative see link:#Gerrit_css[`css()`].
[source,javascript]
----
Gerrit.injectCss('.myplugin_bg {background: #000}');
----
[[Gerrit_install]]
=== Gerrit.install()
Registers a new plugin by invoking the supplied initialization

View File

@ -360,6 +360,16 @@ screen.
Deprecated. Use link:#plugin-settings[`plugin.settings()`] instead.
[[plugin-styles]]
=== styles
`plugin.styles()`
.Params:
- none
.Returns:
- Instance of link:pg-plugin-styles-api.html[GrStylesApi]
=== changeMetadata
`plugin.changeMetadata()`
@ -372,6 +382,7 @@ Deprecated. Use link:#plugin-settings[`plugin.settings()`] instead.
=== theme
`plugin.theme()`
Note: TODO
=== url

View File

@ -0,0 +1,33 @@
= Gerrit Code Review - GrStyleObject
Store information about css style properties. You can't create this object
directly. Instead you should use the link:pg-plugin-styles-api.html#css[css] method.
This object allows to apply style correctly to elements within different shadow
subtree.
[[get-class-name]]
== getClassName
`styleObject.getClassName(element)`
.Params
- `element` - an HTMLElement.
.Returns
- `string` - class name. The class name is valid only within the shadow root of `element`.
Creates a new unique CSS class and injects it into the appropriate place
in DOM (it can be document or shadow root for element). This class can be later
added to the element or to any other element in the same shadow root. It is guarantee,
that method adds CSS class only once for each shadow root.
== apply
`styleObject.apply(element)`
.Params
- `element` - element to apply style.
Create a new unique CSS class (see link:#get-class-name[getClassName]) and
adds class to the element.

View File

@ -0,0 +1,29 @@
= Gerrit Code Review - Plugin styles API
This API is provided by link:pg-plugin-dev.html#plugin-styles[plugin.styles()]
and provides a way to apply dynamically created styles to elements in a
document.
[[css]]
== css
`styles.css(rulesStr)`
.Params
- `*string* rulesStr` string with CSS styling declarations.
Example:
----
const styleObject = plugin.styles().css('background: black; color: white;');
...
const className = styleObject.getClassName(element)
...
element.classList.add(className);
...
styleObject.apply(someOtherElement);
----
.Returns
- Instance of link:pg-plugin-style-object.html[GrStyleObject].

View File

@ -0,0 +1,18 @@
<!--
@license
Copyright (C) 2019 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.
-->
<script src="gr-styles-api.js"></script>

View File

@ -0,0 +1,77 @@
/**
* @license
* Copyright (C) 2019 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(window) {
'use strict';
// Prevent redefinition.
if (window.GrStylesApi) { return; }
let styleObjectCount = 0;
function GrStyleObject(rulesStr) {
this._rulesStr = rulesStr;
this._className = `__pg_js_api_class_${styleObjectCount}`;
styleObjectCount++;
}
/**
* Creates a new unique CSS class and injects it in a root node of the element
* if it hasn't been added yet. A root node is an document or is the
* associated shadowRoot. This class can be added to any element with the same
* root node.
* @param {HTMLElement} element The element to get class name for.
* @return {string} Appropriate class name for the element is returned
*/
GrStyleObject.prototype.getClassName = function(element) {
const rootNode = Polymer.Settings.useShadow
? element.getRootNode() : document.body;
if (!rootNode.__pg_js_api_style_tags) {
rootNode.__pg_js_api_style_tags = {};
}
if (!rootNode.__pg_js_api_style_tags[this._className]) {
const styleTag = document.createElement('style');
styleTag.innerHTML = `.${this._className} { ${this._rulesStr} }`;
rootNode.appendChild(styleTag);
rootNode.__pg_js_api_style_tags[this._className] = true;
}
return this._className;
};
/**
* Apply shared style to the element.
* @param {HTMLElement} element The element to apply style for
*/
GrStyleObject.prototype.apply = function(element) {
element.classList.add(this.getClassName(element));
};
function GrStylesApi() {
}
/**
* Creates a new GrStyleObject with specified style properties.
* @param {string} String with style properties.
* @return {GrStyleObject}
*/
GrStylesApi.prototype.css = function(ruleStr) {
return new GrStyleObject(ruleStr);
};
window.GrStylesApi = GrStylesApi;
})(window);

View File

@ -0,0 +1,182 @@
<!DOCTYPE html>
<!--
@license
Copyright (C) 2019 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">
<title>gr-admin-api</title>
<script src="/test/common-test-setup.js"></script>
<script src="/bower_components/webcomponentsjs/custom-elements-es5-adapter.js"></script>
<script src="/bower_components/webcomponentsjs/webcomponents-lite.js"></script>
<script src="/bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="../../shared/gr-js-api-interface/gr-js-api-interface.html">
<link rel="import" href="gr-styles-api.html">
<script>void(0);</script>
<dom-module id="gr-style-test-element">
<template>
<div id="wrapper"></div>
</template>
<script>Polymer({is: 'gr-style-test-element'});</script>
</dom-module>
<script>
suite('gr-styles-api tests', () => {
let sandbox;
let stylesApi;
setup(() => {
sandbox = sinon.sandbox.create();
let plugin;
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
stylesApi = plugin.styles();
});
teardown(() => {
stylesApi = null;
sandbox.restore();
});
test('exists', () => {
assert.isOk(stylesApi);
});
test('css', () => {
const styleObject = stylesApi.css('background: red');
assert.isDefined(styleObject);
});
});
suite('GrStyleObject tests', () => {
let sandbox;
let stylesApi;
let displayInlineStyle;
let displayNoneStyle;
setup(() => {
sandbox = sinon.sandbox.create();
let plugin;
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
stylesApi = plugin.styles();
displayInlineStyle = stylesApi.css('display: inline');
displayNoneStyle = stylesApi.css('display: none');
});
teardown(() => {
displayInlineStyle = null;
displayNoneStyle = null;
stylesApi = null;
sandbox.restore();
});
function createNestedElements(parentElement) {
/* parentElement
* |--- element1
* |--- element2
* |--- element3
**/
const element1 = document.createElement('div');
const element2 = document.createElement('div');
const element3 = document.createElement('div');
Polymer.dom(parentElement).appendChild(element1);
Polymer.dom(parentElement).appendChild(element2);
Polymer.dom(element2).appendChild(element3);
return [element1, element2, element3];
}
test('getClassName - body level elements', () => {
const bodyLevelElements = createNestedElements(document.body);
testGetClassName(bodyLevelElements);
});
test('getClassName - elements inside polymer element', () => {
const polymerElement = document.createElement('gr-style-test-element');
Polymer.dom(document.body).appendChild(polymerElement);
const contentElements = createNestedElements(polymerElement.$.wrapper);
testGetClassName(contentElements);
});
function testGetClassName(elements) {
assertAllElementsHaveDefaultStyle(elements);
const className1 = displayInlineStyle.getClassName(elements[0]);
const className2 = displayNoneStyle.getClassName(elements[1]);
const className3 = displayInlineStyle.getClassName(elements[2]);
assert.notEqual(className2, className1);
assert.equal(className3, className1);
assertAllElementsHaveDefaultStyle(elements);
elements[0].classList.add(className1);
elements[1].classList.add(className2);
elements[2].classList.add(className1);
assertDisplayPropertyValues(elements, ['inline', 'none', 'inline']);
}
test('apply - body level elements', () => {
const bodyLevelElements = createNestedElements(document.body);
testApply(bodyLevelElements);
});
test('apply - elements inside polymer element', () => {
const polymerElement = document.createElement('gr-style-test-element');
Polymer.dom(document.body).appendChild(polymerElement);
const contentElements = createNestedElements(polymerElement.$.wrapper);
testApply(contentElements);
});
function testApply(elements) {
assertAllElementsHaveDefaultStyle(elements);
displayInlineStyle.apply(elements[0]);
displayNoneStyle.apply(elements[1]);
displayInlineStyle.apply(elements[2]);
assertDisplayPropertyValues(elements, ['inline', 'none', 'inline']);
}
function assertAllElementsHaveDefaultStyle(elements) {
for (const element of elements) {
assert.equal(getComputedStyle(element).getPropertyValue('display'),
'block');
}
}
function assertDisplayPropertyValues(elements, expectedDisplayValues) {
for (const key in elements) {
if (elements.hasOwnProperty(key)) {
assert.equal(
getComputedStyle(elements[key]).getPropertyValue('display'),
expectedDisplayValues[key]);
}
}
}
});
</script>

View File

@ -43,25 +43,26 @@
* Method to add annotations to a content line.
* @param {number} offset The char offset where the update starts.
* @param {number} length The number of chars that the update covers.
* @param {string} cssClass The name of a CSS class created using Gerrit.css.
* @param {GrStyleObject} styleObject The style object for the range.
* @param {string} side The side of the update. ('left' or 'right')
*/
GrAnnotationActionsContext.prototype.annotateRange = function(
offset, length, cssClass, side) {
offset, length, styleObject, side) {
if (this._contentEl && this._contentEl.getAttribute('data-side') == side) {
GrAnnotation.annotateElement(this._contentEl, offset, length, cssClass);
GrAnnotation.annotateElement(this._contentEl, offset, length,
styleObject.getClassName(this._contentEl));
}
};
/**
* Method to add a CSS class to the line number TD element.
* @param {string} cssClass The name of a CSS class created using Gerrit.css.
* @param {GrStyleObject} styleObject The style object for the range.
* @param {string} side The side of the update. ('left' or 'right')
*/
GrAnnotationActionsContext.prototype.annotateLineNumber = function(
cssClass, side) {
styleObject, side) {
if (this._lineNumberEl && this._lineNumberEl.classList.contains(side)) {
this._lineNumberEl.classList.add(cssClass);
styleObject.apply(this._lineNumberEl);
}
};

View File

@ -42,9 +42,13 @@ limitations under the License.
let sandbox;
let el;
let lineNumberEl;
let plugin;
setup(() => {
sandbox = sinon.sandbox.create();
Gerrit.install(p => { plugin = p; }, '0.1',
'http://test.com/plugins/testplugin/static/test.js');
const str = 'lorem ipsum blah blah';
const line = {text: str};
el = document.createElement('div');
@ -64,32 +68,34 @@ limitations under the License.
annotateElementSpy = sandbox.spy(GrAnnotation, 'annotateElement');
const start = 0;
const end = 100;
const cssClass = Gerrit.css('background-color: #000000');
const cssStyleObject = plugin.styles().css('background-color: #000000');
// Assert annotateElement is not called when side is different.
instance.annotateRange(start, end, cssClass, 'left');
instance.annotateRange(start, end, cssStyleObject, 'left');
assert.equal(annotateElementSpy.callCount, 0);
// Assert annotateElement is called once when side is the same.
instance.annotateRange(start, end, cssClass, 'right');
instance.annotateRange(start, end, cssStyleObject, 'right');
assert.equal(annotateElementSpy.callCount, 1);
const args = annotateElementSpy.getCalls()[0].args;
assert.equal(args[0], el);
assert.equal(args[1], start);
assert.equal(args[2], end);
assert.equal(args[3], cssClass);
assert.equal(args[3], cssStyleObject.getClassName(el));
});
test('test annotateLineNumber', () => {
const cssClass = Gerrit.css('background-color: #000000');
const cssStyleObject = plugin.styles().css('background-color: #000000');
const className = cssStyleObject.getClassName(lineNumberEl);
// Assert that css class is *not* applied when side is different.
instance.annotateLineNumber(cssClass, 'left');
assert.isFalse(lineNumberEl.classList.contains(cssClass));
instance.annotateLineNumber(cssStyleObject, 'left');
assert.isFalse(lineNumberEl.classList.contains(className));
// Assert that css class is applied when side is the same.
instance.annotateLineNumber(cssClass, 'right');
assert.isTrue(lineNumberEl.classList.contains(cssClass));
instance.annotateLineNumber(cssStyleObject, 'right');
assert.isTrue(lineNumberEl.classList.contains(className));
});
});
</script>

View File

@ -26,6 +26,7 @@ limitations under the License.
<link rel="import" href="../../plugins/gr-popup-interface/gr-popup-interface.html">
<link rel="import" href="../../plugins/gr-repo-api/gr-repo-api.html">
<link rel="import" href="../../plugins/gr-settings-api/gr-settings-api.html">
<link rel="import" href="../../plugins/gr-styles-api/gr-styles-api.html">
<link rel="import" href="../../plugins/gr-theme-api/gr-theme-api.html">
<link rel="import" href="../gr-rest-api-interface/gr-rest-api-interface.html">

View File

@ -319,6 +319,10 @@
return new GrSettingsApi(this);
};
Plugin.prototype.styles = function() {
return new GrStylesApi();
};
/**
* To make REST requests for plugin-provided endpoints, use
* @example
@ -511,7 +515,13 @@
'Please use plugin.getPluginName() instead.');
};
/**
* @deprecated Use plugin.styles().css(rulesStr) instead. Please, consult
* the documentation how to replace it accordingly.
*/
Gerrit.css = function(rulesStr) {
console.warn('Gerrit.css(rulesStr) is deprecated!',
'Use plugin.styles().css(rulesStr)');
if (!Gerrit._customStyleSheet) {
const styleEl = document.createElement('style');
document.head.appendChild(styleEl);

View File

@ -32,6 +32,11 @@
const coverageData = {};
let displayCoverage = false;
const annotationApi = plugin.annotationApi();
const styleApi = plugin.styles();
const coverageStyle = styleApi.css('background-color: #EF9B9B !important');
const emptyStyle = styleApi.css('');
annotationApi.addLayer(context => {
if (Object.keys(coverageData).length === 0) {
// Coverage data is not ready yet.
@ -41,16 +46,16 @@
const line = context.line;
// Highlight lines missing coverage with this background color if
// coverage should be displayed, else do nothing.
const cssClass = displayCoverage
? Gerrit.css('background-color: #EF9B9B')
: Gerrit.css('');
const annotationStyle = displayCoverage
? coverageStyle
: emptyStyle;
if (coverageData[path] &&
coverageData[path].changeNum === context.changeNum &&
coverageData[path].patchNum === context.patchNum) {
const linesMissingCoverage = coverageData[path].linesMissingCoverage;
if (linesMissingCoverage.includes(line.afterNumber)) {
context.annotateRange(0, line.text.length, cssClass, 'right');
context.annotateLineNumber(cssClass, 'right');
context.annotateRange(0, line.text.length, annotationStyle, 'right');
context.annotateLineNumber(annotationStyle, 'right');
}
}
}).enableToggleCheckbox('Display Coverage', checkbox => {

View File

@ -125,6 +125,7 @@ limitations under the License.
'edit/gr-edit-file-controls/gr-edit-file-controls_test.html',
'edit/gr-editor-view/gr-editor-view_test.html',
'plugins/gr-admin-api/gr-admin-api_test.html',
'plugins/gr-styles-api/gr-styles-api_test.html',
'plugins/gr-attribute-helper/gr-attribute-helper_test.html',
'plugins/gr-endpoint-decorator/gr-endpoint-decorator_test.html',
'plugins/gr-event-helper/gr-event-helper_test.html',