@smui/common
Version:
Svelte Material UI - Common
387 lines (352 loc) • 13.1 kB
text/typescript
/**
* @license
* Copyright 2021 Google Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
import type { Constructor } from './types';
/**
* A class that can observe targets and perform cleanup logic. Classes may
* implement this using the `mdcObserver()` mixin.
*/
export interface MDCObserver {
/**
* Observe a target's properties for changes using the provided map of
* property names and observer functions.
*
* @template T The target type.
* @param target - The target to observe.
* @param observers - An object whose keys are target properties and values
* are observer functions that are called when the associated property
* changes.
* @return A cleanup function that can be called to unobserve the
* target.
*/
observe<T extends object>(
target: T,
observers: ObserverRecord<T, this>,
): () => void;
/**
* Enables or disables all observers for the provided target. Disabling
* observers will prevent them from being called until they are re-enabled.
*
* @param target - The target to enable or disable observers for.
* @param enabled - Whether or not observers should be called.
*/
setObserversEnabled(target: object, enabled: boolean): void;
/**
* Clean up all observers and stop listening for property changes.
*/
unobserve(): void;
}
/**
* A function used to observe property changes on a target.
*
* @template T The observed target type.
* @template K The observed property.
* @template This The `this` context of the observer function.
* @param current - The current value of the property.
* @param previous - The previous value of the property.
*/
export type Observer<
T extends object,
K extends keyof T = keyof T,
This = unknown,
> = (this: This, current: T[K], previous: T[K]) => void;
/**
* An object map whose keys are properties of a target to observe and values
* are `Observer` functions for each property.
*
* @template T The observed target type.
* @template This The `this` context of observer functions.
*/
export type ObserverRecord<T extends object, This = unknown> = {
[K in keyof T]?: Observer<T, K, This>;
};
/**
* Mixin to add `MDCObserver` functionality.
*
* @deprecated Prefer MDCObserverFoundation for stricter closure compliance.
* @return A class with `MDCObserver` functionality.
*/
export function mdcObserver(): Constructor<MDCObserver>;
/**
* Mixin to add `MDCObserver` functionality to a base class.
*
* @deprecated Prefer MDCObserverFoundation for stricter closure compliance.
* @template T Base class instance type. Specify this generic if the base class
* itself has generics that cannot be inferred.
* @template C Base class constructor type.
* @param baseClass - Base class.
* @return A class that extends the optional base class with `MDCObserver`
* functionality.
*/
export function mdcObserver<T, C extends Constructor<T>>(
baseClass: C,
): Constructor<MDCObserver> & Constructor<T> & C;
/**
* Mixin to add `MDCObserver` functionality to an optional base class.
*
* @deprecated Prefer MDCObserverFoundation for stricter closure compliance.
* @template C Optional base class constructor type.
* @param baseClass - Optional base class.
* @return A class that extends the optional base class with `MDCObserver`
* functionality.
*/
export function mdcObserver<C extends Constructor>(
baseClass: C = class {} as C,
) {
// Mixin classes cannot use private members and Symbol() cannot be used in 3P
// for IE11.
const unobserveMap = new WeakMap<MDCObserver, Function[]>();
return class MDCObserver extends baseClass implements MDCObserver {
observe<T extends object>(target: T, observers: ObserverRecord<T, this>) {
const cleanup: Function[] = [];
for (const property of Object.keys(observers) as Array<keyof T>) {
const observer = observers[property]!.bind(this);
cleanup.push(observeProperty(target, property, observer));
}
const unobserve = () => {
for (const cleanupFn of cleanup) {
cleanupFn();
}
const unobserves = unobserveMap.get(this) || [];
const index = unobserves.indexOf(unobserve);
if (index > -1) {
unobserves.splice(index, 1);
}
};
let unobserves = unobserveMap.get(this);
if (!unobserves) {
unobserves = [];
unobserveMap.set(this, unobserves);
}
unobserves.push(unobserve);
return unobserve;
}
setObserversEnabled(target: object, enabled: boolean) {
setObserversEnabled(target, enabled);
}
unobserve() {
// Iterate over a copy since unobserve() will remove themselves from the
// array
const unobserves = unobserveMap.get(this) || [];
for (const unobserve of [...unobserves]) {
unobserve();
}
}
};
}
/**
* A manager for observers listening to a target. A target's `prototype` is its
* `TargetObservers` instance.
*
* @template T The observed target type.
*/
interface TargetObservers<T extends object> {
/**
* Indicates whether or not observers for this target are enabled. If
* disabled, observers will not be called in response to target property
* changes.
*/
isEnabled: boolean;
/**
* Retrieves all observers for a given target property.
*
* @template K The target property key.
* @param key - The property to retrieve observers for.
* @return An array of observers for the provided target property.
*/
getObservers<K extends keyof T>(key: K): Array<Observer<T, K>>;
/**
* A Set of properties that have been installed (their getter/setter) replaced
* to connect with the `TargetObservers`. This prevents multiple installations
* of the same property.
*/
installedProperties: Set<keyof T>;
}
/**
* Observe a target's property for changes. When a property changes, the
* provided `Observer` function will be invoked with the properties current and
* previous values.
*
* The returned cleanup function will stop listening to changes for the
* provided `Observer`.
*
* @template T The observed target type.
* @template K The observed property.
* @param target - The target to observe.
* @param property - The property of the target to observe.
* @param observer - An observer function to invoke each time the property
* changes.
* @return A cleanup function that will stop observing changes for the provided
* `Observer`.
*/
export function observeProperty<T extends object, K extends keyof T>(
target: T,
property: K,
observer: Observer<T, K>,
) {
const targetObservers = installObserver(target, property);
const observers = targetObservers.getObservers(property);
observers.push(observer);
return () => {
observers.splice(observers.indexOf(observer), 1);
};
}
/**
* A Map of all `TargetObservers` that have been installed.
*/
const allTargetObservers = new WeakMap<object, TargetObservers<any>>();
/**
* Installs a `TargetObservers` for the provided target (if not already
* installed), and replaces the given property with a getter and setter that
* will respond to changes and call `TargetObservers`.
*
* Subsequent calls to `installObserver()` with the same target and property
* will not override the property's previously installed getter/setter.
*
* @template T The observed target type.
* @template K The observed property to create a getter/setter for.
* @param target - The target to observe.
* @param property - The property to create a getter/setter for, if needed.
* @return The installed `TargetObservers` for the provided target.
*/
function installObserver<T extends object, K extends keyof T>(
target: T,
property: K,
): TargetObservers<T> {
const observersMap = new Map<keyof T, Array<Observer<T>>>();
if (!allTargetObservers.has(target)) {
allTargetObservers.set(target, {
isEnabled: true,
getObservers(key) {
const observers = observersMap.get(key) || [];
if (!observersMap.has(key)) {
observersMap.set(key, observers);
}
return observers;
},
installedProperties: new Set(),
} as TargetObservers<T>);
}
const targetObservers = allTargetObservers.get(target)!;
if (targetObservers.installedProperties.has(property)) {
// The getter/setter has already been replaced for this property
return targetObservers;
}
// Retrieve (or create if it's a plain property) the original descriptor from
// the target...
const descriptor = getDescriptor(target, property) || {
configurable: true,
enumerable: true,
value: target[property],
writable: true,
};
// ...and create a copy that will be used for the observer.
const observedDescriptor = { ...descriptor };
let { get: descGet, set: descSet } = descriptor;
if ('value' in descriptor) {
// The descriptor is a simple value (not a getter/setter).
// For our observer descriptor that we copied, delete the value/writable
// properties, since they are incompatible with the get/set properties
// for descriptors.
delete observedDescriptor.value;
delete observedDescriptor.writable;
// Set up a simple getter...
let value = descriptor.value as T[K];
descGet = () => value;
// ...and setter (if the original property was writable).
if (descriptor.writable) {
descSet = (newValue) => {
value = newValue;
};
}
}
if (descGet) {
observedDescriptor.get = function (this: T) {
// `this as T` needed for closure conformance
// tslint:disable-next-line:no-unnecessary-type-assertion
return descGet!.call(this as T);
};
}
if (descSet) {
observedDescriptor.set = function (this: T, newValue: T[K]) {
// `thus as T` needed for closure conformance
// tslint:disable-next-line:no-unnecessary-type-assertion
const previous = descGet ? descGet.call(this as T) : newValue;
// tslint:disable-next-line:no-unnecessary-type-assertion
descSet!.call(this as T, newValue);
if (targetObservers.isEnabled && (!descGet || newValue !== previous)) {
for (const observer of targetObservers.getObservers(property)) {
observer(newValue, previous);
}
}
};
}
targetObservers.installedProperties.add(property);
Object.defineProperty(target, property, observedDescriptor);
return targetObservers;
}
/**
* Retrieves the descriptor for a property from the provided target. This
* function will walk up the target's prototype chain to search for the
* descriptor.
*
* @template T The target type.
* @template K The property type.
* @param target - The target to retrieve a descriptor from.
* @param property - The name of the property to retrieve a descriptor for.
* @return the descriptor, or undefined if it does not exist. Keep in mind that
* plain properties may not have a descriptor defined.
*/
export function getDescriptor<T extends object, K extends keyof T>(
target: T,
property: K,
) {
let descriptorTarget: object | null = target;
let descriptor: TypedPropertyDescriptor<T[K]> | undefined;
while (descriptorTarget) {
descriptor = Object.getOwnPropertyDescriptor(descriptorTarget, property);
if (descriptor) {
break;
}
// Walk up the instance's prototype chain in case the property is declared
// on a superclass.
descriptorTarget = Object.getPrototypeOf(descriptorTarget);
}
return descriptor;
}
/**
* Enables or disables all observers for a provided target. Changes to observed
* properties will not call any observers when disabled.
*
* @template T The observed target type.
* @param target - The target to enable or disable observers for.
* @param enabled - True to enable or false to disable observers.
*/
export function setObserversEnabled<T extends object>(
target: T,
enabled: boolean,
) {
const targetObservers = allTargetObservers.get(target);
if (targetObservers) {
targetObservers.isEnabled = enabled;
}
}