UNPKG

@atlaskit/editor-common

Version:

A package that contains common classes and components for editor and renderer

237 lines • 6.78 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; import isEqual from 'lodash/isEqual'; import throttle from 'lodash/throttle'; import { corePlugin } from './core-plugin'; function hasGetSharedState(plugin) { return typeof plugin.getSharedState === 'function'; } function hasActions(plugin) { return typeof plugin.actions === 'object'; } function hasCommands(plugin) { return typeof plugin.commands === 'object'; } const filterPluginsWithListeners = ({ listeners, plugins }) => Array.from(listeners.keys()).map(pluginName => plugins.get(pluginName)).filter(plugin => plugin !== undefined && hasGetSharedState(plugin)); const extractSharedStateFromPlugins = ({ oldEditorState, newEditorState, plugins }) => { const isInitialization = !oldEditorState && newEditorState; const result = new Map(); for (let plugin of plugins) { if (!plugin || !hasGetSharedState(plugin)) { continue; } const nextSharedState = plugin.getSharedState(newEditorState); const prevSharedState = !isInitialization && oldEditorState ? plugin.getSharedState(oldEditorState) : undefined; const isSamePluginState = isEqual(prevSharedState, nextSharedState); if (isInitialization || !isSamePluginState) { result.set(plugin.name, { nextSharedState, prevSharedState }); } } return result; }; const THROTTLE_CALLS_FOR_MILLISECONDS = 0; const notifyListenersThrottled = throttle(({ listeners, updatesToNotifyQueue }) => { const callbacks = []; for (let [pluginName, diffs] of updatesToNotifyQueue.entries()) { const pluginListeners = listeners.get(pluginName) || []; pluginListeners.forEach(callback => { diffs.forEach(diff => { callbacks.push(callback.bind(callback, diff)); }); }); } updatesToNotifyQueue.clear(); if (callbacks.length === 0) { return; } callbacks.reverse().forEach(cb => { cb(); }); }, THROTTLE_CALLS_FOR_MILLISECONDS); export class PluginsData {} class ActionsAPI { createAPI(plugin) { if (!plugin || !hasActions(plugin)) { return {}; } return new Proxy(plugin.actions || {}, { get: function (target, prop, receiver) { // We will be able to track perfomance here return Reflect.get(target, prop); } }); } } class EditorCommandsAPI { createAPI(plugin) { if (!plugin || !hasCommands(plugin)) { return {}; } return new Proxy(plugin.commands || {}, { get: function (target, prop, receiver) { // We will be able to track perfomance here return Reflect.get(target, prop); } }); } } export class SharedStateAPI { constructor({ getEditorState }) { _defineProperty(this, "updatesToNotifyQueue", new Map()); this.getEditorState = getEditorState; this.listeners = new Map(); } createAPI(plugin) { if (!plugin) { return { currentState: () => undefined, onChange: sub => { return () => {}; } }; } const pluginName = plugin.name; return { currentState: () => { if (!hasGetSharedState(plugin)) { return undefined; } const state = this.getEditorState(); return plugin.getSharedState(state); }, onChange: sub => { const pluginListeners = this.listeners.get(pluginName) || new Set(); pluginListeners.add(sub); this.listeners.set(pluginName, pluginListeners); return () => this.cleanupSubscription(pluginName, sub); } }; } cleanupSubscription(pluginName, sub) { (this.listeners.get(pluginName) || new Set()).delete(sub); } notifyListeners({ newEditorState, oldEditorState, plugins }) { const { listeners, updatesToNotifyQueue } = this; const pluginsFiltered = filterPluginsWithListeners({ plugins, listeners }); const sharedStateDiffs = extractSharedStateFromPlugins({ oldEditorState, newEditorState, plugins: pluginsFiltered }); if (sharedStateDiffs.size === 0) { return; } for (let [pluginName, nextDiff] of sharedStateDiffs) { const currentDiffQueue = updatesToNotifyQueue.get(pluginName) || []; updatesToNotifyQueue.set(pluginName, [...currentDiffQueue, nextDiff]); } notifyListenersThrottled({ updatesToNotifyQueue, listeners }); } destroy() { this.listeners.clear(); this.updatesToNotifyQueue.clear(); } } export class EditorPluginInjectionAPI { constructor({ getEditorState, getEditorView }) { _defineProperty(this, "onEditorViewUpdated", ({ newEditorState, oldEditorState }) => { this.sharedStateAPI.notifyListeners({ newEditorState, oldEditorState, plugins: this.plugins }); }); _defineProperty(this, "onEditorPluginInitialized", plugin => { this.addPlugin(plugin); }); _defineProperty(this, "addPlugin", plugin => { // Plugins other than `core` are checked by the preset itself // For some reason in some tests we have duplicates that are missed. // To follow-up in ED-19611 if (plugin.name === 'core' && this.plugins.has(plugin.name)) { throw new Error(`Plugin ${plugin.name} has already been initialised in the Editor API! There cannot be duplicate plugins or you will have unexpected behaviour`); } this.plugins.set(plugin.name, plugin); }); _defineProperty(this, "getPluginByName", pluginName => { const plugin = this.plugins.get(pluginName); return plugin; }); this.sharedStateAPI = new SharedStateAPI({ getEditorState }); this.plugins = new Map(); this.actionsAPI = new ActionsAPI(); this.commandsAPI = new EditorCommandsAPI(); // Special core plugin that is always added this.addPlugin(corePlugin({ config: { getEditorView } })); } api() { const { sharedStateAPI, actionsAPI, commandsAPI, getPluginByName } = this; return new Proxy({}, { get: function (target, prop, receiver) { // If we pass this as a prop React hates us // Let's just reflect the result and ignore these if (prop === 'toJSON') { return Reflect.get(target, prop); } const plugin = getPluginByName(prop); if (!plugin) { return undefined; } const sharedState = sharedStateAPI.createAPI(plugin); const actions = actionsAPI.createAPI(plugin); const commands = commandsAPI.createAPI(plugin); const proxyCoreAPI = { sharedState, actions, commands }; return proxyCoreAPI; } }); } }