diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js deleted file mode 100644 index 3f8aa44c2e..0000000000 --- a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.js +++ /dev/null @@ -1,99 +0,0 @@ -/** - * @license - * Copyright (C) 2017 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. - */ - -/** @constructor */ -export function GrAttributeHelper(element) { - this.element = element; - this._promises = {}; -} - -GrAttributeHelper.prototype._getChangedEventName = function(name) { - return name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() + '-changed'; -}; - -/** - * Returns true if the property is defined on wrapped element. - * - * @param {string} name - * @return {boolean} - */ -GrAttributeHelper.prototype._elementHasProperty = function(name) { - return this.element[name] !== undefined; -}; - -GrAttributeHelper.prototype._reportValue = function(callback, value) { - try { - callback(value); - } catch (e) { - console.info(e); - } -}; - -/** - * Binds callback to property updates. - * - * @param {string} name Property name. - * @param {function(?)} callback - * @return {function()} Unbind function. - */ -GrAttributeHelper.prototype.bind = function(name, callback) { - const attributeChangedEventName = this._getChangedEventName(name); - const changedHandler = e => this._reportValue(callback, e.detail.value); - const unbind = () => this.element.removeEventListener( - attributeChangedEventName, changedHandler); - this.element.addEventListener( - attributeChangedEventName, changedHandler); - if (this._elementHasProperty(name)) { - this._reportValue(callback, this.element[name]); - } - return unbind; -}; - -/** - * Get value of the property from wrapped object. Waits for the property - * to be initialized if it isn't defined. - * - * @param {string} name Property name. - * @return {!Promise} - */ -GrAttributeHelper.prototype.get = function(name) { - if (this._elementHasProperty(name)) { - return Promise.resolve(this.element[name]); - } - if (!this._promises[name]) { - let resolve; - const promise = new Promise(r => resolve = r); - const unbind = this.bind(name, value => { - resolve(value); - unbind(); - }); - this._promises[name] = promise; - } - return this._promises[name]; -}; - -/** - * Sets value and dispatches event to force notify. - * - * @param {string} name Property name. - * @param {?} value - */ -GrAttributeHelper.prototype.set = function(name, value) { - this.element[name] = value; - this.element.dispatchEvent( - new CustomEvent(this._getChangedEventName(name), {detail: {value}})); -}; diff --git a/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.ts b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.ts new file mode 100644 index 0000000000..6c6321b31c --- /dev/null +++ b/polygerrit-ui/app/elements/plugins/gr-attribute-helper/gr-attribute-helper.ts @@ -0,0 +1,97 @@ +/** + * @license + * Copyright (C) 2017 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. + */ + +export class GrAttributeHelper { + private readonly _promises = new Map>(); + + constructor(public element: any) {} + + _getChangedEventName(name: string): string { + return name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() + '-changed'; + } + + /** + * Returns true if the property is defined on wrapped element. + */ + _elementHasProperty(name: string) { + return this.element[name] !== undefined; + } + + _reportValue(callback: (value: any) => void, value: any) { + try { + callback(value); + } catch (e) { + console.info(e); + } + } + + /** + * Binds callback to property updates. + * + * @param {string} name Property name. + * @param {function(?)} callback + * @return {function()} Unbind function. + */ + bind(name: string, callback: (value: any) => void) { + const attributeChangedEventName = this._getChangedEventName(name); + const changedHandler = (e: CustomEvent) => + this._reportValue(callback, e.detail.value); + const unbind = () => + this.element.removeEventListener( + attributeChangedEventName, + changedHandler + ); + this.element.addEventListener(attributeChangedEventName, changedHandler); + if (this._elementHasProperty(name)) { + this._reportValue(callback, this.element[name]); + } + return unbind; + } + + /** + * Get value of the property from wrapped object. Waits for the property + * to be initialized if it isn't defined. + * + * @param {string} name Property name. + * @return {!Promise} + */ + get(name: string) { + if (this._elementHasProperty(name)) { + return Promise.resolve(this.element[name]); + } + if (!this._promises.has(name)) { + let resolve: (value: any) => void; + const promise = new Promise(r => (resolve = r)); + const unbind = this.bind(name, value => { + resolve(value); + unbind(); + }); + this._promises.set(name, promise); + } + return this._promises.get(name); + } + + /** + * Sets value and dispatches event to force notify. + */ + set(name: string, value: any) { + this.element[name] = value; + this.element.dispatchEvent( + new CustomEvent(this._getChangedEventName(name), {detail: {value}}) + ); + } +} diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js deleted file mode 100644 index 466f84aec0..0000000000 --- a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * @license - * Copyright (C) 2017 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. - */ - -/** @constructor */ -export function GrEventHelper(element) { - this.element = element; - this._unsubscribers = []; -} - -/** - * Add a callback to arbitrary event. - * The callback may return false to prevent event bubbling. - * - * @param {string} event Event name - * @param {function(Event):boolean} callback - * @return {function()} Unsubscribe function. - */ -GrEventHelper.prototype.on = function(event, callback) { - return this._listen(this.element, callback, {event}); -}; - -/** - * Alias of onClick - * - * @see onClick - */ -GrEventHelper.prototype.onTap = function(callback) { - return this._listen(this.element, callback); -}; - -/** - * Add a callback to element click or touch. - * The callback may return false to prevent event bubbling. - * - * @param {function(Event):boolean} callback - * @return {function()} Unsubscribe function. - */ -GrEventHelper.prototype.onClick = function(callback) { - return this._listen(this.element, callback); -}; - -/** - * Alias of captureClick - * - * @see captureClick - */ -GrEventHelper.prototype.captureTap = function(callback) { - return this._listen(this.element.parentElement, callback, {capture: true}); -}; - -/** - * Add a callback to element click or touch ahead of normal flow. - * Callback is installed on parent during capture phase. - * https://www.w3.org/TR/DOM-Level-3-Events/#event-flow - * The callback may return false to cancel regular event listeners. - * - * @param {function(Event):boolean} callback - * @return {function()} Unsubscribe function. - */ -GrEventHelper.prototype.captureClick = function(callback) { - return this._listen(this.element.parentElement, callback, {capture: true}); -}; - -GrEventHelper.prototype._listen = function(container, callback, opt_options) { - const capture = opt_options && opt_options.capture; - const event = opt_options && opt_options.event || 'click'; - const handler = e => { - if (e.path.indexOf(this.element) !== -1) { - let mayContinue = true; - try { - mayContinue = callback(e); - } catch (e) { - console.warn(`Plugin error handing event: ${e}`); - } - if (mayContinue === false) { - e.stopImmediatePropagation(); - e.stopPropagation(); - e.preventDefault(); - } - } - }; - container.addEventListener(event, handler, capture); - const unsubscribe = () => - container.removeEventListener(event, handler, capture); - this._unsubscribers.push(unsubscribe); - return unsubscribe; -}; - diff --git a/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.ts b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.ts new file mode 100644 index 0000000000..bf08b8e7bd --- /dev/null +++ b/polygerrit-ui/app/elements/plugins/gr-event-helper/gr-event-helper.ts @@ -0,0 +1,99 @@ +/** + * @license + * Copyright (C) 2017 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. + */ + +interface EventWithPath extends Event { + path?: HTMLElement[]; +} + +export interface ListenOptions { + event?: string; + capture?: boolean; +} + +export class GrEventHelper { + constructor(readonly element: HTMLElement) {} + + /** + * Add a callback to arbitrary event. + * The callback may return false to prevent event bubbling. + */ + on(event: string, callback: (event: Event) => boolean) { + return this._listen(this.element, callback, {event}); + } + + /** + * Alias for @see onClick + */ + onTap(callback: (event: Event) => boolean) { + return this.onClick(callback); + } + + /** + * Add a callback to element click or touch. + * The callback may return false to prevent event bubbling. + */ + onClick(callback: (event: Event) => boolean) { + return this._listen(this.element, callback); + } + + /** + * Alias for @see captureClick + */ + captureTap(callback: (event: Event) => boolean) { + this.captureClick(callback); + } + + /** + * Add a callback to element click or touch ahead of normal flow. + * Callback is installed on parent during capture phase. + * https://www.w3.org/TR/DOM-Level-3-Events/#event-flow + * The callback may return false to cancel regular event listeners. + */ + captureClick(callback: (event: Event) => boolean) { + const parent = this.element.parentElement!; + return this._listen(parent, callback, {capture: true}); + } + + _listen( + container: HTMLElement, + callback: (event: Event) => boolean, + opt_options?: ListenOptions | null + ) { + const capture = opt_options?.capture; + const event = opt_options?.event || 'click'; + const handler = (e: EventWithPath) => { + if (!e.path) return; + if (e.path.indexOf(this.element) !== -1) { + let mayContinue = true; + try { + mayContinue = callback(e); + } catch (exception) { + console.warn(`Plugin error handing event: ${exception}`); + } + if (mayContinue === false) { + e.stopImmediatePropagation(); + e.stopPropagation(); + e.preventDefault(); + } + } + }; + container.addEventListener(event, handler, capture); + const unsubscribe = () => + container.removeEventListener(event, handler, capture); + return unsubscribe; + } +} diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js deleted file mode 100644 index dae8d3e5a8..0000000000 --- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.js +++ /dev/null @@ -1,228 +0,0 @@ -/** - * @license - * Copyright (C) 2017 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 {importHref} from '../../../scripts/import-href.js'; - -/** @constructor */ -export class GrPluginEndpoints { - constructor() { - this._endpoints = {}; - this._callbacks = {}; - this._dynamicPlugins = {}; - this._importedUrls = new Set(); - this._pluginLoaded = false; - } - - setPluginsReady() { - this._pluginLoaded = true; - } - - onNewEndpoint(endpoint, callback) { - if (!this._callbacks[endpoint]) { - this._callbacks[endpoint] = []; - } - this._callbacks[endpoint].push(callback); - } - - onDetachedEndpoint(endpoint, callback) { - if (this._callbacks[endpoint]) { - this._callbacks[endpoint] = this._callbacks[endpoint].filter( - cb => cb !== callback - ); - } - } - - _getOrCreateModuleInfo(plugin, opts) { - const {endpoint, slot, type, moduleName, domHook} = opts; - const existingModule = this._endpoints[endpoint].find( - info => - info.plugin === plugin && - info.moduleName === moduleName && - info.domHook === domHook && - info.slot === slot - ); - if (existingModule) { - return existingModule; - } else { - const newModule = { - moduleName, - plugin, - pluginUrl: plugin._url, - type, - domHook, - slot, - }; - this._endpoints[endpoint].push(newModule); - return newModule; - } - } - - /** - * Register a plugin to an endpoint. - * - * Dynamic plugins are registered to a specific prefix, such as - * 'change-list-header'. These plugins are then fetched by prefix to determine - * which endpoints to dynamically add to the page. - * - * @param {Object} plugin - * @param {Object} opts - */ - registerModule(plugin, opts) { - const {endpoint, dynamicEndpoint} = opts; - if (dynamicEndpoint) { - if (!this._dynamicPlugins[dynamicEndpoint]) { - this._dynamicPlugins[dynamicEndpoint] = new Set(); - } - this._dynamicPlugins[dynamicEndpoint].add(endpoint); - } - if (!this._endpoints[endpoint]) { - this._endpoints[endpoint] = []; - } - const moduleInfo = this._getOrCreateModuleInfo(plugin, opts); - // TODO: the logic below seems wrong when: - // multiple plugins register to the same endpoint - // one register before plugins ready - // the other done after, then only the later one will have the callbacks - // invoked. - if (this._pluginLoaded && this._callbacks[endpoint]) { - this._callbacks[endpoint].forEach(callback => callback(moduleInfo)); - } - } - - getDynamicEndpoints(dynamicEndpoint) { - const plugins = this._dynamicPlugins[dynamicEndpoint]; - if (!plugins) return []; - return Array.from(plugins); - } - - /** - * Get detailed information about modules registered with an extension - * endpoint. - * - * @param {string} name Endpoint name. - * @param {?{ - * type: (string|undefined), - * moduleName: (string|undefined) - * }} opt_options - * @return {!Array<{ - * moduleName: string, - * plugin: Plugin, - * pluginUrl: String, - * type: EndpointType, - * domHook: !Object - * }>} - */ - getDetails(name, opt_options) { - const type = opt_options && opt_options.type; - const moduleName = opt_options && opt_options.moduleName; - if (!this._endpoints[name]) { - return []; - } - return this._endpoints[name].filter( - item => - (!type || item.type === type) && - (!moduleName || moduleName == item.moduleName) - ); - } - - /** - * Get detailed module names for instantiating at the endpoint. - * - * @param {string} name Endpoint name. - * @param {?{ - * type: (string|undefined), - * moduleName: (string|undefined) - * }} opt_options - * @return {!Array} - */ - getModules(name, opt_options) { - const modulesData = this.getDetails(name, opt_options); - if (!modulesData.length) { - return []; - } - return modulesData.map(m => m.moduleName); - } - - /** - * Get plugin URLs with element and module definitions. - * - * @param {string} name Endpoint name. - * @param {?{ - * type: (string|undefined), - * moduleName: (string|undefined) - * }} opt_options - * @return {!Array} - */ - getPlugins(name, opt_options) { - const modulesData = this.getDetails(name, opt_options); - if (!modulesData.length) { - return []; - } - return Array.from(new Set(modulesData.map(m => m.pluginUrl))); - } - - importUrl(pluginUrl) { - let timerId; - return Promise - .race([ - new Promise((resolve, reject) => { - this._importedUrls.add(pluginUrl.href); - importHref(pluginUrl, resolve, reject); - }), - // Timeout after 3s - new Promise(r => timerId = setTimeout(r, 3000)), - ]) - .finally(() => { - if (timerId) clearTimeout(timerId); - }); - } - - /** - * Get plugin URLs with element and module definitions. - * - * @param {string} name Endpoint name. - * @param {?{ - * type: (string|undefined), - * moduleName: (string|undefined) - * }} opt_options - * @return {!Array>} - */ - getAndImportPlugins(name, opt_options) { - return Promise.all( - this.getPlugins(name, opt_options).map(pluginUrl => { - if (this._importedUrls.has(pluginUrl.href)) { - return Promise.resolve(); - } - - // TODO: we will deprecate html plugins entirely - // for now, keep the original behavior and import - // only for html ones - if (pluginUrl && pluginUrl.pathname.endsWith('.html')) { - return this.importUrl(pluginUrl); - } else { - return Promise.resolve(); - } - }) - ); - } -} - -// TODO(dmfilippov): Convert to service and add to appContext -export let pluginEndpoints = new GrPluginEndpoints(); -export function _testOnly_resetEndpoints() { - pluginEndpoints = new GrPluginEndpoints(); -} diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts new file mode 100644 index 0000000000..6a3f957625 --- /dev/null +++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-plugin-endpoints.ts @@ -0,0 +1,223 @@ +/** + * @license + * Copyright (C) 2017 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 {importHref} from '../../../scripts/import-href'; + +type Callback = (value: any) => void; + +interface ModuleInfo { + moduleName: string; + // TODO(TS): Convert type to GrPlugin. + plugin: any; + pluginUrl: URL; + type?: string; + // TODO(TS): Convert type to GrDomHook. + domHook: any; + slot?: string; +} + +interface Options { + endpoint: string; + dynamicEndpoint?: string; + slot?: string; + type?: string; + moduleName?: string; + // TODO(TS): Convert type to GrDomHook. + domHook?: any; +} + +export class GrPluginEndpoints { + private readonly _endpoints = new Map(); + + private readonly _callbacks = new Map void)[]>(); + + private readonly _dynamicPlugins = new Map>(); + + private readonly _importedUrls = new Set(); + + private _pluginLoaded = false; + + setPluginsReady() { + this._pluginLoaded = true; + } + + onNewEndpoint(endpoint: string, callback: Callback) { + if (!this._callbacks.has(endpoint)) { + this._callbacks.set(endpoint, []); + } + this._callbacks.get(endpoint)!.push(callback); + } + + onDetachedEndpoint(endpoint: string, callback: Callback) { + if (this._callbacks.has(endpoint)) { + const filteredCallbacks = this._callbacks + .get(endpoint)! + .filter((cb: Callback) => cb !== callback); + this._callbacks.set(endpoint, filteredCallbacks); + } + } + + _getOrCreateModuleInfo(plugin: any, opts: Options): ModuleInfo { + const {endpoint, slot, type, moduleName, domHook} = opts; + const existingModule = this._endpoints + .get(endpoint!)! + .find( + (info: ModuleInfo) => + info.plugin === plugin && + info.moduleName === moduleName && + info.domHook === domHook && + info.slot === slot + ); + if (existingModule) { + return existingModule; + } else { + const newModule: ModuleInfo = { + moduleName: moduleName!, + plugin, + pluginUrl: plugin._url, + type, + domHook, + slot, + }; + this._endpoints.get(endpoint!)!.push(newModule); + return newModule; + } + } + + /** + * Register a plugin to an endpoint. + * + * Dynamic plugins are registered to a specific prefix, such as + * 'change-list-header'. These plugins are then fetched by prefix to determine + * which endpoints to dynamically add to the page. + * + * @param {Object} plugin + * @param {Object} opts + */ + registerModule(plugin: any, opts: Options) { + const endpoint = opts.endpoint!; + const dynamicEndpoint = opts.dynamicEndpoint; + if (dynamicEndpoint) { + if (!this._dynamicPlugins.has(dynamicEndpoint)) { + this._dynamicPlugins.set(dynamicEndpoint, new Set()); + } + this._dynamicPlugins.get(dynamicEndpoint)!.add(endpoint); + } + if (!this._endpoints.has(endpoint)) { + this._endpoints.set(endpoint, []); + } + const moduleInfo = this._getOrCreateModuleInfo(plugin, opts); + // TODO: the logic below seems wrong when: + // multiple plugins register to the same endpoint + // one register before plugins ready + // the other done after, then only the later one will have the callbacks + // invoked. + if (this._pluginLoaded && this._callbacks.has(endpoint)) { + this._callbacks.get(endpoint)!.forEach(callback => callback(moduleInfo)); + } + } + + getDynamicEndpoints(dynamicEndpoint: string): string[] { + const plugins = this._dynamicPlugins.get(dynamicEndpoint); + if (!plugins) return []; + return Array.from(plugins); + } + + /** + * Get detailed information about modules registered with an extension + * endpoint. + */ + getDetails(name: string, options?: Options): ModuleInfo[] { + const type = options && options.type; + const moduleName = options && options.moduleName; + if (!this._endpoints.has(name)) { + return []; + } else { + return this._endpoints + .get(name)! + .filter( + (item: ModuleInfo) => + (!type || item.type === type) && + (!moduleName || moduleName === item.moduleName) + ); + } + } + + /** + * Get detailed module names for instantiating at the endpoint. + */ + getModules(name: string, options?: Options): string[] { + const modulesData = this.getDetails(name, options); + if (!modulesData.length) { + return []; + } + return modulesData.map(m => m.moduleName); + } + + /** + * Get plugin URLs with element and module definitions. + */ + getPlugins(name: string, options?: Options): URL[] { + const modulesData = this.getDetails(name, options); + if (!modulesData.length) { + return []; + } + return Array.from(new Set(modulesData.map(m => m.pluginUrl))); + } + + importUrl(pluginUrl: URL) { + let timerId: any; + return Promise.race([ + new Promise((resolve, reject) => { + this._importedUrls.add(pluginUrl.href); + importHref(pluginUrl.href, resolve, reject); + }), + // Timeout after 3s + new Promise(r => (timerId = setTimeout(r, 3000))), + ]).finally(() => { + if (timerId) clearTimeout(timerId); + }); + } + + /** + * Get plugin URLs with element and module definitions. + */ + getAndImportPlugins(name: string, options?: Options) { + return Promise.all( + this.getPlugins(name, options).map(pluginUrl => { + if (this._importedUrls.has(pluginUrl.href)) { + return Promise.resolve(); + } + + // TODO: we will deprecate html plugins entirely + // for now, keep the original behavior and import + // only for html ones + if (pluginUrl && pluginUrl.pathname.endsWith('.html')) { + return this.importUrl(pluginUrl); + } else { + return Promise.resolve(); + } + }) + ); + } +} + +// TODO(dmfilippov): Convert to service and add to appContext +export let pluginEndpoints = new GrPluginEndpoints(); +export function _testOnly_resetEndpoints() { + pluginEndpoints = new GrPluginEndpoints(); +} diff --git a/polygerrit-ui/app/scripts/import-href.js b/polygerrit-ui/app/scripts/import-href.ts similarity index 75% rename from polygerrit-ui/app/scripts/import-href.js rename to polygerrit-ui/app/scripts/import-href.ts index 6ff40a5d96..bd1b678292 100644 --- a/polygerrit-ui/app/scripts/import-href.js +++ b/polygerrit-ui/app/scripts/import-href.ts @@ -20,11 +20,16 @@ // file contains code inside and can't be imported // in es6 modules. +interface ImportHrefElement extends HTMLLinkElement { + __dynamicImportLoaded?: boolean; +} + // run a callback when HTMLImports are ready or immediately if // this api is not available. -function whenImportsReady(cb) { - if (window.HTMLImports) { - HTMLImports.whenReady(cb); +function whenImportsReady(cb: () => void) { + const win = window as Window; + if (win.HTMLImports) { + win.HTMLImports.whenReady(cb); } else { cb(); } @@ -39,37 +44,43 @@ function whenImportsReady(cb) { * element will contain the imported document contents. * * @memberof Polymer - * @param {string} href URL to document to load. - * @param {?function(!Event):void=} onload Callback to notify when an import successfully + * @param href URL to document to load. + * @param onload Callback to notify when an import successfully * loaded. - * @param {?function(!ErrorEvent):void=} onerror Callback to notify when an import + * @param onerror Callback to notify when an import * unsuccessfully loaded. - * @param {boolean=} optAsync True if the import should be loaded `async`. + * @param async True if the import should be loaded `async`. * Defaults to `false`. - * @return {!HTMLLinkElement} The link element for the URL to be loaded. + * @return The link element for the URL to be loaded. */ -export function importHref(href, onload, onerror, optAsync) { - let link = /** @type {HTMLLinkElement} */ - (document.head.querySelector('link[href="' + href + '"][import-href]')); +export function importHref( + href: string, + onload: (e: Event) => void, + onerror: (e: Event) => void, + async = false +): HTMLLinkElement { + let link = document.head.querySelector( + 'link[href="' + href + '"][import-href]' + ) as ImportHrefElement; if (!link) { - link = /** @type {HTMLLinkElement} */ (document.createElement('link')); + link = document.createElement('link') as ImportHrefElement; link.rel = 'import'; link.href = href; link.setAttribute('import-href', ''); } // always ensure link has `async` attribute if user specified one, // even if it was previously not async. This is considered less confusing. - if (optAsync) { + if (async) { link.setAttribute('async', ''); } // NOTE: the link may now be in 3 states: (1) pending insertion, // (2) inflight, (3) already loaded. In each case, we need to add // event listeners to process callbacks. - const cleanup = function() { + const cleanup = function () { link.removeEventListener('load', loadListener); link.removeEventListener('error', errorListener); }; - const loadListener = function(event) { + const loadListener = function (event: Event) { cleanup(); // In case of a successful load, cache the load event on the link so // that it can be used to short-circuit this method in the future when @@ -81,7 +92,7 @@ export function importHref(href, onload, onerror, optAsync) { }); } }; - const errorListener = function(event) { + const errorListener = function (event: Event) { cleanup(); // In case of an error, remove the link from the document so that it // will be automatically created again the next time `importHref` is @@ -97,7 +108,7 @@ export function importHref(href, onload, onerror, optAsync) { }; link.addEventListener('load', loadListener); link.addEventListener('error', errorListener); - if (link.parentNode == null) { + if (link.parentNode === null) { document.head.appendChild(link); // if the link already loaded, dispatch a fake load event // so that listeners are called and get a proper event argument. diff --git a/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.js b/polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts similarity index 100% rename from polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.js rename to polygerrit-ui/app/services/gr-reporting/gr-reporting_mock.ts diff --git a/polygerrit-ui/app/test/test-app-context-init.js b/polygerrit-ui/app/test/test-app-context-init.js index 6fffdba792..88232cd384 100644 --- a/polygerrit-ui/app/test/test-app-context-init.js +++ b/polygerrit-ui/app/test/test-app-context-init.js @@ -17,7 +17,7 @@ // Init app context before any other imports import {initAppContext} from '../services/app-context-init.js'; -import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock'; +import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock.js'; import {appContext} from '../services/app-context.js'; initAppContext(); diff --git a/polygerrit-ui/app/types/globals.ts b/polygerrit-ui/app/types/globals.ts index 167ccda367..a09e651a92 100644 --- a/polygerrit-ui/app/types/globals.ts +++ b/polygerrit-ui/app/types/globals.ts @@ -22,6 +22,7 @@ declare global { ShadyCSS?: { getComputedStyleValue(el: Element, name: string): string; }; + HTMLImports?: {whenReady: (cb: () => void) => void}; } interface Performance {