mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Minimazing widgets
Summary: A feature that allows minimizing widgets on the ViewLayout. - Code in ViewLayout and Layout hasn't been changed. Only some methods or variables were made public, and some events are now triggered when a section is dragged. - Widgets can be collapsed or expanded (added back to the main area) - Collapsed widgets can be expanded and shown as a popup - Collapsed widgets support drugging, reordering, and transferring between the main and collapsed areas. Test Plan: New test Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D3779
This commit is contained in:
193
app/client/lib/Signal.ts
Normal file
193
app/client/lib/Signal.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
import { DisposableWithEvents } from 'app/common/DisposableWithEvents';
|
||||
import { Disposable, IDisposable, IDisposableOwner, Observable } from 'grainjs';
|
||||
|
||||
/**
|
||||
* A simple abstraction for events composition. It is an object that can emit a single value of type T,
|
||||
* and holds the last value emitted. It can be used to compose events from other events.
|
||||
*
|
||||
* Simple observables can't be used for this purpose because they are not reentrant. We can't update
|
||||
* an observable from within a listener, because it won't trigger a new event.
|
||||
*
|
||||
* This class is basically a wrapper around Observable, that emits events when the value changes after it is
|
||||
* set.
|
||||
*
|
||||
* Example:
|
||||
* const signal = Signal.create(null, 0);
|
||||
* signal.listen(value => console.log(value));
|
||||
* const onlyEven = signal.filter(value => value % 2 === 0);
|
||||
* onlyEven.listen(value => console.log('even', value));
|
||||
*
|
||||
* const flag1 = Signal.create(null, false);
|
||||
* const flag2 = Signal.create(null, false);
|
||||
* const flagAnd = Signal.compute(null, on => on(flag1) && on(flag2));
|
||||
* // This will still emit multiple times with the same value repeated.
|
||||
* flagAnd.listen(value => console.log('Both are true', value));
|
||||
*
|
||||
* // This will emit only when both are true, and will ignore further changes while both are true.
|
||||
* const toggle = flagAnd.distinct();
|
||||
*
|
||||
* // Current value can be accessed via signal.state.get()
|
||||
* const emitter = Signal.from(null, 0);
|
||||
* // Emit values only when the toggle is true.
|
||||
* const emitterWhileAnd = emitter.filter(() => toggle.state.get());
|
||||
* // Equivalent to:
|
||||
* const emitterWhileAnd = Signal.compute(null, on => on(toggle) ? on(emitter) : null).distinct();
|
||||
*/
|
||||
export class Signal<T = any> implements IDisposable, IDisposableOwner {
|
||||
/**
|
||||
* Creates a new event with a default value. A convenience method for creating an event that supports
|
||||
* generic attribute.
|
||||
*/
|
||||
public static create<T>(owner: IDisposableOwner | null, value: T) {
|
||||
return new Signal(owner, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event from a set of events. Holds last value emitted by any of the events.
|
||||
*/
|
||||
public static fromEvents<T = any>(
|
||||
owner: Disposable | null,
|
||||
emitter: any,
|
||||
first: string,
|
||||
...rest: string[]
|
||||
) {
|
||||
const signal = Signal.create(owner, null);
|
||||
for(const event of [first, ...rest]) {
|
||||
signal._emitter.listenTo(emitter, event, (value: any) => signal.emit(value));
|
||||
}
|
||||
return signal as Signal<T | null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper methods that creates a signal that emits the result of a function that takes a function
|
||||
*/
|
||||
public static compute<T>(owner: Disposable | null, compute: ComputeFunction<T>) {
|
||||
const signal = Signal.create(owner, null as any);
|
||||
const on: any = (s: Signal) => {
|
||||
if (!signal._listeners.has(s)) {
|
||||
signal._listeners.add(s);
|
||||
signal._emitter.listenTo(s._emitter, 'signal', () => signal.emit(compute(on)));
|
||||
}
|
||||
return s.state.get();
|
||||
};
|
||||
signal.state.set(compute(on));
|
||||
return signal as Signal<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Last value emitted if any.
|
||||
*/
|
||||
public state: Observable<T>;
|
||||
|
||||
/**
|
||||
* List of signals that we are listening to. Stored in a WeakSet to avoid memory leaks.
|
||||
*/
|
||||
private _listeners: WeakSet<Signal> = new WeakSet();
|
||||
|
||||
/**
|
||||
* Flag that can be changed by stateless() function. It won't hold last value (but can't be used in compute function).
|
||||
*/
|
||||
private _emitter: DisposableWithEvents;
|
||||
|
||||
private _beforeHandler: CustomEmitter<T>;
|
||||
|
||||
constructor(owner: IDisposableOwner|null, initialValue: T) {
|
||||
this._emitter = DisposableWithEvents.create(owner);
|
||||
this.state = Observable.create(this, initialValue);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this._emitter.dispose();
|
||||
}
|
||||
|
||||
public autoDispose(disposable: IDisposable) {
|
||||
this._emitter.autoDispose(disposable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push all events from this signal to another signal.
|
||||
*/
|
||||
public pipe(signal: Signal<T>) {
|
||||
this.autoDispose(this.listen(value => signal.emit(value)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify all values emitted by this signal.
|
||||
*/
|
||||
public map<Z>(selector: (value: T) => Z): Signal<Z> {
|
||||
const signal = Signal.create(this, selector(this.state.get()));
|
||||
this.listen(value => {
|
||||
signal.emit(selector(value));
|
||||
});
|
||||
return signal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new signal with the same state, but it will only
|
||||
* emit those values that pass the test implemented by the provided function.
|
||||
*/
|
||||
public filter(selector: (value: T) => boolean): Signal<T> {
|
||||
const signal = Signal.create(this, this.state.get());
|
||||
this.listen(value => {
|
||||
if (selector(value)) {
|
||||
signal.emit(value);
|
||||
}
|
||||
});
|
||||
return signal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit only the value that is different from the previous one.
|
||||
*/
|
||||
public distinct(): Signal<T> {
|
||||
let last = this.state.get();
|
||||
const signal = this.filter((value: any) => {
|
||||
if (value !== last) {
|
||||
last = value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
signal.state.set(last);
|
||||
return signal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits true or false only when the value is changed from truthy to falsy or vice versa.
|
||||
*/
|
||||
public flag() {
|
||||
return this.map(Boolean).distinct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to changes of the signal.
|
||||
*/
|
||||
public listen(handler: (value: T) => any) {
|
||||
const stateHandler = () => {
|
||||
handler(this.state.get());
|
||||
};
|
||||
this._emitter.on('signal', stateHandler);
|
||||
return {
|
||||
dispose: () => this._emitter.off('signal', stateHandler),
|
||||
};
|
||||
}
|
||||
|
||||
public emit(value: T) {
|
||||
if (this._beforeHandler) {
|
||||
this._beforeHandler(value, (emitted: T) => {
|
||||
this.state.set(emitted);
|
||||
this._emitter.trigger('signal', emitted);
|
||||
});
|
||||
} else {
|
||||
this.state.set(value);
|
||||
this._emitter.trigger('signal', value);
|
||||
}
|
||||
}
|
||||
|
||||
public before(handler: CustomEmitter<T>) {
|
||||
this._beforeHandler = handler;
|
||||
}
|
||||
}
|
||||
|
||||
type ComputeFunction<T> = (on: <TS>(s: Signal<TS>) => TS) => T;
|
||||
type CustomEmitter<T> = (value: T, emit: (value: T) => void) => any;
|
||||
Reference in New Issue
Block a user