| /** |
| * @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 {PolymerElement} from '@polymer/polymer/polymer-element'; |
| import {HookApi, HookCallback, PluginApi} from '../gr-plugin-types'; |
| |
| export class GrDomHooksManager { |
| private _hooks: Record<string, GrDomHook>; |
| |
| private _plugin: PluginApi; |
| |
| constructor(plugin: PluginApi) { |
| this._plugin = plugin; |
| this._hooks = {}; |
| } |
| |
| _getHookName(endpointName: string, moduleName?: string) { |
| if (moduleName) { |
| return endpointName + ' ' + moduleName; |
| } else { |
| // lowercase in case plugin's name contains uppercase letters |
| // TODO: this still can not prevent if plugin has invalid char |
| // other than uppercase, but is the first step |
| // https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name |
| const pluginName: string = |
| this._plugin.getPluginName() || 'unknown_plugin'; |
| return pluginName.toLowerCase() + '-autogenerated-' + endpointName; |
| } |
| } |
| |
| getDomHook(endpointName: string, moduleName?: string) { |
| const hookName = this._getHookName(endpointName, moduleName); |
| if (!this._hooks[hookName]) { |
| this._hooks[hookName] = new GrDomHook(hookName, moduleName); |
| } |
| return this._hooks[hookName]; |
| } |
| } |
| |
| export class GrDomHook implements HookApi { |
| private _instances: HTMLElement[] = []; |
| |
| private _attachCallbacks: HookCallback[] = []; |
| |
| private _detachCallbacks: HookCallback[] = []; |
| |
| private _moduleName: string; |
| |
| private _lastAttachedPromise: Promise<HTMLElement> | null = null; |
| |
| constructor(hookName: string, moduleName?: string) { |
| if (moduleName) { |
| this._moduleName = moduleName; |
| } else { |
| this._moduleName = hookName; |
| this._createPlaceholder(hookName); |
| } |
| } |
| |
| _createPlaceholder(hookName: string) { |
| class HookPlaceholder extends PolymerElement { |
| static get is() { |
| return hookName; |
| } |
| |
| static get properties() { |
| return { |
| plugin: Object, |
| content: Object, |
| }; |
| } |
| } |
| |
| customElements.define(HookPlaceholder.is, HookPlaceholder); |
| } |
| |
| handleInstanceDetached(instance: HTMLElement) { |
| const index = this._instances.indexOf(instance); |
| if (index !== -1) { |
| this._instances.splice(index, 1); |
| } |
| this._detachCallbacks.forEach(callback => callback(instance)); |
| } |
| |
| handleInstanceAttached(instance: HTMLElement) { |
| this._instances.push(instance); |
| this._attachCallbacks.forEach(callback => callback(instance)); |
| } |
| |
| /** |
| * Get instance of last DOM hook element attached into the endpoint. |
| * Returns a Promise, that's resolved when attachment is done. |
| */ |
| getLastAttached(): Promise<HTMLElement> { |
| if (this._instances.length) { |
| return Promise.resolve(this._instances.slice(-1)[0]); |
| } |
| if (!this._lastAttachedPromise) { |
| let resolve: HookCallback; |
| const promise = new Promise<HTMLElement>(r => { |
| resolve = r; |
| this._attachCallbacks.push(resolve); |
| }); |
| this._lastAttachedPromise = promise.then((element: HTMLElement) => { |
| this._lastAttachedPromise = null; |
| const index = this._attachCallbacks.indexOf(resolve); |
| if (index !== -1) { |
| this._attachCallbacks.splice(index, 1); |
| } |
| return element; |
| }); |
| } |
| return this._lastAttachedPromise; |
| } |
| |
| /** |
| * Get all DOM hook elements. |
| */ |
| getAllAttached() { |
| return this._instances; |
| } |
| |
| /** |
| * Install a new callback to invoke when a new instance of DOM hook element |
| * is attached. |
| */ |
| onAttached(callback: HookCallback) { |
| this._attachCallbacks.push(callback); |
| return this; |
| } |
| |
| /** |
| * Install a new callback to invoke when an instance of DOM hook element |
| * is detached. |
| * |
| */ |
| onDetached(callback: HookCallback) { |
| this._detachCallbacks.push(callback); |
| return this; |
| } |
| |
| /** |
| * Name of DOM hook element that will be installed into the endpoint. |
| */ |
| getModuleName() { |
| return this._moduleName; |
| } |
| } |