/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { isServer } from 'lit';
/**
* A key to retrieve an `Attachable` element's `AttachableController` from a
* global `MutationObserver`.
*/
const ATTACHABLE_CONTROLLER = Symbol('attachableController');
let FOR_ATTRIBUTE_OBSERVER;
if (!isServer) {
/**
* A global `MutationObserver` that reacts to `for` attribute changes on
* `Attachable` elements. If the `for` attribute changes, the controller will
* re-attach to the new referenced element.
*/
FOR_ATTRIBUTE_OBSERVER = new MutationObserver((records) => {
for (const record of records) {
// When a control's `for` attribute changes, inform its
// `AttachableController` to update to a new control.
record.target[ATTACHABLE_CONTROLLER]?.hostConnected();
}
});
}
/**
* A controller that provides an implementation for `Attachable` elements.
*
* @example
* ```ts
* class MyElement extends LitElement implements Attachable {
* get control() { return this.attachableController.control; }
*
* private readonly attachableController = new AttachableController(
* this,
* (previousControl, newControl) => {
* previousControl?.removeEventListener('click', this.handleClick);
* newControl?.addEventListener('click', this.handleClick);
* }
* );
*
* // Implement remaining `Attachable` properties/methods that call the
* // controller's properties/methods.
* }
* ```
*/
export class AttachableController {
get htmlFor() {
return this.host.getAttribute('for');
}
set htmlFor(htmlFor) {
if (htmlFor === null) {
this.host.removeAttribute('for');
}
else {
this.host.setAttribute('for', htmlFor);
}
}
get control() {
if (this.host.hasAttribute('for')) {
if (!this.htmlFor || !this.host.isConnected) {
return null;
}
return this.host.getRootNode().querySelector(`#${this.htmlFor}`);
}
return this.currentControl || this.host.parentElement;
}
set control(control) {
if (control) {
this.attach(control);
}
else {
this.detach();
}
}
/**
* Creates a new controller for an `Attachable` element.
*
* @param host The `Attachable` element.
* @param onControlChange A callback with two parameters for the previous and
* next control. An `Attachable` element may perform setup or teardown
* logic whenever the control changes.
*/
constructor(host, onControlChange) {
this.host = host;
this.onControlChange = onControlChange;
this.currentControl = null;
host.addController(this);
host[ATTACHABLE_CONTROLLER] = this;
FOR_ATTRIBUTE_OBSERVER?.observe(host, { attributeFilter: ['for'] });
}
attach(control) {
if (control === this.currentControl) {
return;
}
this.setCurrentControl(control);
// When imperatively attaching, remove the `for` attribute so
// that the attached control is used instead of a referenced one.
this.host.removeAttribute('for');
}
detach() {
this.setCurrentControl(null);
// When imperatively detaching, add an empty `for=""` attribute. This will
// ensure the control is `null` rather than the `parentElement`.
this.host.setAttribute('for', '');
}
/** @private */
hostConnected() {
this.setCurrentControl(this.control);
}
/** @private */
hostDisconnected() {
this.setCurrentControl(null);
}
setCurrentControl(control) {
this.onControlChange(this.currentControl, control);
this.currentControl = control;
}
}
//# sourceMappingURL=attachable-controller.js.map