UNPKG

aspen-decorations

Version:

Complex styling for react-aspen w/ inheritance and negations

224 lines (186 loc) 7.48 kB
import { Directory, FileEntry, FileType, Root } from 'aspen-core' import { DisposablesComposite, IDisposable } from 'notificar' import { Decoration } from './Decoration' import { ClasslistComposite, DecorationComposite, DecorationCompositeType } from './DecorationComposite' interface IDecorationMeta { /** * All the decorations that will apply to self * * Unless applicables conflict (by having specific Decoration applied or negated) they "share" the data of their parent's `inheritable` */ applicable: DecorationComposite /** * All the decorations that will apply to children (infinitely unless explicitly negated by a target on the way down) * * Unless inheritables conflict (by having specific Decoration applied or negated) they "share" the data of their parent's `inheritable` */ inheritable: DecorationComposite } /** * NOTES FOR CONTRIBUTORS: * - A Target is a directory or a file entry * - A Target is a DirectTarget when it is explicitly listed in a Decoration's application or negation list * - As opposed to DirectTarget, general targets are implicit targets when they simply inherit from their parent's inhertiable DecorationComposite * - All general targets "point to" their first parent's `inheritable` composite see docs for `IDecorationMeta.inheritable` */ export class DecorationsManager implements IDisposable { private decorations: Map<Decoration, IDisposable> = new Map() private decorationsMeta: WeakMap<FileEntry | Directory, IDecorationMeta> = new WeakMap() private disposables: DisposablesComposite = new DisposablesComposite() private disposed = false constructor(root: Root) { // if the object is actually Root, but not of same version this condition will likely be true if (!(root instanceof Root)) { throw new TypeError('Unexpected object type. Expected `Root`. Make sure you are using the latest version of `aspen-decorations` to avoid conflicts') } // this will act as "seed" (base) for rest of the decorations to come this.decorationsMeta.set(root, { applicable: new DecorationComposite(root, DecorationCompositeType.Applicable, null), inheritable: new DecorationComposite(root, DecorationCompositeType.Inheritable, null), }) this.disposables.add(root.onDidChangeParent(this.switchParent)) this.disposables.add(root.onDidDispose(this.decorationsMeta.delete.bind(this.decorationsMeta))) } /** * Permanently disengages the decoration system from the tree */ public dispose(): void { for (const [decoration] of this.decorations) { this.removeDecoration(decoration) } this.disposables.dispose() this.disposed = true } /** * Adds the given `Decoration` to the tree * * `Decoration`s have no effect unless they are targetted at item(s). Use `Decoration#addTarget` to have them render in the filetree */ public addDecoration(decoration: Decoration): void { if (this.disposed) { throw new Error(`DecorationManager disposed`) } if (this.decorations.has(decoration)) { return } const disposable = new DisposablesComposite() disposable.add(decoration.onDidAddTarget(this.targetDecoration)) disposable.add(decoration.onDidRemoveTarget(this.unTargetDecoration)) disposable.add(decoration.onDidNegateTarget(this.negateDecoration)) disposable.add(decoration.onDidUnNegateTarget(this.unNegateDecoration)) this.decorations.set(decoration, disposable) for (const [target] of decoration.appliedTargets) { this.targetDecoration(decoration, target) } for (const [target] of decoration.negatedTargets) { this.negateDecoration(decoration, target) } } /** * Removes a `Decoration` from the tree * * Note that this "removes" entire `Decoration` from tree but the said `Decoration`'s targets are still left intact. * * Calling `DecorationManager#addDecoration` with the same `Decoration` will undo the removal if the targets are left unchanged. */ public removeDecoration(decoration: Decoration): void { const decorationSubscriptions = this.decorations.get(decoration) if (!decorationSubscriptions) { return } for (const [target] of decoration.appliedTargets) { const meta = this.decorationsMeta.get(target) if (meta) { meta.applicable.remove(decoration) if (meta.inheritable) { meta.inheritable.remove(decoration) } } } for (const [target] of decoration.negatedTargets) { const meta = this.decorationsMeta.get(target) if (meta) { meta.applicable.unNegate(decoration) if (meta.inheritable) { meta.inheritable.unNegate(decoration) } } } decorationSubscriptions.dispose() this.decorations.delete(decoration) } /** * Returns resolved decorations for given item * * Resolution includes taking inheritances into consideration, along with any negations that may void some or all of inheritances */ public getDecorations(item: FileEntry | Directory): ClasslistComposite { if (!item || (item.type !== FileType.File && item.type !== FileType.Directory)) { return null } const decMeta = this.getDecorationData(item) if (decMeta) { return decMeta.applicable.compositeCssClasslist } return null } /** * @internal */ public getDecorationData(item: FileEntry | Directory): IDecorationMeta { if (this.disposed) { return null } const meta = this.decorationsMeta.get(item) if (meta) { return meta } // if we make it here that means the item was not a DirectTarget and will simply point to parent's `inheritable` composite (unless is negated) if (!item || !item.parent) { return null } const parentMeta = this.getDecorationData(item.parent) if (parentMeta) { const ownMeta: IDecorationMeta = { applicable: new DecorationComposite(item, DecorationCompositeType.Applicable, parentMeta.inheritable), inheritable: item.type === FileType.Directory ? new DecorationComposite(item, DecorationCompositeType.Inheritable, parentMeta.inheritable) : null, } this.decorationsMeta.set(item, ownMeta) return ownMeta } return null } private targetDecoration = (decoration: Decoration, target: FileEntry | Directory): void => { const { applicable, inheritable } = this.getDecorationData(target) applicable.add(decoration) if (inheritable) { inheritable.add(decoration) } } private unTargetDecoration = (decoration: Decoration, target: FileEntry | Directory): void => { const { applicable, inheritable } = this.getDecorationData(target) applicable.remove(decoration) if (inheritable) { inheritable.remove(decoration) } } private negateDecoration = (decoration: Decoration, target: FileEntry | Directory): void => { const { applicable, inheritable } = this.getDecorationData(target) applicable.negate(decoration) if (inheritable) { inheritable.negate(decoration) } } private unNegateDecoration = (decoration: Decoration, target: FileEntry | Directory): void => { const { applicable, inheritable } = this.getDecorationData(target) applicable.unNegate(decoration) if (inheritable) { inheritable.unNegate(decoration) } } private switchParent = (target: FileEntry | Directory, prevParent: Directory, newParent: Directory): void => { const ownMeta = this.decorationsMeta.get(target) if (!ownMeta) { return } const newParentMeta = this.getDecorationData(newParent) ownMeta.applicable.changeParent(newParentMeta.inheritable) if (ownMeta.inheritable) { ownMeta.inheritable.changeParent(newParentMeta.inheritable) } } }