(core) Cursor in custom widgets

Summary:
Adding a new method `setCursorPos` in the widget API, and a new configuration option for the ready message `allowSelectBy` that exposes custom widgets in the `Select by` dropdown.
With this, a custom widget can control the position of the linked widgets and is able to change the column in the creator panel.

Test Plan: Added new test. Existing tests should pass.

Reviewers: JakubSerafin

Reviewed By: JakubSerafin

Subscribers: JakubSerafin

Differential Revision: https://phab.getgrist.com/D3993
This commit is contained in:
Jarosław Sadziński
2023-08-28 11:16:17 +02:00
parent c02acff361
commit b6a431dd58
33 changed files with 155 additions and 84 deletions

View File

@@ -1,5 +1,5 @@
import { CursorPos } from "app/client/components/Cursor";
import { DocModel, ViewFieldRec } from "app/client/models/DocModel";
import { CursorPos } from 'app/plugin/GristAPI';
import BaseRowModel = require("app/client/models/BaseRowModel");
/**

View File

@@ -1,6 +1,7 @@
import type {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
import type {CellValue} from 'app/common/DocActions';
import type {TableData, UIRowId} from 'app/common/TableData';
import type {TableData} from 'app/common/TableData';
import type {UIRowId} from 'app/plugin/GristAPI';
/**
* The CopySelection class is an abstraction for a subset of currently selected cells.

View File

@@ -8,17 +8,10 @@ import BaseView from 'app/client/components/BaseView';
import * as commands from 'app/client/components/commands';
import BaseRowModel from 'app/client/models/BaseRowModel';
import {LazyArrayModel} from 'app/client/models/DataTableModel';
import type {UIRowId} from 'app/common/TableData';
import {CursorPos, UIRowId} from 'app/plugin/GristAPI';
import {Disposable} from 'grainjs';
import * as ko from 'knockout';
export interface CursorPos {
rowId?: UIRowId;
rowIndex?: number;
fieldIndex?: number;
sectionId?: number;
}
function nullAsUndefined<T>(value: T|null|undefined): T|undefined {
return value == null ? undefined : value;
}

View File

@@ -1,9 +1,9 @@
import {CursorPos} from 'app/client/components/Cursor';
import {GristDoc} from 'app/client/components/GristDoc';
import {getStorage} from 'app/client/lib/storage';
import {IDocPage, isViewDocPage, ViewDocPage} from 'app/common/gristUrls';
import {Disposable, Listener, Observable} from 'grainjs';
import {reportError} from 'app/client/models/errors';
import {CursorPos} from 'app/plugin/GristAPI';
/**
* Enriched cursor position with a view id

View File

@@ -9,7 +9,6 @@ import BaseView from 'app/client/components/BaseView';
import {isNumericLike, isNumericOnly} from 'app/client/components/ChartView';
import {CodeEditorPanel} from 'app/client/components/CodeEditorPanel';
import * as commands from 'app/client/components/commands';
import {CursorPos} from 'app/client/components/Cursor';
import {CursorMonitor, ViewCursorPos} from "app/client/components/CursorMonitor";
import {DocComm} from 'app/client/components/DocComm';
import * as DocConfigTab from 'app/client/components/DocConfigTab';
@@ -70,6 +69,7 @@ import {LocalPlugin} from "app/common/plugin";
import {StringUnion} from 'app/common/StringUnion';
import {TableData} from 'app/common/TableData';
import {DocStateComparison} from 'app/common/UserAPI';
import {CursorPos} from 'app/plugin/GristAPI';
import {
bundleChanges,
Computed,
@@ -1256,7 +1256,7 @@ export class GristDoc extends DisposableWithEvents {
const fieldIndex = activeSection.viewFields.peek().all().findIndex(f => f.colRef.peek() === hash.colRef);
if (fieldIndex >= 0) {
const view = await this._waitForView(activeSection);
view?.setCursorPos({sectionId: hash.sectionId, rowId: hash.rowId, fieldIndex});
view?.setCursorPos({rowId: hash.rowId, fieldIndex});
}
}
this.viewLayout?.maximized.set(hash.sectionId);
@@ -1306,7 +1306,7 @@ export class GristDoc extends DisposableWithEvents {
const fieldIndex = popupSection.viewFields.peek().all().findIndex(f => f.colRef.peek() === hash.colRef);
if (fieldIndex >= 0) {
const view = await this._waitForView(popupSection);
view?.setCursorPos({sectionId: hash.sectionId, rowId: hash.rowId, fieldIndex});
view?.setCursorPos({rowId: hash.rowId, fieldIndex});
}
}
}

View File

@@ -4,13 +4,13 @@ import {DocModel} from 'app/client/models/DocModel';
import {ColumnRec} from "app/client/models/entities/ColumnRec";
import {TableRec} from "app/client/models/entities/TableRec";
import {ViewSectionRec} from "app/client/models/entities/ViewSectionRec";
import {UIRowId} from "app/common/TableData";
import {LinkConfig} from "app/client/ui/selectBy";
import {FilterColValues, QueryOperation} from "app/common/ActiveDocAPI";
import {isList, isListType, isRefListType} from "app/common/gristTypes";
import * as gutil from "app/common/gutil";
import {UIRowId} from 'app/plugin/GristAPI';
import {encodeObject} from 'app/plugin/objtypes';
import {Disposable, toKo} from "grainjs";
import {Disposable} from "grainjs";
import * as ko from "knockout";
import identity = require('lodash/identity');
import mapValues = require('lodash/mapValues');
@@ -85,7 +85,7 @@ export class LinkingState extends Disposable {
if (tgtColId) {
const operation = isRefListType(tgtCol.type()) ? 'intersects' : 'in';
if (srcSection.parentKey() === 'custom') {
if (srcSection.selectedRowsActive()) {
this.filterColValues = this._srcCustomFilter(tgtColId, operation);
} else if (srcColId) {
this.filterColValues = this._srcCellFilter(tgtColId, operation);
@@ -128,7 +128,7 @@ export class LinkingState extends Disposable {
}
_filterColValues(result);
}
} else if (srcSection.parentKey() === 'custom') {
} else if (srcSection.selectedRowsActive()) {
this.filterColValues = this._srcCustomFilter('id', 'in');
} else {
const srcValueFunc = srcColId ? this._makeSrcCellGetter() : identity;
@@ -207,7 +207,7 @@ export class LinkingState extends Disposable {
// Value for this.filterColValues based on the values in srcSection.selectedRows
private _srcCustomFilter(colId: string, operation: QueryOperation): ko.Computed<FilterColValues> | undefined {
return this.autoDispose(ko.computed(() => {
const values = toKo(ko, this._srcSection.selectedRows)();
const values = this._srcSection.selectedRows();
return {filters: {[colId]: values}, operations: {[colId]: operation}} as FilterColValues;
}));
}

View File

@@ -1,8 +1,8 @@
import {CursorPos} from 'app/client/components/Cursor';
import {GristDoc} from 'app/client/components/GristDoc';
import * as dispose from 'app/client/lib/dispose';
import {MinimalActionGroup} from 'app/common/ActionGroup';
import {PromiseChain, setDefault} from 'app/common/gutil';
import {CursorPos} from 'app/plugin/GristAPI';
import {fromKo, Observable} from 'grainjs';
import * as ko from 'knockout';
import sortBy = require('lodash/sortBy');

View File

@@ -7,7 +7,7 @@ import {AccessLevel, isSatisfied} from 'app/common/CustomWidget';
import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
import {BulkColValues, fromTableDataAction, RowRecord} from 'app/common/DocActions';
import {extractInfoFromColType, reencodeAsAny} from 'app/common/gristTypes';
import {AccessTokenOptions, CustomSectionAPI, GristDocAPI, GristView,
import {AccessTokenOptions, CursorPos, CustomSectionAPI, GristDocAPI, GristView,
InteractionOptionsRequest, WidgetAPI, WidgetColumnMap} from 'app/plugin/grist-plugin-api';
import {MsgType, Rpc} from 'grain-rpc';
import {Computed, Disposable, dom, Observable} from 'grainjs';
@@ -380,12 +380,25 @@ export class GristViewImpl implements GristView {
return data;
}
/**
* This is deprecated method to turn on cursor linking. Previously it was used
* to create a custom row id filter. Now widgets can be treated as normal source of linking.
* Now allowSelectBy should be set using the ready event.
*/
public async allowSelectBy(): Promise<void> {
this._baseView.viewSection.allowSelectBy.set(true);
this._baseView.viewSection.allowSelectBy(true);
// This is to preserve a legacy behavior, where when allowSelectBy is called widget expected
// that the filter was already applied to clear all rows.
this._baseView.viewSection.selectedRows([]);
}
public async setSelectedRows(rowIds: number[]): Promise<void> {
this._baseView.viewSection.selectedRows.set(rowIds);
public async setSelectedRows(rowIds: number[]|null): Promise<void> {
this._baseView.viewSection.selectedRows(rowIds);
}
public setCursorPos(cursorPos: CursorPos): Promise<void> {
this._baseView.setCursorPos(cursorPos);
return Promise.resolve();
}
private _visibleColumns() {
@@ -615,5 +628,8 @@ export class CustomSectionAPIImpl extends Disposable implements CustomSectionAPI
} else {
this._section.columnsToMap(null);
}
if (settings.allowSelectBy !== undefined) {
this._section.allowSelectBy(settings.allowSelectBy);
}
}
}

