Provide plugin DOM hooks for components
In order to simplify custom components usage by plugins, provide DOM hooks for plugin-defined custom components. Previously DOM hook API was only provided for API-generated placeholder elements. Other changes: - endpoint insertions now can be performed at any point in time and will retrospectively attached to DOM for fully initialized endpoints as well - helper method for querying all hook instances - getLastAttached() method returns a promise to simplify singleton element setup and usage (e.g. popups) Change-Id: I1c95146f76ee52bea3b9e01b666112ce6448efcd
This commit is contained in:
		@@ -45,14 +45,14 @@ hook is a custom element that is instantiated for the plugin endpoint. In the
 | 
				
			|||||||
decoration case, a hook is set with a `content` attribute that points to the DOM
 | 
					decoration case, a hook is set with a `content` attribute that points to the DOM
 | 
				
			||||||
element.
 | 
					element.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1. Get the DOM hook API instance via `plugin.getDomHook(endpointName)`
 | 
					1. Get the DOM hook API instance via `plugin.hook(endpointName)`
 | 
				
			||||||
2. Set up an `onAttached` callback
 | 
					2. Set up an `onAttached` callback
 | 
				
			||||||
3. Callback is called when the hook element is created and inserted into DOM
 | 
					3. Callback is called when the hook element is created and inserted into DOM
 | 
				
			||||||
4. Use element.content to get UI element
 | 
					4. Use element.content to get UI element
 | 
				
			||||||
 | 
					
 | 
				
			||||||
``` js
 | 
					``` js
 | 
				
			||||||
