blob: dd76be41121039bc7adc111e6ad4d9aae5a06081 [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';
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;
}
}