Generalize library loader Until recently, the only side-loaded resource was the HLJS library for computing syntax highlighting. For this task, the gr-syntax-lib-loader provided an interface to load the library whether or not PG is being served from a CDN. With this change, the component is refactored to allow loading resources other than the syntax library. A method is added for loading the "dark-theme" document independently of whether a CDN is configured. Also, some documentation comments are added to the existing methods. Change-Id: I9891539cd4cf76ac0fe430ff3988e3a9dfbb0ca3
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html index cd9f9dc..017cd5d 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html +++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.html
@@ -15,11 +15,11 @@ limitations under the License. --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> -<link rel="import" href="../gr-syntax-lib-loader/gr-syntax-lib-loader.html"> +<link rel="import" href="../../shared/gr-lib-loader/gr-lib-loader.html"> <dom-module id="gr-syntax-layer"> <template> - <gr-syntax-lib-loader id="libLoader"></gr-syntax-lib-loader> + <gr-lib-loader id="libLoader"></gr-lib-loader> </template> <script src="../gr-diff/gr-diff-line.js"></script> <script src="../gr-diff-highlight/gr-annotation.js"></script>
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js index f8db343..15a8a0a 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js +++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -442,7 +442,7 @@ }, _loadHLJS() { - return this.$.libLoader.get().then(hljs => { + return this.$.libLoader.getHLJS().then(hljs => { this._hljs = hljs; }); },
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html index 74fc3bf..f2458fc 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html +++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer_test.html
@@ -185,7 +185,7 @@ const mockHLJS = getMockHLJS(); const highlightSpy = sinon.spy(mockHLJS, 'highlight'); - sandbox.stub(element.$.libLoader, 'get', + sandbox.stub(element.$.libLoader, 'getHLJS', () => { return Promise.resolve(mockHLJS); }); const processNextSpy = sandbox.spy(element, '_processNextLine'); const processPromise = element.process();
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js b/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js deleted file mode 100644 index 6ec7ab2..0000000 --- a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.js +++ /dev/null
@@ -1,113 +0,0 @@ -/** - * @license - * Copyright (C) 2016 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() { - 'use strict'; - - const HLJS_PATH = 'bower_components/highlightjs/highlight.min.js'; - const LIB_ROOT_PATTERN = /(.+\/)elements\/gr-app\.html/; - - Polymer({ - is: 'gr-syntax-lib-loader', - - properties: { - _state: { - type: Object, - - // NOTE: intended singleton. - value: { - configured: false, - loading: false, - callbacks: [], - }, - }, - }, - - get() { - return new Promise((resolve, reject) => { - // If the lib is totally loaded, resolve immediately. - if (this._getHighlightLib()) { - resolve(this._getHighlightLib()); - return; - } - - // If the library is not currently being loaded, then start loading it. - if (!this._state.loading) { - this._state.loading = true; - this._loadHLJS().then(this._onLibLoaded.bind(this)).catch(reject); - } - - this._state.callbacks.push(resolve); - }); - }, - - _onLibLoaded() { - const lib = this._getHighlightLib(); - this._state.loading = false; - for (const cb of this._state.callbacks) { - cb(lib); - } - this._state.callbacks = []; - }, - - _getHighlightLib() { - const lib = window.hljs; - if (lib && !this._state.configured) { - this._state.configured = true; - - lib.configure({classPrefix: 'gr-diff gr-syntax gr-syntax-'}); - } - return lib; - }, - - _getLibRoot() { - if (this._cachedLibRoot) { return this._cachedLibRoot; } - - const appLink = document.head - .querySelector('link[rel=import][href$="gr-app.html"]'); - - if (!appLink) { return null; } - - return this._cachedLibRoot = appLink - .href - .match(LIB_ROOT_PATTERN)[1]; - }, - _cachedLibRoot: null, - - _loadHLJS() { - return new Promise((resolve, reject) => { - const script = document.createElement('script'); - const src = this._getHLJSUrl(); - - if (!src) { - reject(new Error('Unable to load blank HLJS url.')); - return; - } - - script.src = src; - script.onload = resolve; - script.onerror = reject; - Polymer.dom(document.head).appendChild(script); - }); - }, - - _getHLJSUrl() { - const root = this._getLibRoot(); - if (!root) { return null; } - return root + HLJS_PATH; - }, - }); -})();
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html index de62646..7661d8e 100644 --- a/polygerrit-ui/app/elements/gr-app.html +++ b/polygerrit-ui/app/elements/gr-app.html
@@ -56,6 +56,7 @@ <link rel="import" href="./settings/gr-registration-dialog/gr-registration-dialog.html"> <link rel="import" href="./settings/gr-settings-view/gr-settings-view.html"> <link rel="import" href="./shared/gr-fixed-panel/gr-fixed-panel.html"> +<link rel="import" href="./shared/gr-lib-loader/gr-lib-loader.html"> <link rel="import" href="./shared/gr-rest-api-interface/gr-rest-api-interface.html"> <script src="../scripts/util.js"></script> @@ -229,6 +230,7 @@ <gr-plugin-host id="plugins" config="[[_serverConfig]]"> </gr-plugin-host> + <gr-lib-loader id="libLoader"></gr-lib-loader> <gr-external-style id="externalStyle" name="app-theme"></gr-external-style> </template> <script src="gr-app.js" crossorigin="anonymous"></script>
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js index 921415f..af9c27a 100644 --- a/polygerrit-ui/app/elements/gr-app.js +++ b/polygerrit-ui/app/elements/gr-app.js
@@ -128,7 +128,7 @@ }); if (window.localStorage.getItem('dark-theme')) { - this.importHref('../styles/themes/dark-theme.html'); + this.$.libLoader.loadDarkTheme(); } // Note: this is evaluated here to ensure that it only happens after the
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.html b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html similarity index 88% rename from polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.html rename to polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html index f5b71be..f70aff4 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader.html +++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.html
@@ -16,6 +16,6 @@ --> <link rel="import" href="../../../bower_components/polymer/polymer.html"> -<dom-module id="gr-syntax-lib-loader"> - <script src="gr-syntax-lib-loader.js"></script> +<dom-module id="gr-lib-loader"> + <script src="gr-lib-loader.js"></script> </dom-module>
diff --git a/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js new file mode 100644 index 0000000..fc26b51 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader.js
@@ -0,0 +1,152 @@ +/** + * @license + * Copyright (C) 2016 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() { + 'use strict'; + + const HLJS_PATH = 'bower_components/highlightjs/highlight.min.js'; + const DARK_THEME_PATH = 'styles/themes/dark-theme.html'; + const LIB_ROOT_PATTERN = /(.+\/)elements\/gr-app\.html/; + + Polymer({ + is: 'gr-lib-loader', + + properties: { + _hljsState: { + type: Object, + + // NOTE: intended singleton. + value: { + configured: false, + loading: false, + callbacks: [], + }, + }, + }, + + /** + * Get the HLJS library. Returns a promise that resolves with a reference to + * the library after it's been loaded. The promise resolves immediately if + * it's already been loaded. + * @return {!Promise<Object>} + */ + getHLJS() { + return new Promise((resolve, reject) => { + // If the lib is totally loaded, resolve immediately. + if (this._getHighlightLib()) { + resolve(this._getHighlightLib()); + return; + } + + // If the library is not currently being loaded, then start loading it. + if (!this._hljsState.loading) { + this._hljsState.loading = true; + this._loadScript(this._getHLJSUrl()) + .then(this._onHLJSLibLoaded.bind(this)).catch(reject); + } + + this._hljsState.callbacks.push(resolve); + }); + }, + + /** + * Load and apply the dark theme document. + */ + loadDarkTheme() { + this.importHref(this._getLibRoot() + DARK_THEME_PATH); + }, + + /** + * Execute callbacks awaiting the HLJS lib load. + */ + _onHLJSLibLoaded() { + const lib = this._getHighlightLib(); + this._hljsState.loading = false; + for (const cb of this._hljsState.callbacks) { + cb(lib); + } + this._hljsState.callbacks = []; + }, + + /** + * Get the HLJS library, assuming it has been loaded. Configure the library + * if it hasn't already been configured. + * @return {!Object} + */ + _getHighlightLib() { + const lib = window.hljs; + if (lib && !this._hljsState.configured) { + this._hljsState.configured = true; + + lib.configure({classPrefix: 'gr-diff gr-syntax gr-syntax-'}); + } + return lib; + }, + + /** + * Get the resource path used to load the application. If the application + * was loaded through a CDN, then this will be the path to CDN resources. + * @return {string} + */ + _getLibRoot() { + if (this._cachedLibRoot) { return this._cachedLibRoot; } + + const appLink = document.head + .querySelector('link[rel=import][href$="gr-app.html"]'); + + if (!appLink) { throw new Error('Could not find application link'); } + + this._cachedLibRoot = appLink + .href + .match(LIB_ROOT_PATTERN)[1]; + + if (!this._cachedLibRoot) { + throw new Error('Could not extract lib root'); + } + + return this._cachedLibRoot; + }, + _cachedLibRoot: null, + + /** + * Load and execute a JS file from the lib root. + * @param {string} src The path to the JS file without the lib root. + * @return {Promise} a promise that resolves when the script's onload + * executes. + */ + _loadScript(src) { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + + if (!src) { + reject(new Error('Unable to load blank script url.')); + return; + } + + script.src = src; + script.onload = resolve; + script.onerror = reject; + Polymer.dom(document.head).appendChild(script); + }); + }, + + _getHLJSUrl() { + const root = this._getLibRoot(); + if (!root) { return null; } + return root + HLJS_PATH; + }, + }); +})();
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html similarity index 77% rename from polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html rename to polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html index a260a97..cf9a41c 100644 --- a/polygerrit-ui/app/elements/diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html +++ b/polygerrit-ui/app/elements/shared/gr-lib-loader/gr-lib-loader_test.html
@@ -17,64 +17,67 @@ --> <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> -<title>gr-syntax-lib-loader</title> +<title>gr-lib-loader</title> <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.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="gr-syntax-lib-loader.html"> +<link rel="import" href="gr-lib-loader.html"> <script>void(0);</script> <test-fixture id="basic"> <template> - <gr-syntax-lib-loader></gr-syntax-lib-loader> + <gr-lib-loader></gr-lib-loader> </template> </test-fixture> <script> - suite('gr-syntax-lib-loader tests', () => { + suite('gr-lib-loader tests', () => { + let sandbox; let element; let resolveLoad; let loadStub; setup(() => { + sandbox = sinon.sandbox.create(); element = fixture('basic'); - loadStub = sinon.stub(element, '_loadHLJS', () => + loadStub = sandbox.stub(element, '_loadScript', () => new Promise(resolve => resolveLoad = resolve) ); // Assert preconditions: - assert.isFalse(element._state.loading); + assert.isFalse(element._hljsState.loading); }); teardown(() => { if (window.hljs) { delete window.hljs; } - loadStub.restore(); + sandbox.restore(); // Because the element state is a singleton, clean it up. - element._state.configured = false; - element._state.loading = false; - element._state.callbacks = []; + element._hljsState.configured = false; + element._hljsState.loading = false; + element._hljsState.callbacks = []; }); test('only load once', done => { + sandbox.stub(element, '_getHLJSUrl').returns(''); const firstCallHandler = sinon.stub(); - element.get().then(firstCallHandler); + element.getHLJS().then(firstCallHandler); // It should now be in the loading state. assert.isTrue(loadStub.called); - assert.isTrue(element._state.loading); + assert.isTrue(element._hljsState.loading); assert.isFalse(firstCallHandler.called); const secondCallHandler = sinon.stub(); - element.get().then(secondCallHandler); + element.getHLJS().then(secondCallHandler); // No change in state. - assert.isTrue(element._state.loading); + assert.isTrue(element._hljsState.loading); assert.isFalse(firstCallHandler.called); assert.isFalse(secondCallHandler.called); @@ -82,7 +85,7 @@ resolveLoad(); flush(() => { // The state should be loaded and both handlers called. - assert.isFalse(element._state.loading); + assert.isFalse(element._hljsState.loading); assert.isTrue(firstCallHandler.called); assert.isTrue(secondCallHandler.called); done(); @@ -105,7 +108,7 @@ test('returns hljs', done => { const firstCallHandler = sinon.stub(); - element.get().then(firstCallHandler); + element.getHLJS().then(firstCallHandler); flush(() => { assert.isTrue(firstCallHandler.called); assert.isTrue(firstCallHandler.calledWith(hljsStub)); @@ -114,7 +117,7 @@ }); test('configures hljs', done => { - element.get().then(() => { + element.getHLJS().then(() => { assert.isTrue(window.hljs.configure.calledOnce); done(); }); @@ -123,15 +126,10 @@ suite('_getHLJSUrl', () => { suite('checking _getLibRoot', () => { - let libRootStub; let root; setup(() => { - libRootStub = sinon.stub(element, '_getLibRoot', () => root); - }); - - teardown(() => { - libRootStub.restore(); + sandbox.stub(element, '_getLibRoot', () => root); }); test('with no root', () => {
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html index 6a562fc..5a5dbcd 100644 --- a/polygerrit-ui/app/test/index.html +++ b/polygerrit-ui/app/test/index.html
@@ -112,7 +112,6 @@ 'diff/gr-ranged-comment-layer/gr-ranged-comment-layer_test.html', 'diff/gr-selection-action-box/gr-selection-action-box_test.html', 'diff/gr-syntax-layer/gr-syntax-layer_test.html', - 'diff/gr-syntax-lib-loader/gr-syntax-lib-loader_test.html', 'edit/gr-default-editor/gr-default-editor_test.html', 'edit/gr-edit-controls/gr-edit-controls_test.html', 'edit/gr-edit-file-controls/gr-edit-file-controls_test.html', @@ -165,6 +164,7 @@ 'shared/gr-js-api-interface/gr-plugin-endpoints_test.html', 'shared/gr-js-api-interface/gr-plugin-rest-api_test.html', 'shared/gr-fixed-panel/gr-fixed-panel_test.html', + 'shared/gr-lib-loader/gr-lib-loader_test.html', 'shared/gr-limited-text/gr-limited-text_test.html', 'shared/gr-linked-chip/gr-linked-chip_test.html', 'shared/gr-linked-text/gr-linked-text_test.html',