Gerrit.install(function(plugin) {
 | 
					Gerrit.install(function(plugin) {
 | 
				
			||||||
  const domHook = plugin.getDomHook('reply-text');
 | 
					  const domHook = plugin.hook('reply-text');
 | 
				
			||||||
  domHook.onAttached(element => {
 | 
					  domHook.onAttached(element => {
 | 
				
			||||||
    if (!element.content) { return; }
 | 
					    if (!element.content) { return; }
 | 
				
			||||||
    // element.content is a reply dialog text area.
 | 
					    // element.content is a reply dialog text area.
 | 
				
			||||||
@@ -70,7 +70,7 @@ NOTE: TODO: Insert link to the full endpoints API.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
``` js
 | 
					``` js
 | 
				
			||||||
Gerrit.install(function(plugin) {
 | 
					Gerrit.install(function(plugin) {
 | 
				
			||||||
  const domHook = plugin.getDomHook('reply-text');
 | 
					  const domHook = plugin.hook('reply-text');
 | 
				
			||||||
  domHook.onAttached(element => {
 | 
					  domHook.onAttached(element => {
 | 
				
			||||||
    if (!element.content) { return; }
 | 
					    if (!element.content) { return; }
 | 
				
			||||||
    element.content.style.border = '1px red dashed';
 | 
					    element.content.style.border = '1px red dashed';
 | 
				
			||||||
@@ -86,7 +86,7 @@ option.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
``` js
 | 
					``` js
 | 
				
			||||||
Gerrit.install(function(plugin) {
 | 
					Gerrit.install(function(plugin) {
 | 
				
			||||||
  const domHook = plugin.getDomHook('header-title', {replace: true});
 | 
					  const domHook = plugin.hook('header-title', {replace: true});
 | 
				
			||||||
  domHook.onAttached(element => {
 | 
					  domHook.onAttached(element => {
 | 
				
			||||||
    element.appendChild(document.createElement('my-site-header'));
 | 
					    element.appendChild(document.createElement('my-site-header'));
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,49 +14,122 @@
 | 
				
			|||||||
(function(window) {
 | 
					(function(window) {
 | 
				
			||||||
  'use strict';
 | 
					  'use strict';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function GrDomHooks(plugin) {
 | 
					  function GrDomHooksManager(plugin) {
 | 
				
			||||||
    this._plugin = plugin;
 | 
					    this._plugin = plugin;
 | 
				
			||||||
    this._hooks = {};
 | 
					    this._hooks = {};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  GrDomHooks.prototype._getName = function(endpointName) {
 | 
					  GrDomHooksManager.prototype._getHookName = function(endpointName,
 | 
				
			||||||
    return this._plugin.getPluginName() + '-autogenerated-' + endpointName;
 | 
					      opt_moduleName) {
 | 
				
			||||||
 | 
					    if (opt_moduleName) {
 | 
				
			||||||
 | 
					      return endpointName + ' ' + opt_moduleName;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return this._plugin.getPluginName() + '-autogenerated-' + endpointName;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  GrDomHooks.prototype.getDomHook = function(endpointName) {
 | 
					  GrDomHooksManager.prototype.getDomHook = function(endpointName,
 | 
				
			||||||
    const hookName = this._getName(endpointName);
 | 
					      opt_moduleName) {
 | 
				
			||||||
 | 
					    const hookName = this._getHookName(endpointName, opt_moduleName);
 | 
				
			||||||
    if (!this._hooks[hookName]) {
 | 
					    if (!this._hooks[hookName]) {
 | 
				
			||||||
      this._hooks[hookName] = new GrDomHook(hookName);
 | 
					      this._hooks[hookName] = new GrDomHook(hookName, opt_moduleName);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return this._hooks[hookName];
 | 
					    return this._hooks[hookName];
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function GrDomHook(hookName) {
 | 
					  function GrDomHook(hookName, opt_moduleName) {
 | 
				
			||||||
 | 
					    this._instances = [];
 | 
				
			||||||
    this._callbacks = [];
 | 
					    this._callbacks = [];
 | 
				
			||||||
    // Expose to closure.
 | 
					    if (opt_moduleName) {
 | 
				
			||||||
    const callbacks = this._callbacks;
 | 
					      this._moduleName = opt_moduleName;
 | 
				
			||||||
    this._componentClass = Polymer({
 | 
					    } else {
 | 
				
			||||||
 | 
					      this._moduleName = hookName;
 | 
				
			||||||
 | 
					      this._createPlaceholder(hookName);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  GrDomHook.prototype._createPlaceholder = function(hookName) {
 | 
				
			||||||
 | 
					    Polymer({
 | 
				
			||||||
      is: hookName,
 | 
					      is: hookName,
 | 
				
			||||||
      properties: {
 | 
					      properties: {
 | 
				
			||||||
        plugin: Object,
 | 
					        plugin: Object,
 | 
				
			||||||
        content: Object,
 | 
					        content: Object,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      attached() {
 | 
					 | 
				
			||||||
        callbacks.forEach(callback => {
 | 
					 | 
				
			||||||
          callback(this);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  GrDomHook.prototype.handleInstanceDetached = function(instance) {
 | 
				
			||||||
 | 
					    const index = this._instances.indexOf(instance);
 | 
				
			||||||
 | 
					    if (index !== -1) {
 | 
				
			||||||
 | 
					      this._instances.splice(index, 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  GrDomHook.prototype.handleInstanceAttached = function(instance) {
 | 
				
			||||||
 | 
					    this._instances.push(instance);
 | 
				
			||||||
 | 
					    this._callbacks.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 {!Promise<!Element>}
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  GrDomHook.prototype.getLastAttached = function() {
 | 
				
			||||||
 | 
					    if (this._instances.length) {
 | 
				
			||||||
 | 
					      return Promise.resolve(this._instances.slice(-1)[0]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!this._lastAttachedPromise) {
 | 
				
			||||||
 | 
					      let resolve;
 | 
				
			||||||
 | 
					      const promise = new Promise(r => resolve = r);
 | 
				
			||||||
 | 
					      this._callbacks.push(resolve);
 | 
				
			||||||
 | 
					      this._lastAttachedPromise = promise.then(element => {
 | 
				
			||||||
 | 
					        this._lastAttachedPromise = null;
 | 
				
			||||||
 | 
					        const index = this._callbacks.indexOf(resolve);
 | 
				
			||||||
 | 
					        if (index !== -1) {
 | 
				
			||||||
 | 
					          this._callbacks.splice(index, 1);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return element;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return this._lastAttachedPromise;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Get all DOM hook elements.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  GrDomHook.prototype.getAllAttached = function() {
 | 
				
			||||||
 | 
					    return this._instances;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Install a new callback to invoke when a new instance of DOM hook element
 | 
				
			||||||
 | 
					   * is attached.
 | 
				
			||||||
 | 
					   * @param {function(Element)} callback
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
  GrDomHook.prototype.onAttached = function(callback) {
 | 
					  GrDomHook.prototype.onAttached = function(callback) {
 | 
				
			||||||
    this._callbacks.push(callback);
 | 
					    this._callbacks.push(callback);
 | 
				
			||||||
    return this;
 | 
					    return this;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Name of DOM hook element that will be installed into the endpoint.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
  GrDomHook.prototype.getModuleName = function() {
 | 
					  GrDomHook.prototype.getModuleName = function() {
 | 
				
			||||||
    return this._componentClass.prototype.is;
 | 
					    return this._moduleName;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  window.GrDomHooks = GrDomHooks;
 | 
					  GrDomHook.prototype.getPublicAPI = function() {
 | 
				
			||||||
 | 
					    const result = {};
 | 
				
			||||||
 | 
					    const exposedMethods = [
 | 
				
			||||||
 | 
					      'onAttached', 'getLastAttached', 'getAllAttached', 'getModuleName',
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					    for (const p of exposedMethods) {
 | 
				
			||||||
 | 
					      result[p] = this[p].bind(this);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return result;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  window.GrDomHook = GrDomHook;
 | 
				
			||||||
 | 
					  window.GrDomHooksManager = GrDomHooksManager;
 | 
				
			||||||
})(window);
 | 
					})(window);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,28 +33,110 @@ limitations under the License.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<script>
 | 
					<script>
 | 
				
			||||||
  suite('gr-dom-hooks tests', () => {
 | 
					  suite('gr-dom-hooks tests', () => {
 | 
				
			||||||
 | 
					    const PUBLIC_METHODS =
 | 
				
			||||||
 | 
					        ['onAttached', 'getLastAttached', 'getAllAttached', 'getModuleName'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let instance;
 | 
					    let instance;
 | 
				
			||||||
    let sandbox;
 | 
					    let sandbox;
 | 
				
			||||||
 | 
					    let hook;
 | 
				
			||||||
 | 
					    let hookInternal;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setup(() => {
 | 
					    setup(() => {
 | 
				
			||||||
      sandbox = sinon.sandbox.create();
 | 
					      sandbox = sinon.sandbox.create();
 | 
				
			||||||
      let plugin;
 | 
					      let plugin;
 | 
				
			||||||
      Gerrit.install(p => { plugin = p; }, '0.1',
 | 
					      Gerrit.install(p => { plugin = p; }, '0.1',
 | 
				
			||||||
          'http://test.com/plugins/testplugin/static/test.js');
 | 
					          'http://test.com/plugins/testplugin/static/test.js');
 | 
				
			||||||
      instance = new GrDomHooks(plugin);
 | 
					      instance = new GrDomHooksManager(plugin);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    teardown(() => {
 | 
					    teardown(() => {
 | 
				
			||||||
      sandbox.restore();
 | 
					      sandbox.restore();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    test('defines a Polymer components', () => {
 | 
					    suite('placeholder', () => {
 | 
				
			||||||
      const onAttachedSpy = sandbox.spy();
 | 
					      setup(()=>{
 | 
				
			||||||
      instance.getDomHook('foo-bar').onAttached(onAttachedSpy);
 | 
					        sandbox.stub(GrDomHook.prototype, '_createPlaceholder');
 | 
				
			||||||
      const hookName = Object.keys(instance._hooks).pop();
 | 
					        hookInternal = instance.getDomHook('foo-bar');
 | 
				
			||||||
      assert.equal(hookName, 'testplugin-autogenerated-foo-bar');
 | 
					        hook = hookInternal.getPublicAPI();
 | 
				
			||||||
      const el = fixture('basic').appendChild(document.createElement(hookName));
 | 
					      });
 | 
				
			||||||
      assert.isTrue(onAttachedSpy.calledWithExactly(el));
 | 
					
 | 
				
			||||||
 | 
					      test('public hook API has only public methods', () => {
 | 
				
			||||||
 | 
					        assert.deepEqual(Object.keys(hook), PUBLIC_METHODS);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      test('registers placeholder class', () => {
 | 
				
			||||||
 | 
					        assert.isTrue(hookInternal._createPlaceholder.calledWithExactly(
 | 
				
			||||||
 | 
					            'testplugin-autogenerated-foo-bar'));
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      test('getModuleName()', () => {
 | 
				
			||||||
 | 
					        const hookName = Object.keys(instance._hooks).pop();
 | 
				
			||||||
 | 
					        assert.equal(hookName, 'testplugin-autogenerated-foo-bar');
 | 
				
			||||||
 | 
					        assert.equal(hook.getModuleName(), 'testplugin-autogenerated-foo-bar');
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    suite('custom element', () => {
 | 
				
			||||||
 | 
					      setup(() => {
 | 
				
			||||||
 | 
					        hookInternal = instance.getDomHook('foo-bar', 'my-el');
 | 
				
			||||||
 | 
					        hook = hookInternal.getPublicAPI();
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      test('public hook API has only public methods', () => {
 | 
				
			||||||
 | 
					        assert.deepEqual(Object.keys(hook), PUBLIC_METHODS);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      test('getModuleName()', () => {
 | 
				
			||||||
 | 
					        const hookName = Object.keys(instance._hooks).pop();
 | 
				
			||||||
 | 
					        assert.equal(hookName, 'foo-bar my-el');
 | 
				
			||||||
 | 
					        assert.equal(hook.getModuleName(), 'my-el');
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      test('onAttached', () => {
 | 
				
			||||||
 | 
					        const onAttachedSpy = sandbox.spy();
 | 
				
			||||||
 | 
					        hook.onAttached(onAttachedSpy);
 | 
				
			||||||
 | 
					        const [el1, el2] = [
 | 
				
			||||||
 | 
					          document.createElement(hook.getModuleName()),
 | 
				
			||||||
 | 
					          document.createElement(hook.getModuleName()),
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					        hookInternal.handleInstanceAttached(el1);
 | 
				
			||||||
 | 
					        hookInternal.handleInstanceAttached(el2);
 | 
				
			||||||
 | 
					        assert.isTrue(onAttachedSpy.firstCall.calledWithExactly(el1));
 | 
				
			||||||
 | 
					        assert.isTrue(onAttachedSpy.secondCall.calledWithExactly(el2));
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      test('getAllAttached', () => {
 | 
				
			||||||
 | 
					        const [el1, el2] = [
 | 
				
			||||||
 | 
					          document.createElement(hook.getModuleName()),
 | 
				
			||||||
 | 
					          document.createElement(hook.getModuleName()),
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					        el1.textContent = 'one';
 | 
				
			||||||
 | 
					        el2.textContent = 'two';
 | 
				
			||||||
 | 
					        hookInternal.handleInstanceAttached(el1);
 | 
				
			||||||
 | 
					        hookInternal.handleInstanceAttached(el2);
 | 
				
			||||||
 | 
					        assert.deepEqual([el1, el2], hook.getAllAttached());
 | 
				
			||||||
 | 
					        hookI.handleInstanceDetached(el1);
 | 
				
			||||||
 | 
					        assert.deepEqual([el2], hook.getAllAttached());
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      test('getLastAttached', () => {
 | 
				
			||||||
 | 
					        const beforeAttachedPromise = hook.getLastAttached().then(
 | 
				
			||||||
 | 
					            el => assert.strictEqual(el1, el));
 | 
				
			||||||
 | 
					        const [el1, el2] = [
 | 
				
			||||||
 | 
					          document.createElement(hook.getModuleName()),
 | 
				
			||||||
 | 
					          document.createElement(hook.getModuleName()),
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					        el1.textContent = 'one';
 | 
				
			||||||
 | 
					        el2.textContent = 'two';
 | 
				
			||||||
 | 
					        hookInternal.handleInstanceAttached(el1);
 | 
				
			||||||
 | 
					        hookInternal.handleInstanceAttached(el2);
 | 
				
			||||||
 | 
					        const afterAttachedPromise = hook.getLastAttached().then(
 | 
				
			||||||
 | 
					            el => assert.strictEqual(el2, el));
 | 
				
			||||||
 | 
					        return Promise.all([
 | 
				
			||||||
 | 
					          beforeAttachedPromise,
 | 
				
			||||||
 | 
					          afterAttachedPromise,
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,6 +19,17 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    properties: {
 | 
					    properties: {
 | 
				
			||||||
      name: String,
 | 
					      name: String,
 | 
				
			||||||
 | 
					      /** @type {!Map} */
 | 
				
			||||||
 | 
					      _domHooks: {
 | 
				
			||||||
 | 
					        type: Map,
 | 
				
			||||||
 | 
					        value() { return new Map(); },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    detached() {
 | 
				
			||||||
 | 
					      for (const [el, domHook] of this._domHooks) {
 | 
				
			||||||
 | 
					        domHook.handleInstanceDetached(el);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _import(url) {
 | 
					    _import(url) {
 | 
				
			||||||
@@ -31,33 +42,48 @@
 | 
				
			|||||||
      const el = document.createElement(name);
 | 
					      const el = document.createElement(name);
 | 
				
			||||||
      el.plugin = plugin;
 | 
					      el.plugin = plugin;
 | 
				
			||||||
      el.content = this.getContentChildren()[0];
 | 
					      el.content = this.getContentChildren()[0];
 | 
				
			||||||
      return Polymer.dom(this.root).appendChild(el);
 | 
					      this._appendChild(el);
 | 
				
			||||||
 | 
					      return el;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    _initReplacement(name, plugin) {
 | 
					    _initReplacement(name, plugin) {
 | 
				
			||||||
      this.getContentChildren().forEach(node => node.remove());
 | 
					      this.getContentChildren().forEach(node => node.remove());
 | 
				
			||||||
      const el = document.createElement(name);
 | 
					      const el = document.createElement(name);
 | 
				
			||||||
      el.plugin = plugin;
 | 
					      el.plugin = plugin;
 | 
				
			||||||
      return Polymer.dom(this.root).appendChild(el);
 | 
					      this._appendChild(el);
 | 
				
			||||||
 | 
					      return el;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _appendChild(el) {
 | 
				
			||||||
 | 
					      Polymer.dom(this.root).appendChild(el);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    _initModule({moduleName, plugin, type, domHook}) {
 | 
				
			||||||
 | 
					      let el;
 | 
				
			||||||
 | 
					      switch (type) {
 | 
				
			||||||
 | 
					        case 'decorate':
 | 
				
			||||||
 | 
					          el = this._initDecoration(moduleName, plugin);
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        case 'replace':
 | 
				
			||||||
 | 
					          el = this._initReplacement(moduleName, plugin);
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (el) {
 | 
				
			||||||
 | 
					        domHook.handleInstanceAttached(el);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this._domHooks.set(el, domHook);
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ready() {
 | 
					    ready() {
 | 
				
			||||||
 | 
					      Gerrit._endpoints.onNewEndpoint(this.name, this._initModule.bind(this));
 | 
				
			||||||
      Gerrit.awaitPluginsLoaded().then(() => Promise.all(
 | 
					      Gerrit.awaitPluginsLoaded().then(() => Promise.all(
 | 
				
			||||||
          Gerrit._endpoints.getPlugins(this.name).map(
 | 
					          Gerrit._endpoints.getPlugins(this.name).map(
 | 
				
			||||||
              pluginUrl => this._import(pluginUrl)))
 | 
					              pluginUrl => this._import(pluginUrl)))
 | 
				
			||||||
      ).then(() => {
 | 
					      ).then(() =>
 | 
				
			||||||
        const modulesData = Gerrit._endpoints.getDetails(this.name);
 | 
					        Gerrit._endpoints
 | 
				
			||||||
        for (const {moduleName, plugin, type} of modulesData) {
 | 
					            .getDetails(this.name)
 | 
				
			||||||
          switch (type) {
 | 
					            .forEach(this._initModule, this)
 | 
				
			||||||
            case 'decorate':
 | 
					      );
 | 
				
			||||||
              this._initDecoration(moduleName, plugin);
 | 
					 | 
				
			||||||
              break;
 | 
					 | 
				
			||||||
            case 'replace':
 | 
					 | 
				
			||||||
              this._initReplacement(moduleName, plugin);
 | 
					 | 
				
			||||||
              break;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
})();
 | 
					})();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,10 +34,20 @@ limitations under the License.
 | 
				
			|||||||
    let sandbox;
 | 
					    let sandbox;
 | 
				
			||||||
    let element;
 | 
					    let element;
 | 
				
			||||||
    let plugin;
 | 
					    let plugin;
 | 
				
			||||||
 | 
					    let domHookStub;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setup(done => {
 | 
					    setup(done => {
 | 
				
			||||||
 | 
					      Gerrit._endpoints = new GrPluginEndpoints();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      sandbox = sinon.sandbox.create();
 | 
					      sandbox = sinon.sandbox.create();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      domHookStub = {
 | 
				
			||||||
 | 
					        handleInstanceAttached: sandbox.stub(),
 | 
				
			||||||
 | 
					        handleInstanceDetached: sandbox.stub(),
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      sandbox.stub(
 | 
				
			||||||
 | 
					          GrDomHooksManager.prototype, 'getDomHook').returns(domHookStub);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // NB: Order is important.
 | 
					      // NB: Order is important.
 | 
				
			||||||
      Gerrit.install(p => {
 | 
					      Gerrit.install(p => {
 | 
				
			||||||
        plugin = p;
 | 
					        plugin = p;
 | 
				
			||||||
@@ -45,11 +55,12 @@ limitations under the License.
 | 
				
			|||||||
        plugin.registerCustomComponent('foo', 'other-module', {replace: true});
 | 
					        plugin.registerCustomComponent('foo', 'other-module', {replace: true});
 | 
				
			||||||
      }, '0.1', 'http://some/plugin/url.html');
 | 
					      }, '0.1', 'http://some/plugin/url.html');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
 | 
				
			||||||
      sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
 | 
					      sandbox.stub(Gerrit, 'awaitPluginsLoaded').returns(Promise.resolve());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      element = fixture('basic');
 | 
					      element = fixture('basic');
 | 
				
			||||||
      sandbox.stub(element, '_initDecoration');
 | 
					      sandbox.stub(element, '_initDecoration').returns({});
 | 
				
			||||||
      sandbox.stub(element, '_initReplacement');
 | 
					      sandbox.stub(element, '_initReplacement').returns({});
 | 
				
			||||||
      sandbox.stub(element, 'importHref', (url, resolve) => resolve());
 | 
					      sandbox.stub(element, 'importHref', (url, resolve) => resolve());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      flush(done);
 | 
					      flush(done);
 | 
				
			||||||
@@ -65,13 +76,39 @@ limitations under the License.
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    test('inits decoration dom hook', () => {
 | 
					    test('inits decoration dom hook', () => {
 | 
				
			||||||
      assert.isTrue(
 | 
					      assert.strictEqual(
 | 
				
			||||||
          element._initDecoration.calledWith('some-module', plugin));
 | 
					          element._initDecoration.lastCall.args[0], 'some-module');
 | 
				
			||||||
 | 
					      assert.strictEqual(
 | 
				
			||||||
 | 
					          element._initDecoration.lastCall.args[1], plugin);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    test('inits replacement dom hook', () => {
 | 
					    test('inits replacement dom hook', () => {
 | 
				
			||||||
      assert.isTrue(
 | 
					      assert.strictEqual(
 | 
				
			||||||
          element._initReplacement.calledWith('other-module', plugin));
 | 
					          element._initReplacement.lastCall.args[0], 'other-module');
 | 
				
			||||||
 | 
					      assert.strictEqual(
 | 
				
			||||||
 | 
					          element._initReplacement.lastCall.args[1], plugin);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    test('calls dom hook handleInstanceAttached', () => {
 | 
				
			||||||
 | 
					      assert.equal(domHookStub.handleInstanceAttached.callCount, 2);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    test('calls dom hook handleInstanceDetached', () => {
 | 
				
			||||||
 | 
					      element.detached();
 | 
				
			||||||
 | 
					      assert.equal(domHookStub.handleInstanceDetached.callCount, 2);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    test('installs modules on late registration', done => {
 | 
				
			||||||
 | 
					      domHookStub.handleInstanceAttached.reset();
 | 
				
			||||||
 | 
					      plugin.registerCustomComponent('foo', 'noob-noob');
 | 
				
			||||||
 | 
					      flush(() => {
 | 
				
			||||||
 | 
					        assert.equal(domHookStub.handleInstanceAttached.callCount, 1);
 | 
				
			||||||
 | 
					        assert.strictEqual(
 | 
				
			||||||
 | 
					            element._initDecoration.lastCall.args[0], 'noob-noob');
 | 
				
			||||||
 | 
					        assert.strictEqual(
 | 
				
			||||||
 | 
					            element._initDecoration.lastCall.args[1], plugin);
 | 
				
			||||||
 | 
					        done();
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,7 +22,7 @@
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  GrThemeApi.prototype.setHeaderLogoAndTitle = function(logoUrl, title) {
 | 
					  GrThemeApi.prototype.setHeaderLogoAndTitle = function(logoUrl, title) {
 | 
				
			||||||
    this.plugin.getDomHook('header-title', {replace: true}).onAttached(
 | 
					    this.plugin.hook('header-title', {replace: true}).onAttached(
 | 
				
			||||||
        element => {
 | 
					        element => {
 | 
				
			||||||
          const customHeader =
 | 
					          const customHeader =
 | 
				
			||||||
                document.createElement('gr-custom-plugin-header');
 | 
					                document.createElement('gr-custom-plugin-header');
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -49,7 +49,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  GrChangeReplyInterface.prototype.addReplyTextChangedCallback =
 | 
					  GrChangeReplyInterface.prototype.addReplyTextChangedCallback =
 | 
				
			||||||
    function(handler) {
 | 
					    function(handler) {
 | 
				
			||||||
      this.plugin.getDomHook('reply-text').onAttached(el => {
 | 
					      this.plugin.hook('reply-text').onAttached(el => {
 | 
				
			||||||
        if (!el.content) { return; }
 | 
					        if (!el.content) { return; }
 | 
				
			||||||
        el.content.addEventListener('value-changed', e => {
 | 
					        el.content.addEventListener('value-changed', e => {
 | 
				
			||||||
          handler(e.detail.value);
 | 
					          handler(e.detail.value);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,19 +16,32 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  function GrPluginEndpoints() {
 | 
					  function GrPluginEndpoints() {
 | 
				
			||||||
    this._endpoints = {};
 | 
					    this._endpoints = {};
 | 
				
			||||||
 | 
					    this._callbacks = {};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  GrPluginEndpoints.prototype.onNewEndpoint = function(endpoint, callback) {
 | 
				
			||||||
 | 
					    if (!this._callbacks[endpoint]) {
 | 
				
			||||||
 | 
					      this._callbacks[endpoint] = [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this._callbacks[endpoint].push(callback);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  GrPluginEndpoints.prototype.registerModule = function(plugin, endpoint, type,
 | 
					  GrPluginEndpoints.prototype.registerModule = function(plugin, endpoint, type,
 | 
				
			||||||
      moduleName) {
 | 
					      moduleName, domHook) {
 | 
				
			||||||
    if (!this._endpoints[endpoint]) {
 | 
					    if (!this._endpoints[endpoint]) {
 | 
				
			||||||
      this._endpoints[endpoint] = [];
 | 
					      this._endpoints[endpoint] = [];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    this._endpoints[endpoint].push({
 | 
					    const moduleInfo = {
 | 
				
			||||||
      moduleName,
 | 
					      moduleName,
 | 
				
			||||||
      plugin,
 | 
					      plugin,
 | 
				
			||||||
      pluginUrl: plugin._url,
 | 
					      pluginUrl: plugin._url,
 | 
				
			||||||
      type,
 | 
					      type,
 | 
				
			||||||
    });
 | 
					      domHook,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    this._endpoints[endpoint].push(moduleInfo);
 | 
				
			||||||
 | 
					    if (Gerrit._arePluginsLoaded() && this._callbacks[endpoint]) {
 | 
				
			||||||
 | 
					      this._callbacks[endpoint].forEach(callback => callback(moduleInfo));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
@@ -44,6 +57,7 @@
 | 
				
			|||||||
   *   plugin: Plugin,
 | 
					   *   plugin: Plugin,
 | 
				
			||||||
   *   pluginUrl: String,
 | 
					   *   pluginUrl: String,
 | 
				
			||||||
   *   type: EndpointType,
 | 
					   *   type: EndpointType,
 | 
				
			||||||
 | 
					   *   domHook: !Object
 | 
				
			||||||
   * }>}
 | 
					   * }>}
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
  GrPluginEndpoints.prototype.getDetails = function(name, opt_options) {
 | 
					  GrPluginEndpoints.prototype.getDetails = function(name, opt_options) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,16 +29,21 @@ limitations under the License.
 | 
				
			|||||||
    let instance;
 | 
					    let instance;
 | 
				
			||||||
    let pluginFoo;
 | 
					    let pluginFoo;
 | 
				
			||||||
    let pluginBar;
 | 
					    let pluginBar;
 | 
				
			||||||
 | 
					    let domHook;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setup(() => {
 | 
					    setup(() => {
 | 
				
			||||||
      sandbox = sinon.sandbox.create();
 | 
					      sandbox = sinon.sandbox.create();
 | 
				
			||||||
 | 
					      domHook = {};
 | 
				
			||||||
      instance = new GrPluginEndpoints();
 | 
					      instance = new GrPluginEndpoints();
 | 
				
			||||||
      Gerrit.install(p => { pluginFoo = p; }, '0.1',
 | 
					      Gerrit.install(p => { pluginFoo = p; }, '0.1',
 | 
				
			||||||
          'http://test.com/plugins/testplugin/static/foo.html');
 | 
					          'http://test.com/plugins/testplugin/static/foo.html');
 | 
				
			||||||
      instance.registerModule(pluginFoo, 'a-place', 'decorate', 'foo-module');
 | 
					      instance.registerModule(
 | 
				
			||||||
 | 
					          pluginFoo, 'a-place', 'decorate', 'foo-module', domHook);
 | 
				
			||||||
      Gerrit.install(p => { pluginBar = p; }, '0.1',
 | 
					      Gerrit.install(p => { pluginBar = p; }, '0.1',
 | 
				
			||||||
          'http://test.com/plugins/testplugin/static/bar.html');
 | 
					          'http://test.com/plugins/testplugin/static/bar.html');
 | 
				
			||||||
      instance.registerModule(pluginBar, 'a-place', 'style', 'bar-module');
 | 
					      instance.registerModule(
 | 
				
			||||||
 | 
					          pluginBar, 'a-place', 'style', 'bar-module', domHook);
 | 
				
			||||||
 | 
					      sandbox.stub(Gerrit, '_arePluginsLoaded').returns(true);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    teardown(() => {
 | 
					    teardown(() => {
 | 
				
			||||||
@@ -52,12 +57,14 @@ limitations under the License.
 | 
				
			|||||||
          plugin: pluginFoo,
 | 
					          plugin: pluginFoo,
 | 
				
			||||||
          pluginUrl: pluginFoo._url,
 | 
					          pluginUrl: pluginFoo._url,
 | 
				
			||||||
          type: 'decorate',
 | 
					          type: 'decorate',
 | 
				
			||||||
 | 
					          domHook,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          moduleName: 'bar-module',
 | 
					          moduleName: 'bar-module',
 | 
				
			||||||
          plugin: pluginBar,
 | 
					          plugin: pluginBar,
 | 
				
			||||||
          pluginUrl: pluginBar._url,
 | 
					          pluginUrl: pluginBar._url,
 | 
				
			||||||
          type: 'style',
 | 
					          type: 'style',
 | 
				
			||||||
 | 
					          domHook,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      ]);
 | 
					      ]);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@@ -69,6 +76,7 @@ limitations under the License.
 | 
				
			|||||||
          plugin: pluginBar,
 | 
					          plugin: pluginBar,
 | 
				
			||||||
          pluginUrl: pluginBar._url,
 | 
					          pluginUrl: pluginBar._url,
 | 
				
			||||||
          type: 'style',
 | 
					          type: 'style',
 | 
				
			||||||
 | 
					          domHook,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      ]);
 | 
					      ]);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@@ -82,6 +90,7 @@ limitations under the License.
 | 
				
			|||||||
              plugin: pluginFoo,
 | 
					              plugin: pluginFoo,
 | 
				
			||||||
              pluginUrl: pluginFoo._url,
 | 
					              pluginUrl: pluginFoo._url,
 | 
				
			||||||
              type: 'decorate',
 | 
					              type: 'decorate',
 | 
				
			||||||
 | 
					              domHook,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
          ]);
 | 
					          ]);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@@ -95,5 +104,19 @@ limitations under the License.
 | 
				
			|||||||
      assert.deepEqual(
 | 
					      assert.deepEqual(
 | 
				
			||||||
          instance.getPlugins('a-place'), [pluginFoo._url, pluginBar._url]);
 | 
					          instance.getPlugins('a-place'), [pluginFoo._url, pluginBar._url]);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    test('onNewEndpoint', () => {
 | 
				
			||||||
 | 
					      const newModuleStub = sandbox.stub();
 | 
				
			||||||
 | 
					      instance.onNewEndpoint('a-place', newModuleStub);
 | 
				
			||||||
 | 
					      instance.registerModule(
 | 
				
			||||||
 | 
					          pluginFoo, 'a-place', 'replace', 'zaz-module', domHook);
 | 
				
			||||||
 | 
					      assert.deepEqual(newModuleStub.lastCall.args[0], {
 | 
				
			||||||
 | 
					        moduleName: 'zaz-module',
 | 
				
			||||||
 | 
					        plugin: pluginFoo,
 | 
				
			||||||
 | 
					        pluginUrl: pluginFoo._url,
 | 
				
			||||||
 | 
					        type: 'replace',
 | 
				
			||||||
 | 
					        domHook,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,8 +55,7 @@
 | 
				
			|||||||
  window.$wnd = window;
 | 
					  window.$wnd = window;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function Plugin(opt_url) {
 | 
					  function Plugin(opt_url) {
 | 
				
			||||||
    this._generatedHookNames = [];
 | 
					    this._domHooks = new GrDomHooksManager(this);
 | 
				
			||||||
    this._domHooks = new GrDomHooks(this);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!opt_url) {
 | 
					    if (!opt_url) {
 | 
				
			||||||
      console.warn('Plugin not being loaded from /plugins base path.',
 | 
					      console.warn('Plugin not being loaded from /plugins base path.',
 | 
				
			||||||
@@ -93,11 +92,22 @@
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Plugin.prototype.registerCustomComponent = function(
 | 
					  Plugin.prototype.registerCustomComponent = function(
 | 
				
			||||||
      endpointName, moduleName, opt_options) {
 | 
					      endpointName, opt_moduleName, opt_options) {
 | 
				
			||||||
    const type = opt_options && opt_options.replace ?
 | 
					    const type = opt_options && opt_options.replace ?
 | 
				
			||||||
          EndpointType.REPLACE : EndpointType.DECORATE;
 | 
					          EndpointType.REPLACE : EndpointType.DECORATE;
 | 
				
			||||||
 | 
					    const hook = this._domHooks.getDomHook(endpointName, opt_moduleName);
 | 
				
			||||||
 | 
					    const moduleName = opt_moduleName || hook.getModuleName();
 | 
				
			||||||
    Gerrit._endpoints.registerModule(
 | 
					    Gerrit._endpoints.registerModule(
 | 
				
			||||||
        this, endpointName, type, moduleName);
 | 
					        this, endpointName, type, moduleName, hook);
 | 
				
			||||||
 | 
					    return hook.getPublicAPI();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Returns instance of DOM hook API for endpoint. Creates a placeholder
 | 
				
			||||||
 | 
					   * element for the first call.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  Plugin.prototype.hook = function(endpointName, opt_options) {
 | 
				
			||||||
 | 
					    return this.registerCustomComponent(endpointName, undefined, opt_options);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Plugin.prototype.getServerInfo = function() {
 | 
					  Plugin.prototype.getServerInfo = function() {
 | 
				
			||||||
@@ -166,14 +176,6 @@
 | 
				
			|||||||
    return new GrAttributeHelper(element);
 | 
					    return new GrAttributeHelper(element);
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Plugin.prototype.getDomHook = function(endpointName, opt_options) {
 | 
					 | 
				
			||||||
    const hook = this._domHooks.getDomHook(endpointName);
 | 
					 | 
				
			||||||
    const moduleName = hook.getModuleName();
 | 
					 | 
				
			||||||
    const type = opt_options && opt_options.type || EndpointType.DECORATE;
 | 
					 | 
				
			||||||
    Gerrit._endpoints.registerModule(this, endpointName, type, moduleName);
 | 
					 | 
				
			||||||
    return hook;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const Gerrit = window.Gerrit || {};
 | 
					  const Gerrit = window.Gerrit || {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Number of plugins to initialize, -1 means 'not yet known'.
 | 
					  // Number of plugins to initialize, -1 means 'not yet known'.
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user