blob: daf7d675b66e10be1a7f45ded78bcdb1b1a68b0e [file] [log] [blame]
/**
* @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),
};
}
}