aspen-decorations
Version:
Complex styling for react-aspen w/ inheritance and negations
353 lines • 16.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const notificar_1 = require("notificar");
const types_1 = require("./types");
/**
* The "consumer level" part of Decoration
*
* `ClasslistComposite` contains composited classnames from all the decorations applicable to a target
*
* The composite includes all the applicable inheritances and negations.
*
* Note that due to performance considerations, `ClasslistComposite` is one of the few cases where
* we *do not* use `Disposables` for event management and instead use old school `addChangeListener`/`removeChangeListener` methods.
*
* Remember to pass same **named** function reference to `addChangeListener` and `removeChangeLiseneer` while subscribing and unsusbscribing
* in `componentDidMount` and `componentWillUnmount` respectively.
*
* Note: You should also implement a `componentDidUpdate` hook where you can unsubscribe from previous decoration object and subscribe to the new one.
*/
class ClasslistComposite {
/** @internal */
constructor(
/**
* Registers a function to be called when composited classlist changes
*
* *⚠ Remember not to use anonymous function!! (pass a named function reference instead)*
*/
addChangeListener,
/**
* Unregisters a previsously registered classlist change listener
*
* *⚠ Remember not to use anonymous function!! (pass a named function reference instead)*
*/
removeChangeListener) {
this.addChangeListener = addChangeListener;
this.removeChangeListener = removeChangeListener;
}
}
exports.ClasslistComposite = ClasslistComposite;
var DecorationCompositeType;
(function (DecorationCompositeType) {
DecorationCompositeType[DecorationCompositeType["Applicable"] = 1] = "Applicable";
DecorationCompositeType[DecorationCompositeType["Inheritable"] = 2] = "Inheritable";
})(DecorationCompositeType = exports.DecorationCompositeType || (exports.DecorationCompositeType = {}));
var ChangeReason;
(function (ChangeReason) {
ChangeReason[ChangeReason["UnTargetDecoration"] = 1] = "UnTargetDecoration";
ChangeReason[ChangeReason["TargetDecoration"] = 2] = "TargetDecoration";
})(ChangeReason = exports.ChangeReason || (exports.ChangeReason = {}));
/**
* Compositer for decorations
*
* When multiple `Decoration`s are applied to a target, they get grouped into a `DecorationComposite`
*
* @internal
*/
class DecorationComposite {
constructor(target, type, parent) {
this.handleDecorationDidAddClassname = (decoration, classname) => {
if (!this.selfOwned) {
throw new Error(`[INTERNAL] A non-self owned composite must not be incharge of Decoration events`);
}
this.compositeCssClasslist.classlist.push(classname);
this.notifyClasslistChange();
};
this.handleDecorationDidRemoveClassname = (decoration, classname) => {
if (!this.selfOwned) {
throw new Error(`[INTERNAL] A non-self owned composite must not be incharge of Decoration events`);
}
const idx = this.compositeCssClasslist.classlist.indexOf(classname);
if (idx > -1) {
this.compositeCssClasslist.classlist.splice(idx, 1);
this.notifyClasslistChange();
}
};
this.mergeDecorationClasslist = (decoration) => {
if (!this.selfOwned) {
throw new Error(`[INTERNAL] A non-self owned composite must not be incharge of Decoration events`);
}
this.compositeCssClasslist.classlist.push(...decoration.cssClasslist);
this.notifyClasslistChange();
};
this.target = target;
this.type = type;
this.linkedComposites = new Set();
this.classlistChangeCallbacks = new Set();
this.compositeCssClasslist = new ClasslistComposite(this.classlistChangeCallbacks.add.bind(this.classlistChangeCallbacks), this.classlistChangeCallbacks.delete.bind(this.classlistChangeCallbacks));
if (parent) {
this.selfOwned = false;
this.parent = parent;
this.renderedDecorations = parent.renderedDecorations;
this.compositeCssClasslist.classlist = parent.compositeCssClasslist.classlist;
parent.linkedComposites.add(this);
}
else {
this.renderedDecorations = new Map();
this.targetedDecorations = new Set();
this.negatedDecorations = new Set();
this.compositeCssClasslist.classlist = [];
this.selfOwned = true;
}
}
changeParent(newParent) {
if (!this.selfOwned) {
return this.parentOwn(newParent);
}
// first purge all the decorations (unless applicable)
for (const [decoration] of this.renderedDecorations) {
this.recursiveRefresh(this, false, ChangeReason.UnTargetDecoration, decoration, false);
}
if (this.parent !== newParent) {
this.parent.linkedComposites.delete(this);
this.parent = newParent;
newParent.linkedComposites.add(this);
}
// then add all the inherited decorations (unless not applicable)
for (const [decoration] of newParent.renderedDecorations) {
this.recursiveRefresh(this, false, ChangeReason.TargetDecoration, decoration, false);
}
}
add(decoration) {
const applicationMode = decoration.appliedTargets.get(this.target);
const applicableToSelf = applicationMode && (applicationMode === types_1.TargetMatchMode.Self || applicationMode === types_1.TargetMatchMode.SelfAndChildren);
const applicableToChildren = applicationMode && (applicationMode === types_1.TargetMatchMode.Children || applicationMode === types_1.TargetMatchMode.SelfAndChildren);
if (this.type === DecorationCompositeType.Applicable && !applicableToSelf) {
return;
}
if (this.type === DecorationCompositeType.Inheritable && !applicableToChildren) {
return;
}
if (!this.selfOwned) {
this.selfOwn(ChangeReason.TargetDecoration, decoration);
this.targetedDecorations.add(decoration);
return;
}
if (this.targetedDecorations.has(decoration)) {
return;
}
this.targetedDecorations.add(decoration);
this.recursiveRefresh(this, false, ChangeReason.TargetDecoration, decoration);
}
remove(decoration) {
// a non-self owned composite wouldn't have had a decoration to begin with
if (!this.selfOwned) {
return;
}
if (this.targetedDecorations.delete(decoration)) {
if (this.negatedDecorations.size === 0 && this.targetedDecorations.size === 0 && this.parent) {
return this.parentOwn(null, ChangeReason.UnTargetDecoration, decoration);
}
this.recursiveRefresh(this, false, ChangeReason.UnTargetDecoration, decoration);
}
}
negate(decoration) {
const negationMode = decoration.negatedTargets.get(this.target);
const negatedOnSelf = negationMode && (negationMode === types_1.TargetMatchMode.Self || negationMode === types_1.TargetMatchMode.SelfAndChildren);
const negatedOnChildren = negationMode && (negationMode === types_1.TargetMatchMode.Children || negationMode === types_1.TargetMatchMode.SelfAndChildren);
if (this.type === DecorationCompositeType.Applicable && !negatedOnSelf) {
return;
}
if (this.type === DecorationCompositeType.Inheritable && !negatedOnChildren) {
return;
}
if (!this.selfOwned) {
this.selfOwn(ChangeReason.UnTargetDecoration, decoration);
this.negatedDecorations.add(decoration);
return;
}
if (this.negatedDecorations.has(decoration)) {
return;
}
this.negatedDecorations.add(decoration);
if (this.renderedDecorations.has(decoration)) {
this.removeDecorationClasslist(decoration);
}
}
/**
* unNegate doesn't mean "explicit apply"
*/
unNegate(decoration) {
// a non-self owned composite wouldn't have been negated to begin with
if (!this.selfOwned) {
return;
}
if (this.negatedDecorations.delete(decoration)) {
if (this.negatedDecorations.size === 0 && this.targetedDecorations.size === 0 && this.parent) {
return this.parentOwn();
}
// currently not present
if (!this.renderedDecorations.has(decoration) &&
// **and** either parent or itself has it applied
(this.parent.renderedDecorations.has(decoration) || decoration.appliedTargets.has(this.target))) {
this.recursiveRefresh(this, false, ChangeReason.TargetDecoration, decoration);
}
}
}
selfOwn(reason, decoration) {
if (this.selfOwned) {
throw new Error(`DecorationComposite is already self owned`);
}
const parent = this.parent;
this.selfOwned = true;
this.compositeCssClasslist.classlist = [];
this.renderedDecorations = new Map();
this.targetedDecorations = new Set();
this.negatedDecorations = new Set();
// first process all the inherited decorations
for (const [inheritedDecoration] of parent.renderedDecorations) {
// fate of the decoration (second arg) will be decided in `#recursiveRefresh`
if (inheritedDecoration !== decoration) {
this.processCompositeAlteration(ChangeReason.TargetDecoration, inheritedDecoration);
}
}
// perhaps negation is why this composite branched off
if (reason === ChangeReason.UnTargetDecoration &&
// parent had it
this.parent.renderedDecorations.has(decoration) &&
// this one won't
!this.renderedDecorations.has(decoration)) {
// announce the change
this.notifyClasslistChange(false);
}
// then move on to main business
this.recursiveRefresh(this, true, reason, decoration);
}
parentOwn(newParent, reason, decoration) {
this.selfOwned = false;
this.targetedDecorations = void 0;
this.negatedDecorations = void 0;
if (newParent && this.parent !== newParent) {
if (this.parent) {
this.parent.linkedComposites.delete(this);
}
newParent.linkedComposites.add(this);
this.parent = newParent;
}
this.recursiveRefresh(this, true, reason, decoration);
}
processCompositeAlteration(reason, decoration) {
if (!this.selfOwned) {
throw new Error(`DecorationComposite is not self owned`);
}
if (reason === ChangeReason.UnTargetDecoration) {
const disposable = this.renderedDecorations.get(decoration);
if (disposable) {
const applicationMode = decoration.appliedTargets.get(this.target);
const applicableToSelf = applicationMode && (applicationMode === types_1.TargetMatchMode.Self || applicationMode === types_1.TargetMatchMode.SelfAndChildren);
const applicableToChildren = applicationMode && (applicationMode === types_1.TargetMatchMode.Children || applicationMode === types_1.TargetMatchMode.SelfAndChildren);
if (applicableToSelf && this.type === DecorationCompositeType.Applicable) {
return false;
}
if (applicableToChildren && this.type === DecorationCompositeType.Inheritable) {
return false;
}
this.removeDecorationClasslist(decoration, false);
if (disposable) {
disposable.dispose();
}
return this.renderedDecorations.delete(decoration);
}
return false;
}
if (reason === ChangeReason.TargetDecoration) {
const negationMode = decoration.negatedTargets.get(this.target);
const negatedOnSelf = negationMode && (negationMode === types_1.TargetMatchMode.Self || negationMode === types_1.TargetMatchMode.SelfAndChildren);
const negatedOnChildren = negationMode && (negationMode === types_1.TargetMatchMode.Children || negationMode === types_1.TargetMatchMode.SelfAndChildren);
if (negatedOnSelf && this.type === DecorationCompositeType.Applicable) {
return;
}
if (negatedOnChildren && this.type === DecorationCompositeType.Inheritable) {
return;
}
if (!this.renderedDecorations.has(decoration)) {
const disposables = new notificar_1.DisposablesComposite();
disposables.add(decoration.onDidAddCSSClassname(this.handleDecorationDidAddClassname));
disposables.add(decoration.onDidRemoveCSSClassname(this.handleDecorationDidRemoveClassname));
disposables.add(decoration.onDidDisableDecoration(this.removeDecorationClasslist));
disposables.add(decoration.onDidEnableDecoration(this.mergeDecorationClasslist));
this.renderedDecorations.set(decoration, disposables);
if (!decoration.disabled) {
this.compositeCssClasslist.classlist.push(...decoration.cssClasslist);
return true;
}
return false;
}
}
}
recursiveRefresh(origin, updateReferences, reason, decoration, notifyListeners = true) {
// references changed
if (!this.selfOwned && updateReferences) {
this.renderedDecorations = this.parent.renderedDecorations;
this.compositeCssClasslist.classlist = this.parent.compositeCssClasslist.classlist;
}
if (this.selfOwned && updateReferences && origin !== this) {
// purge all the decorations (unless applicable)
for (const [renderedDecoration] of this.renderedDecorations) {
this.processCompositeAlteration(ChangeReason.UnTargetDecoration, renderedDecoration);
}
// then add all the inherited decorations (unless not applicable)
for (const [inheritedDecoration] of this.parent.renderedDecorations) {
this.processCompositeAlteration(ChangeReason.TargetDecoration, inheritedDecoration);
}
if (notifyListeners) {
this.notifyClasslistChange(false);
}
}
else if (this.selfOwned && reason === ChangeReason.UnTargetDecoration && this.renderedDecorations.has(decoration)) {
this.processCompositeAlteration(reason, decoration);
if (notifyListeners) {
this.notifyClasslistChange(false);
}
}
else if (this.selfOwned && reason === ChangeReason.TargetDecoration && this.processCompositeAlteration(reason, decoration) && notifyListeners) {
this.notifyClasslistChange(false);
}
else if (!this.selfOwned && notifyListeners) {
this.notifyClasslistChange(false);
}
for (const linkedComposite of this.linkedComposites) {
linkedComposite.recursiveRefresh(origin, updateReferences, reason, decoration, notifyListeners);
}
}
removeDecorationClasslist(decoration, notifyAll = true) {
if (!this.selfOwned) {
throw new Error(`[INTERNAL] A non-self owned composite must not be incharge of Decoration events`);
}
for (const classname of decoration.cssClasslist) {
const idx = this.compositeCssClasslist.classlist.indexOf(classname);
if (idx > -1) {
this.compositeCssClasslist.classlist.splice(idx, 1);
}
}
if (notifyAll) {
this.notifyClasslistChange();
}
}
notifyClasslistChange(recursive = true) {
// here it's important that we don't iterate directly over this.classlistChangeCallbacks, instead create a copy of them first
// if one of the callbacks alters the Set by adding/removing callback (inside another callback) it makes this loop infinite
for (const cb of [...this.classlistChangeCallbacks]) {
cb();
}
if (recursive) {
for (const linkedComposite of this.linkedComposites) {
if (!linkedComposite.selfOwned) {
linkedComposite.notifyClasslistChange();
}
}
}
}
}
exports.DecorationComposite = DecorationComposite;
//# sourceMappingURL=DecorationComposite.js.map