| /** |
| * @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'; |
| |
| type HookCallback = (el: Element) => void; |
| interface HookApi { |
| onAttached(callback: HookCallback): void; |
| } |
| interface PluginAPI { |
| hook(hookname: string): HookApi; |
| getPluginName(): string; |
| } |
| |
| /** @constructor */ |
| export class GrDomHooksManager { |
| private _hooks: Record<string, GrDomHook>; |
| |
| // TODO(TS): Convert type to GrPlugin. |
| private _plugin: PluginAPI; |
| |
| // TODO(TS): Convert type to GrPlugin. |
| 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]; |
| } |
| } |
| |
| interface PublicApi { |
| onAttached(callback: HookCallback): PublicApi; |
| onDetached(callback: HookCallback): PublicApi; |
| getAllAttached(): any; |
| getLastAttached(): any; |
| getModuleName(): string; |
| } |
| |
| /** @constructor */ |
| export class GrDomHook { |
| // TODO(TS): specify type for this |
| private _instances: unknown[] = []; |
| |
| private _attachCallbacks: HookCallback[] = []; |
| |
| private _detachCallbacks: HookCallback[] = []; |
| |
| private _moduleName: string; |
| |
| private _lastAttachedPromise: Promise<HookCallback> | 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: Element) { |
| const index = this._instances.indexOf(instance); |
| if (index !== -1) { |
| this._instances.splice(index, 1); |
| } |
| this._detachCallbacks.forEach(callback => callback(instance)); |
| } |
| |
| handleInstanceAttached(instance: Element) { |
| 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. |
| * |
| * @return |
| */ |
| getLastAttached() { |
| if (this._instances.length) { |
| return Promise.resolve(this._instances.slice(-1)[0]); |
| } |
| if (!this._lastAttachedPromise) { |
| let resolve: HookCallback; |
| const promise = new Promise(r => { |
| resolve = r; |
| this._attachCallbacks.push(resolve); |
| }); |
| this._lastAttachedPromise = promise.then(element => { |
| this._lastAttachedPromise = null; |
| const index = this._attachCallbacks.indexOf(resolve); |
| if (index !== -1) { |
| this._attachCallbacks.splice(index, 1); |
| } |
| return element; |
| }) as Promise<HookCallback>; |
| } |
| 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; |
| } |
| |
| getPublicAPI(): PublicApi { |
| return { |
| onAttached: this.onAttached.bind(this), |
| onDetached: this.onDetached.bind(this), |
| getAllAttached: this.getAllAttached.bind(this), |
| getLastAttached: this.getLastAttached.bind(this), |
| getModuleName: this.getModuleName.bind(this), |
| }; |
| } |
| } |