Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions src/controllers/mutation-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { type ReactiveController, type ReactiveControllerHost } from "lit";

/**
* Controller responsible for observing mutations within a host.
*/
export class MutationController<T = unknown> implements ReactiveController {
/**
* Value provided by the callback function, invoked when a mutation occurs.
*/
public value: T | undefined;

/**
* Determines whether the mutation observer can observe mutations. This is set to `true` when `disconnect`
* was called explicitly, and set to `false` when `observe` is called.
*/
private canObserve = true;

/**
* Configuration options.
*/
private readonly config?: Configuration<T>;

/**
* Host this controller is attached to.
*/
private readonly host: Node & ReactiveControllerHost;

/**
* Determines whether the mutation observer is currently observing the host.
*/
private isObserving = false;

/**
* Mutation observer monitoring changes.
*/
private readonly observer: MutationObserver;

/**
* Initializes a new instance of the {@link MutationController} class.
* @param host Host to attach to, and observe.
* @param config Configuration options.
*/
constructor(host: Node & ReactiveControllerHost, config?: Configuration<T>) {
this.host = host;
this.observer = new MutationObserver((mutations: MutationRecord[]) => this.handleChanges(mutations));
this.config = config;

this.host.addController(this);
}

/**
* Disconnects the mutation observer from the host.
*/
public disconnect(): void {
this.canObserve = false;
this.hostDisconnected();
}

/**
* @inheritdoc
*/
public hostConnected(): void {
if (this.canObserve) {
this.observe();
}
}

/**
* @inheritdoc
*/
public hostDisconnected(): void {
this.observer.disconnect();
this.isObserving = false;
}

/**
* @inheritdoc
*/
public hostUpdated(): void {
// Eagerly deliver any changes that happened during update.
const pendingRecords = this.observer.takeRecords();
if (pendingRecords.length) {
this.handleChanges(pendingRecords);
}
}

/**
* Observes mutations on the host. When mutations are already being observed, nothing happens.
*/
public observe(): void {
this.canObserve = true;

if (!this.isObserving) {
const options = Object.assign(
{
attributes: true,
childList: true,
subtree: true,
},
this.config?.options,
);

// Start observing.
this.observer.observe(this.host, options);
this.isObserving = true;

// Initialize the value.
this.handleChanges([]);
}
}

/**
* Invokes the callback function, and assigns the result to the controller's `value`.
* @param mutations Mutations that occurred.
*/
private handleChanges(mutations: MutationRecord[]): void {
if (this.config?.callback) {
const newValue = this.config.callback(mutations);
const oldValue = this.value;

this.value = newValue;

if (oldValue !== newValue && this.config.changed) {
this.config.changed();
}
}

this.host.requestUpdate();
}
}

/**
* Configuration options.
*/
type Configuration<T> = {
/**
* Callback function invoked when a mutation occurs.
* @param mutations Mutations that occurred.
* @returns The new value of the mutation controller.
*/
callback(mutations: MutationRecord[]): T;

changed?(): void;

/**
* Options provided to the mutation observer.
*/
options?: MutationObserverInit;
};
31 changes: 31 additions & 0 deletions src/controllers/option-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { type ReactiveControllerHost } from "lit";

import { MutationController } from "./mutation-controller.js";

/**
* Controller responsible for monitoring options, and option groups, within a host.
*/
export class OptionController extends MutationController<(HTMLOptionElement | HTMLOptGroupElement)[]> {
/**
* Initializes a new instance of the {@link OptionController} class.
* @param host Host to attach to, and observe.
* @param change Function invoked after the value changed.
*/
constructor(host: HTMLElement & ReactiveControllerHost, changed?: () => void) {
super(host, {
callback: () => {
return Array.from(host.querySelectorAll(":scope > option, :scope > sd-optgroup"));
},
changed: () => {
if (changed) {
changed();
}
},
options: {
attributes: true,
childList: true,
subtree: true,
},
});
}
}
17 changes: 4 additions & 13 deletions src/mixins/data-sourced.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { LitElement } from "lit";
import { property } from "lit/decorators.js";
import { SendToPropertyInspectorEvent } from "stream-deck";

import { FilteredMutationObserver, i18n, LocalizedMessage, localizedMessagePropertyOptions } from "../core";
import { OptionController } from "../controllers/option-controller";
import { i18n, LocalizedMessage, localizedMessagePropertyOptions } from "../core";
import streamDeckClient from "../stream-deck/stream-deck-client";

export type DataSourceResult = DataSourceResultItem[];
Expand All @@ -29,18 +30,8 @@ export const DataSourced = <T extends Constructor<LitElement>>(superClass: T) =>
class DataSourced extends superClass {
private _dataSourceInitialized = false;
private _itemsDataSource?: SendToPropertyInspectorEvent;
private _mutationObserver = new FilteredMutationObserver(["optgroup", "option"], () => this.refresh());

/**
* Initializes a new instance of the data source mixin.
* @param args The arguments.
* @constructor
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(...args: any[]) {
super(args);
this._mutationObserver.observe(this);
}
private readonly _optionController = new OptionController(this, () => this.refresh());

/**
* When specified, the items will be data sourced from the Stream Deck using the specified `dataSource` as the payload (sub) event.
Expand Down Expand Up @@ -174,7 +165,7 @@ export const DataSourced = <T extends Constructor<LitElement>>(superClass: T) =>
return items;
};

return this._mutationObserver.items.reduce(reducer, []);
return (this._optionController.value ?? []).reduce(reducer, []);
}

/**
Expand Down