diff --git a/src/index.ts b/src/index.ts index 1e0caf2c54..5e492d6a75 100644 --- a/src/index.ts +++ b/src/index.ts @@ -209,6 +209,7 @@ export { findCellInNewZone } from "./helpers/zones"; export { load } from "./migrations/data"; export { Model } from "./model"; export { CorePlugin } from "./plugins/core_plugin"; +export { CoreViewPlugin } from "./plugins/core_view_plugin"; export { UIPlugin } from "./plugins/ui_plugin"; export { Registry } from "./registries/registry"; export { setTranslationMethod } from "./translation"; diff --git a/src/model.ts b/src/model.ts index 7e487758b5..d785ed5822 100644 --- a/src/model.ts +++ b/src/model.ts @@ -14,6 +14,7 @@ import { import { BasePlugin } from "./plugins/base_plugin"; import { RangeAdapter } from "./plugins/core/range"; import { CorePlugin, CorePluginConfig, CorePluginConstructor } from "./plugins/core_plugin"; +import { CoreViewPluginConfig, CoreViewPluginConstructor } from "./plugins/core_view_plugin"; import { corePluginRegistry, coreViewsPluginRegistry, @@ -131,6 +132,7 @@ const enum Status { export class Model extends EventBus implements CommandDispatcher { private corePlugins: CorePlugin[] = []; + private statefulUIPlugins: UIPlugin[] = []; private range: RangeAdapter; @@ -162,6 +164,7 @@ export class Model extends EventBus implements CommandDispatcher { */ readonly config: ModelConfig; private corePluginConfig: CorePluginConfig; + private coreViewPluginConfig: CoreViewPluginConfig; private uiPluginConfig: UIPluginConfig; private state: StateObserver; @@ -239,6 +242,7 @@ export class Model extends EventBus implements CommandDispatcher { this.handlers.push(this.range); this.corePluginConfig = this.setupCorePluginConfig(); + this.coreViewPluginConfig = this.setupCoreViewPluginConfig(); this.uiPluginConfig = this.setupUiPluginConfig(); // registering plugins @@ -250,7 +254,7 @@ export class Model extends EventBus implements CommandDispatcher { this.session.loadInitialMessages(stateUpdateMessages); for (let Plugin of coreViewsPluginRegistry.getAll()) { - const plugin = this.setupUiPlugin(Plugin); + const plugin = this.setupCoreViewPlugin(Plugin); this.handlers.push(plugin); this.uiHandlers.push(plugin); this.coreHandlers.push(plugin); @@ -323,6 +327,20 @@ export class Model extends EventBus implements CommandDispatcher { return plugin; } + private setupCoreViewPlugin(Plugin: CoreViewPluginConstructor) { + const plugin = new Plugin(this.coreViewPluginConfig); + for (let name of Plugin.getters) { + if (!(name in plugin)) { + throw new Error(`Invalid getter name: ${name} for plugin ${plugin.constructor}`); + } + if (name in this.getters) { + throw new Error(`Getter "${name}" is already defined.`); + } + this.getters[name] = plugin[name].bind(plugin); + } + return plugin; + } + /** * Initialize and properly configure a plugin. * @@ -435,6 +453,21 @@ export class Model extends EventBus implements CommandDispatcher { }; } + private setupCoreViewPluginConfig(): CoreViewPluginConfig { + return { + getters: this.getters, + stateObserver: this.state, + selection: this.selection, + moveClient: this.session.move.bind(this.session), + custom: this.config.custom, + uiActions: this.config, + session: this.session, + defaultCurrency: this.config.defaultCurrency, + customColors: this.config.customColors || [], + external: this.config.external, + }; + } + private setupUiPluginConfig(): UIPluginConfig { return { getters: this.getters, diff --git a/src/plugins/base_plugin.ts b/src/plugins/base_plugin.ts index 952e0cc03e..870968d394 100644 --- a/src/plugins/base_plugin.ts +++ b/src/plugins/base_plugin.ts @@ -1,6 +1,5 @@ import { StateObserver } from "../state_observer"; import { - CommandDispatcher, CommandHandler, CommandResult, ExcelWorkbookData, @@ -25,20 +24,12 @@ export class BasePlugin implements CommandHandler, Vali static getters: readonly string[] = []; protected history: WorkbookHistory; - protected dispatch: CommandDispatcher["dispatch"]; - protected canDispatch: CommandDispatcher["dispatch"]; - constructor( - stateObserver: StateObserver, - dispatch: CommandDispatcher["dispatch"], - canDispatch: CommandDispatcher["dispatch"] - ) { + constructor(stateObserver: StateObserver) { this.history = Object.assign(Object.create(stateObserver), { update: stateObserver.addChange.bind(stateObserver, this), selectCell: () => {}, }); - this.dispatch = dispatch; - this.canDispatch = canDispatch; } /** diff --git a/src/plugins/core_plugin.ts b/src/plugins/core_plugin.ts index 51a71a919f..ea62a794f7 100644 --- a/src/plugins/core_plugin.ts +++ b/src/plugins/core_plugin.ts @@ -38,11 +38,15 @@ export class CorePlugin implements RangeProvider { protected getters: CoreGetters; + protected dispatch: CoreCommandDispatcher["dispatch"]; + protected canDispatch: CoreCommandDispatcher["dispatch"]; constructor({ getters, stateObserver, range, dispatch, canDispatch }: CorePluginConfig) { - super(stateObserver, dispatch, canDispatch); + super(stateObserver); range.addRangeProvider(this.adaptRanges.bind(this)); this.getters = getters; + this.dispatch = dispatch; + this.canDispatch = canDispatch; } // --------------------------------------------------------------------------- diff --git a/src/plugins/core_view_plugin.ts b/src/plugins/core_view_plugin.ts new file mode 100644 index 0000000000..a0776a4ad4 --- /dev/null +++ b/src/plugins/core_view_plugin.ts @@ -0,0 +1,37 @@ +import { Session } from "../collaborative/session"; +import { ModelConfig } from "../model"; +import { SelectionStreamProcessor } from "../selection_stream/selection_stream_processor"; +import { StateObserver } from "../state_observer"; +import { ClientPosition, Color, Command, Currency, Getters } from "../types/index"; +import { BasePlugin } from "./base_plugin"; +import { UIActions } from "./ui_plugin"; + +export interface CoreViewPluginConfig { + readonly getters: Getters; + readonly stateObserver: StateObserver; + readonly selection: SelectionStreamProcessor; + readonly moveClient: (position: ClientPosition) => void; + readonly uiActions: UIActions; + readonly custom: ModelConfig["custom"]; + readonly session: Session; + readonly defaultCurrency?: Partial; + readonly customColors: Color[]; + readonly external: ModelConfig["external"]; +} + +export interface CoreViewPluginConstructor { + new (config: CoreViewPluginConfig): CoreViewPlugin; + getters: readonly string[]; +} + +/** + * Core view plugins handle any data derived from core date (i.e. evaluation). + * They cannot impact the model data (i.e. cannot dispatch commands). + */ +export class CoreViewPlugin extends BasePlugin { + protected getters: Getters; + constructor({ getters, stateObserver }: CoreViewPluginConfig) { + super(stateObserver); + this.getters = getters; + } +} diff --git a/src/plugins/index.ts b/src/plugins/index.ts index 9bbb04b70e..b351e770c7 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -19,6 +19,7 @@ import { SettingsPlugin } from "./core/settings"; import { SpreadsheetPivotCorePlugin } from "./core/spreadsheet_pivot"; import { TableStylePlugin } from "./core/table_style"; import { CorePluginConstructor } from "./core_plugin"; +import { CoreViewPluginConstructor } from "./core_view_plugin"; import { CustomColorsPlugin, EvaluationChartPlugin, @@ -107,7 +108,7 @@ export const statefulUIPluginRegistry = new Registry() .add("clipboard", ClipboardPlugin); // Plugins which have a derived state from core data -export const coreViewsPluginRegistry = new Registry() +export const coreViewsPluginRegistry = new Registry() .add("evaluation", EvaluationPlugin) .add("evaluation_chart", EvaluationChartPlugin) .add("evaluation_cf", EvaluationConditionalFormatPlugin) diff --git a/src/plugins/ui_core_views/cell_evaluation/evaluation_plugin.ts b/src/plugins/ui_core_views/cell_evaluation/evaluation_plugin.ts index 0c9d9b356b..b71941630e 100644 --- a/src/plugins/ui_core_views/cell_evaluation/evaluation_plugin.ts +++ b/src/plugins/ui_core_views/cell_evaluation/evaluation_plugin.ts @@ -22,7 +22,7 @@ import { isMatrix, } from "../../../types/index"; import { FormulaCellWithDependencies } from "../../core"; -import { UIPlugin, UIPluginConfig } from "../../ui_plugin"; +import { CoreViewPlugin, CoreViewPluginConfig } from "../../core_view_plugin"; import { CoreViewCommand, invalidateEvaluationCommands } from "./../../../types/commands"; import { Evaluator } from "./evaluator"; @@ -141,7 +141,7 @@ import { Evaluator } from "./evaluator"; // of other cells depending on it, at the next iteration. //#endregion -export class EvaluationPlugin extends UIPlugin { +export class EvaluationPlugin extends CoreViewPlugin { static getters = [ "evaluateFormula", "evaluateFormulaResult", @@ -164,7 +164,7 @@ export class EvaluationPlugin extends UIPlugin { private evaluator: Evaluator; private positionsToUpdate: CellPosition[] = []; - constructor(config: UIPluginConfig) { + constructor(config: CoreViewPluginConfig) { super(config); this.evaluator = new Evaluator(config.custom, this.getters); } diff --git a/src/plugins/ui_core_views/custom_colors.ts b/src/plugins/ui_core_views/custom_colors.ts index f8768b4860..13d2c5777c 100644 --- a/src/plugins/ui_core_views/custom_colors.ts +++ b/src/plugins/ui_core_views/custom_colors.ts @@ -10,7 +10,7 @@ import { toHex, } from "../../helpers"; import { Color, Command, Immutable, RGBA, TableElementStyle, UID } from "../../types"; -import { UIPlugin, UIPluginConfig } from "../ui_plugin"; +import { CoreViewPlugin, CoreViewPluginConfig } from "../core_view_plugin"; const chartColorRegex = /"(#[0-9a-fA-F]{6})"/g; @@ -70,12 +70,12 @@ interface CustomColorState { * This plugins aims to compute and keep to custom colors used in the * current spreadsheet */ -export class CustomColorsPlugin extends UIPlugin { +export class CustomColorsPlugin extends CoreViewPlugin { private readonly customColors: Immutable> = {}; private readonly shouldUpdateColors = true; static getters = ["getCustomColors"] as const; - constructor(config: UIPluginConfig) { + constructor(config: CoreViewPluginConfig) { super(config); this.tryToAddColors(config.customColors ?? []); } diff --git a/src/plugins/ui_core_views/dynamic_tables.ts b/src/plugins/ui_core_views/dynamic_tables.ts index 3c5bb2619a..c51584ae19 100644 --- a/src/plugins/ui_core_views/dynamic_tables.ts +++ b/src/plugins/ui_core_views/dynamic_tables.ts @@ -23,10 +23,9 @@ import { Zone, invalidateEvaluationCommands, } from "../../types/index"; +import { CoreViewPlugin } from "../core_view_plugin"; -import { UIPlugin } from "../ui_plugin"; - -export class DynamicTablesPlugin extends UIPlugin { +export class DynamicTablesPlugin extends CoreViewPlugin { static getters = [ "canCreateDynamicTableOnZones", "doesZonesContainFilter", diff --git a/src/plugins/ui_core_views/evaluation_chart.ts b/src/plugins/ui_core_views/evaluation_chart.ts index f7852611d5..318d12d5af 100644 --- a/src/plugins/ui_core_views/evaluation_chart.ts +++ b/src/plugins/ui_core_views/evaluation_chart.ts @@ -8,7 +8,7 @@ import { invalidateChartEvaluationCommands, invalidateEvaluationCommands, } from "../../types/commands"; -import { UIPlugin } from "../ui_plugin"; +import { CoreViewPlugin } from "../core_view_plugin"; interface EvaluationChartStyle { background: Color; @@ -19,7 +19,7 @@ interface EvaluationChartState { charts: Record; } -export class EvaluationChartPlugin extends UIPlugin { +export class EvaluationChartPlugin extends CoreViewPlugin { static getters = ["getChartRuntime", "getStyleOfSingleCellChart"] as const; charts: Record = {}; diff --git a/src/plugins/ui_core_views/evaluation_conditional_format.ts b/src/plugins/ui_core_views/evaluation_conditional_format.ts index 103f367c01..58c5fc69e9 100644 --- a/src/plugins/ui_core_views/evaluation_conditional_format.ts +++ b/src/plugins/ui_core_views/evaluation_conditional_format.ts @@ -25,14 +25,14 @@ import { invalidateCFEvaluationCommands, isMatrix, } from "../../types/index"; -import { UIPlugin } from "../ui_plugin"; +import { CoreViewPlugin } from "../core_view_plugin"; import { CoreViewCommand, invalidateEvaluationCommands } from "./../../types/commands"; type ComputedStyles = { [col: HeaderIndex]: (Style | undefined)[] }; type ComputedIcons = { [col: HeaderIndex]: (string | undefined)[] }; type ComputedDataBars = { [col: HeaderIndex]: (DataBarFill | undefined)[] }; -export class EvaluationConditionalFormatPlugin extends UIPlugin { +export class EvaluationConditionalFormatPlugin extends CoreViewPlugin { static getters = [ "getConditionalIcon", "getCellConditionalFormatStyle", diff --git a/src/plugins/ui_core_views/evaluation_data_validation.ts b/src/plugins/ui_core_views/evaluation_data_validation.ts index 1295be766e..15dfab1a74 100644 --- a/src/plugins/ui_core_views/evaluation_data_validation.ts +++ b/src/plugins/ui_core_views/evaluation_data_validation.ts @@ -16,7 +16,7 @@ import { isMatrix, } from "../../types"; import { CoreViewCommand, invalidateEvaluationCommands } from "../../types/commands"; -import { UIPlugin } from "../ui_plugin"; +import { CoreViewPlugin } from "../core_view_plugin"; import { _t } from "./../../translation"; interface InvalidValidationResult { @@ -35,7 +35,7 @@ type ValidationResult = ValidValidationResult | InvalidValidationResult; type SheetValidationResult = { [col: HeaderIndex]: Array> }; -export class EvaluationDataValidationPlugin extends UIPlugin { +export class EvaluationDataValidationPlugin extends CoreViewPlugin { static getters = [ "getDataValidationInvalidCriterionValueMessage", "getInvalidDataValidationMessage", diff --git a/src/plugins/ui_core_views/header_sizes_ui.ts b/src/plugins/ui_core_views/header_sizes_ui.ts index be9c3e6758..5fef60a247 100644 --- a/src/plugins/ui_core_views/header_sizes_ui.ts +++ b/src/plugins/ui_core_views/header_sizes_ui.ts @@ -10,7 +10,7 @@ import { } from "../../helpers"; import { Command } from "../../types"; import { CellPosition, Dimension, HeaderIndex, Immutable, Pixel, UID } from "../../types/misc"; -import { UIPlugin } from "../ui_plugin"; +import { CoreViewPlugin } from "../core_view_plugin"; interface HeaderSizeState { tallestCellInRow: Immutable>>; @@ -21,7 +21,7 @@ interface CellWithSize { size: Pixel; } -export class HeaderSizeUIPlugin extends UIPlugin implements HeaderSizeState { +export class HeaderSizeUIPlugin extends CoreViewPlugin implements HeaderSizeState { static getters = ["getRowSize", "getHeaderSize"] as const; readonly tallestCellInRow: Immutable>> = {}; diff --git a/src/plugins/ui_core_views/pivot_ui.ts b/src/plugins/ui_core_views/pivot_ui.ts index dbca6caf5c..369ed049a7 100644 --- a/src/plugins/ui_core_views/pivot_ui.ts +++ b/src/plugins/ui_core_views/pivot_ui.ts @@ -25,7 +25,8 @@ import { invalidateEvaluationCommands, } from "../../types"; import { Pivot } from "../../types/pivot_runtime"; -import { UIPlugin, UIPluginConfig } from "../ui_plugin"; +import { CoreViewPlugin, CoreViewPluginConfig } from "../core_view_plugin"; +import { UIPluginConfig } from "../ui_plugin"; export const UNDO_REDO_PIVOT_COMMANDS = ["ADD_PIVOT", "UPDATE_PIVOT"]; @@ -33,7 +34,7 @@ function isPivotCommand(cmd: CoreCommand): cmd is AddPivotCommand | UpdatePivotC return UNDO_REDO_PIVOT_COMMANDS.includes(cmd.type); } -export class PivotUIPlugin extends UIPlugin { +export class PivotUIPlugin extends CoreViewPlugin { static getters = [ "getPivot", "getFirstPivotFunction", @@ -48,7 +49,7 @@ export class PivotUIPlugin extends UIPlugin { private unusedPivots?: UID[]; private custom: UIPluginConfig["custom"]; - constructor(config: UIPluginConfig) { + constructor(config: CoreViewPluginConfig) { super(config); this.custom = config.custom; } diff --git a/src/plugins/ui_plugin.ts b/src/plugins/ui_plugin.ts index d74ae21199..945ad267a5 100644 --- a/src/plugins/ui_plugin.ts +++ b/src/plugins/ui_plugin.ts @@ -14,7 +14,7 @@ import { } from "../types/index"; import { BasePlugin } from "./base_plugin"; -type UIActions = Pick; +export type UIActions = Pick; export interface UIPluginConfig { readonly getters: Getters; @@ -47,6 +47,8 @@ export class UIPlugin extends BasePlugin { protected getters: Getters; protected ui: UIActions; protected selection: SelectionStreamProcessor; + protected dispatch: CommandDispatcher["dispatch"]; + protected canDispatch: CommandDispatcher["dispatch"]; constructor({ getters, @@ -56,10 +58,12 @@ export class UIPlugin extends BasePlugin { uiActions, selection, }: UIPluginConfig) { - super(stateObserver, dispatch, canDispatch); + super(stateObserver); this.getters = getters; this.ui = uiActions; this.selection = selection; + this.dispatch = dispatch; + this.canDispatch = canDispatch; } // ---------------------------------------------------------------------------