diff --git a/Documentation/js-api.txt b/Documentation/js-api.txt index 4ef2a6ce8d..d909c00b26 100644 --- a/Documentation/js-api.txt +++ b/Documentation/js-api.txt @@ -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 `
` and the div is returned. - -.Signature -[source,javascript] ----- -Gerrit.html(htmlText, options, wantElements); ----- - -* htmlText: string of HTML to be parsed. A new unattached `
` 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( - 'Hello {name}!', - {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('Close', 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( - '
Name:
' - + '
Age:
' - + '', - { - 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 diff --git a/Documentation/pg-plugin-dev.txt b/Documentation/pg-plugin-dev.txt index 8fb5655214..d90185147e 100644 --- a/Documentation/pg-plugin-dev.txt +++ b/Documentation/pg-plugin-dev.txt @@ -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 diff --git a/Documentation/pg-plugin-style-object.txt b/Documentation/pg-plugin-style-object.txt new file mode 100644 index 0000000000..cdcfb557d7 --- /dev/null +++ b/Documentation/pg-plugin-style-object.txt @@ -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. + + + diff --git a/Documentation/pg-plugin-styles-api.txt b/Documentation/pg-plugin-styles-api.txt new file mode 100644 index 0000000000..a8293251ec --- /dev/null +++ b/Documentation/pg-plugin-styles-api.txt @@ -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]. + + + diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.html b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.html new file mode 100644 index 0000000000..74b87c8241 --- /dev/null +++ b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.html @@ -0,0 +1,18 @@ + + + diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js new file mode 100644 index 0000000000..879b3921b9 --- /dev/null +++ b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api.js @@ -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); diff --git a/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html new file mode 100644 index 0000000000..d67a3092bd --- /dev/null +++ b/polygerrit-ui/app/elements/plugins/gr-styles-api/gr-styles-api_test.html @@ -0,0 +1,182 @@ + + + + +gr-admin-api + + + + + + + + + + + + + + + + + diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context.js index 45f28d1998..76cb51f2f7 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context.js @@ -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); } }; diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html index 653999c7cf..2a6487ed8a 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-annotation-actions-context_test.html @@ -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)); }); }); diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html index e04867ac3f..e460660bdf 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface.html @@ -26,6 +26,7 @@ limitations under the License. + diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js index fff2e3320e..2dd0e09516 100644 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-public-js-api.js @@ -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); diff --git a/polygerrit-ui/app/samples/coverage-plugin.html b/polygerrit-ui/app/samples/coverage-plugin.html index f8b5560573..f5f9c6e787 100644 --- a/polygerrit-ui/app/samples/coverage-plugin.html +++ b/polygerrit-ui/app/samples/coverage-plugin.html @@ -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 => { diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html index 2b052a01c5..0fbc8f1388 100644 --- a/polygerrit-ui/app/test/index.html +++ b/polygerrit-ui/app/test/index.html @@ -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',