View File

@@ -25,7 +25,7 @@ declare module "app/client/components/Base" {
declare module "app/client/components/BaseView" {
import {Cursor, CursorPos} from 'app/client/components/Cursor';
import {Cursor} from 'app/client/components/Cursor';
import {GristDoc} from 'app/client/components/GristDoc';
import {IGristUrlState} from 'app/common/gristUrls';
import {SelectionSummary} from 'app/client/components/SelectionSummary';
@@ -39,6 +39,7 @@ declare module "app/client/components/BaseView" {
import {SortedRowSet} from 'app/client/models/rowset';
import {IColumnFilterMenuOptions} from 'app/client/ui/ColumnFilterMenu';
import {FieldBuilder} from "app/client/widgets/FieldBuilder";
import {CursorPos} from 'app/plugin/GristAPI';
import {DomArg} from 'grainjs';
import {IOpenController} from 'popweasel';

View File

@@ -5,7 +5,7 @@ import * as rowset from 'app/client/models/rowset';
import { MANUALSORT } from 'app/common/gristTypes';
import { SortFunc } from 'app/common/SortFunc';
import { Sort } from 'app/common/SortSpec';
import { UIRowId } from 'app/common/TableData';
import { UIRowId } from 'app/plugin/GristAPI';
import * as ko from 'knockout';
import range = require('lodash/range');

View File

@@ -29,7 +29,6 @@ import {isHiddenTable, isSummaryTable} from 'app/common/isHiddenTable';
import {canEdit} from 'app/common/roles';
import {RowFilterFunc} from 'app/common/RowFilterFunc';
import {schema, SchemaTypes} from 'app/common/schema';
import {UIRowId} from 'app/common/TableData';
import {ACLRuleRec, createACLRuleRec} from 'app/client/models/entities/ACLRuleRec';
import {ColumnRec, createColumnRec} from 'app/client/models/entities/ColumnRec';
import {createDocInfoRec, DocInfoRec} from 'app/client/models/entities/DocInfoRec';
@@ -45,6 +44,7 @@ import {CellRec, createCellRec} from 'app/client/models/entities/CellRec';
import {RefListValue} from 'app/common/gristTypes';
import {decodeObject} from 'app/plugin/objtypes';
import {toKo} from 'grainjs';
import {UIRowId} from 'app/plugin/GristAPI';
// Re-export all the entity types available. The recommended usage is like this:
// import {ColumnRec, ViewFieldRec} from 'app/client/models/DocModel';

View File

@@ -26,6 +26,7 @@
* TODO: client-side should show "..." or "50000 more rows not shown" in that case.
* TODO: Reference columns don't work properly because always use a displayCol which relies on formulas
*/
import {ClientColumnGettersByColId} from 'app/client/models/ClientColumnGetters';
import DataTableModel from 'app/client/models/DataTableModel';
import {DocModel} from 'app/client/models/DocModel';
import {BaseFilteredRowSource, RowList, RowSource} from 'app/client/models/rowset';
@@ -36,11 +37,11 @@ import {DocData} from 'app/common/DocData';
import {nativeCompare} from 'app/common/gutil';
import {IRefCountSub, RefCountMap} from 'app/common/RefCountMap';
import {getLinkingFilterFunc, RowFilterFunc} from 'app/common/RowFilterFunc';
import {TableData as BaseTableData, UIRowId} from 'app/common/TableData';
import {TableData as BaseTableData} from 'app/common/TableData';
import {tbind} from 'app/common/tbind';
import {UIRowId} from 'app/plugin/GristAPI';
import {Disposable, Holder, IDisposableOwnerT} from 'grainjs';
import * as ko from 'knockout';
import {ClientColumnGettersByColId} from 'app/client/models/ClientColumnGetters';
import debounce = require('lodash/debounce');
// Limit on the how many rows to request for OnDemand tables.

View File

@@ -1,7 +1,6 @@
// tslint:disable:no-console
// TODO: Add documentation and clean up log statements.
import {CursorPos} from 'app/client/components/Cursor';
import {GristDoc} from 'app/client/components/GristDoc';
import {PageRec, ViewFieldRec, ViewSectionRec} from 'app/client/models/DocModel';
import {reportError} from 'app/client/models/errors';
@@ -10,9 +9,10 @@ import {IDocPage} from 'app/common/gristUrls';
import {nativeCompare, waitObs} from 'app/common/gutil';
import {TableData} from 'app/common/TableData';
import {BaseFormatter} from 'app/common/ValueFormatter';
import { makeT } from 'app/client/lib/localization';
import {CursorPos} from 'app/plugin/GristAPI';
import {Computed, Disposable, Observable} from 'grainjs';
import debounce = require('lodash/debounce');
import { makeT } from 'app/client/lib/localization';
const t = makeT('SearchModel');

View File

@@ -3,7 +3,7 @@ import {ColumnRec, ViewFieldRec, ViewSectionRec} from 'app/client/models/DocMode
import {TableData} from 'app/client/models/TableData';
import {buildColFilter, ColumnFilterFunc} from 'app/common/ColumnFilterFunc';
import {buildRowFilter, RowFilterFunc, RowValueFunc } from 'app/common/RowFilterFunc';
import {UIRowId} from 'app/common/TableData';
import {UIRowId} from 'app/plugin/GristAPI';
import {Computed, Disposable, MutableObsArray, obsArray, Observable, UseCB} from 'grainjs';
export type {ColumnFilterFunc};

View File

@@ -1,5 +1,4 @@
import BaseView from 'app/client/components/BaseView';
import {CursorPos} from 'app/client/components/Cursor';
import {LinkingState} from 'app/client/components/LinkingState';
import {KoArray} from 'app/client/lib/koArray';
import {
@@ -22,11 +21,11 @@ import {AccessLevel, ICustomWidget} from 'app/common/CustomWidget';
import {UserAction} from 'app/common/DocActions';
import {arrayRepeat} from 'app/common/gutil';
import {Sort} from 'app/common/SortSpec';
import {UIRowId} from 'app/common/TableData';
import {ColumnsToMap, WidgetColumnMap} from 'app/plugin/CustomSectionAPI';
import {ColumnToMapImpl} from 'app/client/models/ColumnToMap';
import {BEHAVIOR} from 'app/client/models/entities/ColumnRec';
import {removeRule, RuleOwner} from 'app/client/models/RuleOwner';
import {CursorPos, UIRowId} from 'app/plugin/GristAPI';
import {Computed, Holder, Observable} from 'grainjs';
import * as ko from 'knockout';
import defaults = require('lodash/defaults');
@@ -192,10 +191,14 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section">, RuleO
desiredAccessLevel: ko.Observable<AccessLevel|null>;
// Show widget as linking source. Used by custom widget.
allowSelectBy: Observable<boolean>;
allowSelectBy: ko.Observable<boolean>;
// List of selected rows
selectedRows: Observable<number[]>;
// List of selected rows from a custom widget, or null if a filter shouldn't be applied.
selectedRows: ko.Observable<number[]|null>;
// If the row filter is active (i.e. if selectedRows is non-null). Separate computed to avoid
// re-computing the filter when selectedRows changes.
selectedRowsActive: ko.Computed<boolean>;
editingFormula: ko.Computed<boolean>;
@@ -714,8 +717,9 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
return result;
});
this.allowSelectBy = Observable.create(this, false);
this.selectedRows = Observable.create(this, []);
this.allowSelectBy = ko.observable(false);
this.selectedRows = ko.observable(null as number[]|null);
this.selectedRowsActive = this.autoDispose(ko.pureComputed(() => this.selectedRows() !== null));
this.tableId = this.autoDispose(ko.pureComputed(() => this.table().tableId()));
const rawSection = this.autoDispose(ko.pureComputed(() => this.table().rawViewSection()));

View File

@@ -24,8 +24,9 @@
import koArray, {KoArray} from 'app/client/lib/koArray';
import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
import {CompareFunc, sortedIndex} from 'app/common/gutil';
import {SkippableRows, UIRowId} from 'app/common/TableData';
import {SkippableRows} from 'app/common/TableData';
import {RowFilterFunc} from "app/common/RowFilterFunc";
import {UIRowId} from 'app/plugin/GristAPI';
import {Observable} from 'grainjs';
/**

View File

@@ -5,6 +5,7 @@
*/
import * as commands from 'app/client/components/commands';
import {GristDoc} from 'app/client/components/GristDoc';
import {FocusLayer} from 'app/client/lib/FocusLayer';
import {makeT} from 'app/client/lib/localization';
import {ColumnFilter, NEW_FILTER_JSON} from 'app/client/models/ColumnFilter';
import {ColumnFilterMenuModel, IFilterCount} from 'app/client/models/ColumnFilterMenuModel';
@@ -13,21 +14,30 @@ import {FilterInfo} from 'app/client/models/entities/ViewSectionRec';
import {RowSource} from 'app/client/models/rowset';
import {ColumnFilterFunc, SectionFilter} from 'app/client/models/SectionFilter';
import {TableData} from 'app/client/models/TableData';
import {ColumnFilterCalendarView} from 'app/client/ui/ColumnFilterCalendarView';
import {relativeDatesControl} from 'app/client/ui/ColumnFilterMenuUtils';
import {cssInput} from 'app/client/ui/cssInput';
import {DateRangeOptions, IDateRangeOption} from 'app/client/ui/DateRangeOptions';
import {cssPinButton} from 'app/client/ui/RightPanelStyles';
import {basicButton, primaryButton, textButton} from 'app/client/ui2018/buttons';
import {cssLabel as cssCheckboxLabel, cssCheckboxSquare, cssLabelText, Indeterminate, labeledTriStateSquareCheckbox
} from 'app/client/ui2018/checkbox';
import {cssLabel as cssCheckboxLabel, cssCheckboxSquare,
cssLabelText, Indeterminate, labeledTriStateSquareCheckbox} from 'app/client/ui2018/checkbox';
import {theme, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {cssOptionRowIcon, menu, menuCssClass, menuDivider, menuItem} from 'app/client/ui2018/menus';
import {cssDeleteButton, cssDeleteIcon, cssToken as cssTokenTokenBase} from 'app/client/widgets/ChoiceListEditor';
import {ChoiceOptions} from 'app/client/widgets/ChoiceTextBox';
import {choiceToken} from 'app/client/widgets/ChoiceToken';
import {CellValue} from 'app/common/DocActions';
import {IRelativeDateSpec, isEquivalentFilter, isRelativeBound} from "app/common/FilterState";
import {formatRelBounds} from "app/common/RelativeDates";
import {
Computed, dom, DomArg, DomElementArg, DomElementMethod, IDisposableOwner, input, makeTestId,
Observable, styled
} from 'grainjs';
import {IRelativeDateSpec, isEquivalentFilter, isRelativeBound} from 'app/common/FilterState';
import {extractTypeFromColType, isDateLikeType, isList, isNumberType, isRefListType} from 'app/common/gristTypes';
import {formatRelBounds} from 'app/common/RelativeDates';
import {createFormatter} from 'app/common/ValueFormatter';
import {UIRowId} from 'app/plugin/GristAPI';
import {decodeObject} from 'app/plugin/objtypes';
import {Computed, dom, DomArg, DomElementArg, DomElementMethod, IDisposableOwner,
input, makeTestId, Observable, styled} from 'grainjs';
import {IOpenController, IPopupOptions, setPopupToCreateDom} from 'popweasel';
import concat = require('lodash/concat');
import identity = require('lodash/identity');
import noop = require('lodash/noop');
@@ -35,18 +45,6 @@ import partition = require('lodash/partition');
import some = require('lodash/some');
import tail = require('lodash/tail');
import debounce = require('lodash/debounce');
import {IOpenController, IPopupOptions, setPopupToCreateDom} from 'popweasel';
import {decodeObject} from 'app/plugin/objtypes';
import {extractTypeFromColType, isDateLikeType, isList, isNumberType, isRefListType} from 'app/common/gristTypes';
import {choiceToken} from 'app/client/widgets/ChoiceToken';
import {ChoiceOptions} from 'app/client/widgets/ChoiceTextBox';
import {ColumnFilterCalendarView} from 'app/client/ui/ColumnFilterCalendarView';
import {cssDeleteButton, cssDeleteIcon, cssToken as cssTokenTokenBase} from 'app/client/widgets/ChoiceListEditor';
import {relativeDatesControl} from 'app/client/ui/ColumnFilterMenuUtils';
import {FocusLayer} from 'app/client/lib/FocusLayer';
import {DateRangeOptions, IDateRangeOption} from 'app/client/ui/DateRangeOptions';
import {createFormatter} from 'app/common/ValueFormatter';
import {UIRowId} from 'app/common/TableData';
const t = makeT('ColumnFilterMenu');

View File

@@ -1,10 +1,10 @@
import {CursorPos} from 'app/client/components/Cursor';
import {makeT} from 'app/client/lib/localization';
import { KoSaveableObservable } from 'app/client/models/modelUtil';
import {KoSaveableObservable} from 'app/client/models/modelUtil';
import {autoGrow} from 'app/client/ui/forms';
import {textarea} from 'app/client/ui/inputs';
import {cssLabel, cssRow} from 'app/client/ui/RightPanelStyles';
import {testId, theme} from 'app/client/ui2018/cssVars';
import {CursorPos} from 'app/plugin/GristAPI';
import {dom, fromKo, MultiHolder, styled} from 'grainjs';
const t = makeT('DescriptionConfig');

View File

@@ -1,5 +1,4 @@
import {makeT} from 'app/client/lib/localization';
import {CursorPos} from 'app/client/components/Cursor';
import {GristDoc} from 'app/client/components/GristDoc';
import {BEHAVIOR, ColumnRec} from 'app/client/models/entities/ColumnRec';
import {buildHighlightedCode, cssCodeBlock} from 'app/client/ui/CodeHighlight';
@@ -16,6 +15,7 @@ import {selectMenu, selectOption, selectTitle} from 'app/client/ui2018/menus';
import {createFormulaErrorObs, cssError} from 'app/client/widgets/FormulaEditor';
import {sanitizeIdent} from 'app/common/gutil';
import {Theme} from 'app/common/ThemePrefs';
import {CursorPos} from 'app/plugin/GristAPI';
import {bundleChanges, Computed, dom, DomContents, DomElementArg, fromKo, MultiHolder,
Observable, styled} from 'grainjs';
import * as ko from 'knockout';

View File

@@ -127,7 +127,7 @@ function isValidLink(source: LinkNode, target: LinkNode) {
}
// custom widget must allow select by
if (!source.section.allowSelectBy.get()) {
if (!source.section.allowSelectBy()) {
return false;
}
}

View File

@@ -1,5 +1,5 @@
import * as commands from 'app/client/components/commands';
import {Cursor, CursorPos} from 'app/client/components/Cursor';
import {Cursor} from 'app/client/components/Cursor';
import {GristDoc} from 'app/client/components/GristDoc';
import {UnsavedChange} from 'app/client/components/UnsavedChanges';
import {makeT} from 'app/client/lib/localization';
@@ -14,6 +14,7 @@ import {CellValue} from "app/common/DocActions";
import * as gutil from 'app/common/gutil';
import {CellPosition} from "app/client/components/CellPosition";
import {FloatingEditor} from 'app/client/widgets/FloatingEditor';
import {CursorPos} from 'app/plugin/GristAPI';
import isEqual = require('lodash/isEqual');
import {Disposable, dom, Emitter, Holder, MultiHolder, Observable} from 'grainjs';