UNPKG

@atlaskit/editor-common

Version:

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

749 lines (710 loc) 27 kB
import _slicedToArray from "@babel/runtime/helpers/slicedToArray"; import _construct from "@babel/runtime/helpers/construct"; import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray"; import _classCallCheck from "@babel/runtime/helpers/classCallCheck"; import _createClass from "@babel/runtime/helpers/createClass"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; import { EventDispatcher } from '../event-dispatcher'; /********************* * * * BASE TYPES * * * **********************/ /** * 🧱 Internal Type: Editor FE Platform * */ /** * 🧱 Internal Type: Editor FE Platform * */ /** * 🧱 Internal Type: Editor FE Platform * */ /**************************************************** * * * METADATA PROPERTIES EXTRACTION TYPES * * * ****************************************************/ /** * 🧱 Internal Type: Editor FE Platform * * Extracts the configuration type from a given plugin. * * * @returns The extracted plugin configuration type if applicable, or `never`. * * @example * ```typescript * type DogPlugin = NextEditorPlugin<'dog'>; * * // it returns never, since Dog has no configuration * type MyPluginConfiguration = ExtractPluginConfiguration<MyPlugin>; * * * type CatPlugin = NextEditorPlugin<'cat', { configuration: { color: 'red' | 'blue' } }>; * * // it returns this type { color: 'red' | 'blue' } * type MyPluginConfiguration = ExtractPluginConfiguration<MyPlugin>; * ``` */ /** * 🧱 Internal Type: Editor FE Platform * * Extracts and filters the plugin dependencies from the plugin metadata, excluding * optional dependencies. * * This type first checks if the `dependencies` property in the given `Metadata` type * is an array of `DependencyPlugin`. If true, it applies `FilterOptionalPlugins` to * filter out the optional dependencies. If the `dependencies` property does not exist * or is not an array of `DependencyPlugin`, the type resolves to an empty array. * * @returns An array of filtered plugin dependencies or an empty array. * * @example * ```typescript * type DogPlugin = NextEditorPlugin<'dog'>; * type LoudPlugin = NextEditorPlugin<'loud'>; * type BarkMetadata = {dependencies: [ * OptionalPlugin<LoudPlugin>, * DogPlugin, * ]} * type BarkPlugin = NextEditorPlugin<'bark', BarkMetadata>; * * // It returns [DogPlugin] * type RequiredDependencies = ExtractPluginDependenciesFromMetadataWithoutOptionals<BarkMetadata>; * * ``` * * You probably wants to use this other type util @see ExtractPluginDependencies * since you wouldn't need to infer the Metadata twice */ /** * 🧱 Internal Type: Editor FE Platform * * Extracts the plugin configuration from the given plugin metadata if the * `pluginConfiguration` property exists. * * This type conditionally checks if the `Metadata` type includes a `pluginConfiguration` * key. If such a key exists, the type of `pluginConfiguration` is returned. If not, * the type resolves to `never`. */ /******************************** * * * TYPE INFER * * * *********************************/ /** * 🧱 Internal Type: Editor FE Platform * * Extracts the NextEditorPlugin type from a PresetPuglin, * this is useful because the EditorPresetBuilder can accept the plugin in multiple ways: * * @example * ``` * preset * // valid * .add([plugin, { myConfiguration }] // Type: [NextEditorPlugin, Configuration] * * // valid * .add([plugin]) // Type: [NextEditorPlugin, Configuration?] * * // valid * .add(plugin) // Type: NextEditorPlugin * * ``` * * This type conditionally checks if `Plugin` is an array. If it is an array, it then checks if the first element * (`MPlugin`) extends `NextEditorPlugin`. But if `Plugin` directly extends `NextEditorPlugin`, it returns the `Plugin` * type itself. Otherwise, it resolves to `never`. * * You probably wants to use this if you need to extract the NextEditorPlugin from a @see PresetPlugin . * Since the PresetPlugin is an union between a tuple and a plugin. */ /** * 🧱 Internal Type: Editor FE Platform * * Extracts non-optional plugin dependencies, excluding any optional dependencies, from a given plugin's metadata. * * We can declare the depencies like this: * * @example * ```typescript * NextEditorPlugin<'bark', { * dependencies: [DogPlugin, Optional<LoudPlugin>] * }> * * ``` * * * This tyope is similar to @see ExtractPluginDependenciesFromMetadataWithoutOptionals * but you can use it to extract the non-optional-dependencies from any NextEditorPlugin without infer the metadata * * @example * ```typescript * type BarkPlugin = NextEditorPlugin<'bark', { * dependencies: [DogPlugin, Optional<LoudPlugin>] * }> * * type PluginDependencies = ExtractPluginDependencies<BarkPlugin>; // Type: [DogPlugin] * ``` */ /** * 🧱 Internal Type: Editor FE Platform * * Extracts the NextEditorPlugin type from a PluginWithConfiguration. * * * You probably wants to use this if you need to extract the NextEditorPlugin from a @see PresetPlugin . * Since the PresetPlugin is an union between a tuple and a plugin. */ /** * 🧱 Internal Type: Editor FE Platform * * Extracts the plugin name from a PresetPlugins. * * @example * ```typescript * ExtractPluginNameFromAllBuilderPlugins<NextEditorPlugin<'bark'>> // 'bark' * * ExtractPluginNameFromAllBuilderPlugins<[NextEditorPlugin<'dog'>, { configuration: {} }> // 'dog' * * ``` * Similar to @see ExtractPluginAllBuilderPlugins, this type conditionally checks if `Plugin` is an array. If it is, * it attempts to extract the name of the first plugin (`MPlugin`) in the array that extends `NextEditorPlugin` with * a name and any metadata. If `Plugin` itself directly extends `NextEditorPlugin`, it extracts the plugin's name. * If none of these conditions are met, it resolves to `never`. * */ /****************************** * * * MAPPED TUPLES * * * ******************************/ /** * 🧱 Internal Type: Editor FE Platform * * Filters out optional plugins from a tuple of dependency plugins. * * * This type is using the Tail Head trick to map a tuple to another one. * It does this by conditionally iterating over each element in the tuple: if the head of the tuple (the first element) * is an optional plugin, it is excluded from the resulting tuple; otherwise, it is included. This process is repeated * for the tail (the remaining elements) of the tuple until all elements have been evaluated. * */ /** * 🧱 Internal Type: Editor FE Platform * * One of the main type system for the EditorPresetBuilder. * * Verifies if a given plugin's dependencies are satisfied within a provided stack of plugins. * * Usually, the stack of plugins are coming from a generic parameter in the EditorPresetBuilder<PluginNames, PluginStack>. * * This type checks if the dependencies of the given `Plugin` are included in the provided `PluginsStack`. * * - If the plugin has no dependencies, it simply returns the plugin itself, (provided it is either a `PluginWithConfiguration` or `NextEditorPlugin`, in case someone tries to add a non-NextEditorPlugin to the Preset) * * - If the plugin has dependencies, it verifies each dependency against the `PluginsStack` to ensure * they are present. This includes checking direct dependencies as well as dependencies hidden inside tuples (by unwrapping * them). If all dependencies are satisfied, it returns the plugin; otherwise, it resolves to `never`. * * * @example * ```typescript * type DogPlugin = NextEditorPlugin<'dog'>; * type LoudPlugin = NextEditorPlugin<'loud'>; * type BarkPlugin = NextEditorPlugin<'bark', { dependencies: [DogPlugin, LoudPlugin] }>; * * * // When there we are missing dependencies * VerifyPluginDependencies<BarkPlugin, [DogPlugin]> // Type: never * * * // When there all dependencies are already added on the stack * VerifyPluginDependencies<BarkPlugin, [DogPlugin, LoudPlugin]> // Type: BarkPlugin * * ``` */ /******************************** * * * BETTER ERROR MESSAGE TYPES * * * *********************************/ /** * 🧱 Internal Type: Editor FE Platform * * TypeScript doesn't allow custom error messages (yet). So, use this type to force a specific error message to the user. * * This is useful because in a situation where a Preset has too many plugins, its become really hard to understand what the error message is. * * Extracts the names of required dependencies for a given plugin, or provides an error message if dependencies are * missing, invalid, or if the plugin itself is not a recognized NextEditorPlugin. * * This type evaluates whether a given `Plugin` has defined dependencies. If dependencies are absent, it returns * a message indicating no dependencies were found. If dependencies are present but do not conform to expected types, * or if an unspecified issue occurs, appropriate error messages are generated. Valid dependencies result in the * extraction of their names; otherwise, an error message specific to the situation is returned. * * It is used by the @see GetDependencyErrorMessage to group all error messages when a new plugin is being added into a preset. */ /** * 🧱 Internal Type: Editor FE Platform * * Retrieves an error message if any dependency-related issues are detected for a given plugin within a specified * plugin stack. This includes missing dependencies or other errors as identified by `ExtractRequiredDependencies`. * * It attempts to extract required dependencies for the `Plugin` from the `StackPlugins`. If the result is a string, * it indicates a missing dependency and constructs an error message accordingly. Otherwise, it directly returns the * result from `ExtractRequiredDependencies`, which could be an error message detailing the issue encountered. * * It is used by the @see SafePresetCheck to make improve the error message */ /** * 🧱 Internal Type: Editor FE Platform * * Filters through an array of dependency plugins, removing any that do not exist in the provided plugins stack. * * This type recursively checks each plugin dependency against the provided `PluginsStack`. If a dependency is found * within the stack, it is included in the result; otherwise, it is excluded. This process helps in identifying * missing plugins from a set of required dependencies. * */ /***************************** * * * VALIDATION HELPER TYPES * * * ******************************/ /** * 🧱 Internal Type: Editor FE Platform * * Checks for duplicate plugin entries within a stack of plugins. If a duplicate is found, it returns an error message; * otherwise, it proceeds without error. * * This type primarily serves to ensure that each plugin in the plugin stack is unique, preventing issues related to * duplicate plugin registration. It also includes a check to accommodate scenarios where strict typing is bypassed. * * If the plugin is used with other configuration this type will not complain. */ /** * 🧱 Internal Type: Editor FE Platform * * Verifies if a given plugin meets basic requirements to be considered a valid editor plugin. * * This type checks if the plugin is a function that matches the expected signature for an next editor plugin. If it does, * it further checks the plugin's configuration requirements to ensure compatibility and adherence to expected * configurations. * */ // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any /** * 🧱 Internal Type: Editor FE Platform * * Evaluates whether a plugin's configuration meets the requirements to be used either as a standalone plugin or * as part of a plugin-with-configuration tuple. * * This type assesses the plugin configuration's status—whether it's optional, mandatory, or not present—and determines * the valid ways in which the plugin can be registered or used. This is crucial for maintaining backward compatibility * and ensuring plugins are correctly configured upon registration into the Preset * */ /***************************** * * * EDITOR API HELPER TYPES * * * ******************************/ /** * 🧱 Internal Type: Editor FE Platform * * Extracts the numeric indices as literal types from a tuple. * * This utility type takes a tuple and produces a union of its numeric indices as literal types. It's useful for * iterating over tuples with TypeScript's mapped types, allowing for operations on each tuple element based on its index. * * It is being used to separate plugins registred with `preset.maybeAdd` and `preset.add`. */ // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any /** * 🧱 Internal Type: Editor FE Platform * * Constructs a plugin api type with optional properties based on the optional plugins from a given tuple of plugins. * * This type iterates over a tuple of plugins and checks for plugins marked as optional (indicated by the presence * of `undefined`). For each optional plugin, it attempts to extract the plugin's name and corresponding * `PluginDependenciesAPI` type. The resulting object type has properties with these plugin names as keys and their * respective APIs as optional values. * * @example * ```typescript * type DogPlugin = NextEditorPlugin<'dog'>; * type CatPlugin = NextEditorPlugin<'cat'>; * * * BuildOptionalAPIEntry<[DogPlugin, MaybePlugin<CatPlugin>]> // Type: { cat?: { } } * * ``` */ /** * 🧱 Internal Type: Editor FE Platform * Generates a plugin api type with properties based on the required plugins from a given tuple of plugins. * * This type traverses a tuple of plugins, focusing on those not marked as optional. For each required plugin, * it extracts the plugin's name to use as a key and determines the corresponding `PluginDependenciesAPI` type * for the value. The resulting object type includes these key-value pairs, ensuring that each required plugin * has a defined API entry in the object. * * @example * ```typescript * type DogPlugin = NextEditorPlugin<'dog'>; * type CatPlugin = NextEditorPlugin<'cat'>; * * * BuildOptionalAPIEntry<[DogPlugin, MaybePlugin<CatPlugin>]> // Type: { dog?: { } } * * ``` */ /** * 🧱 Internal Type: Editor FE Platform * * Forces the expansion (simplification/normalization) of conditional and mapped types. * This can be particularly useful for making the types more readable and manageable in * environments like IntelliSense or when generating type documentation. * * More info {@link https://github.com/microsoft/TypeScript/issues/47980 TypeScript/issues/47980} */ /************************* * * * PUBLIC TYPES * * * *************************/ /** * 🧱 Internal Type: Editor FE Platform * * Represents a utility type that wraps a string type in a tuple, often used to denote * plugin names that might be optionally included or excluded in certain contexts within * the editor preset builder. */ /** * 🧱 Internal Type: Editor FE Platform * * A union type that represents a plugin which could either be a standalone `NextEditorPlugin` * or a `PluginWithConfiguration` that bundles a plugin with its specific configuration. * * This type is fundamental in managing plugins within presets, allowing for flexible plugin * registration that accommodates plugins with or without explicit configurations. */ /** * 🧱 Internal Type: Editor FE Platform * * A union type that aggregates all possible plugin name representations within the editor preset builder, * including simple strings for direct plugin names and wrapped strings in tuples when a plugin is registred with `maybeAdd`. * */ /** * 🧱 Internal Type: Editor FE Platform * * Represents all possible types of plugins that can be included within an editor preset. * This includes both `PresetPlugin` types and `MaybePlugin` types, accommodating a wide range * of plugin registration scenarios likw: * * @example * ```typescript * preset * .add([plugin, { myConfiguration }] * .add([plugin]) * .add(plugin) * .maybeAdd(plugin, () => true); * .maybeAdd([plugin], () => true); * .maybeAdd([plugin, { myConfiguration }], () => true); * * ``` */ // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any /** * 🧱 Internal Type: Editor FE Platform * * Performs a series of checks to ensure that a given plugin can be safely added to a preset. * This includes verifying the plugin's dependencies, checking for duplicate registrations, and ensuring * the plugin meets basic criteria for being considered a valid plugin. * * @returns The plugin type if all checks pass, or error messages detailing why the plugin cannot be added. */ /** * 📢 Public Type API * * Extracts the complete API surface for a given editor preset, including both core and plugin-specific APIs. * This type dynamically assembles the API object based on the included plugins, differentiating between * optional and required plugins to accurately reflect the available API calls. * * @template Preset The editor preset builder instance from which to extract the API. * @returns An object type representing the complete API surface for the given preset. * * @example * ```typescript * const dogPlugin: NextEditorPlugin<'dog'>; * const catPlugin: NextEditorPlugin<'cat'>; * * const myPreset = new EditorPresetBuilder() * .add(dogPlugin) * .maybeAdd(catPlugin, () => true) * * const api: ExtractPresetAPI<typeof myPreset>; * * * // Core is always available * api.core.actions * * // Dog was registred with `add`, so it will always be available * api.dog.actions * * // Cat was registred with `maybeAdd`, so it may not be available on runtime * api.cat?.actions * ``` */ // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-explicit-any /************************* * * * PROP TYPES * * * *************************/ /** * This class is the main way to build an Editor. * * A Preset is an immutable object, any modification like `.add` or `.maybeAdd` * will always result in a new preset instance. * * ⚠️⚠️⚠️ ATTENTION ⚠️⚠️⚠️ * For ComposableEditor, a new Preset means a full redraw, * it is one of the most expensive operation. * Please make sure you aren't recreating this all the time. * * EditorAPI: * In case you need access to the EditorAPI type definition based in the preset you have. * Please use the util type exported in this package: @see ExtractPresetAPI<Preset> * * ```typescript * const myPreset = new EditorPresetBuilder() * .add(pluginDog) * .add(pluginCat); * * * function someFunc(myApi: ExtractPresetAPI<typeof myPreset>) { * * } * ``` * * If your code is inside an EditorPlugin you should be using the @see ExtractInjectionAPI. */ export var EditorPresetBuilder = /*#__PURE__*/function () { function EditorPresetBuilder() { var _this = this; _classCallCheck(this, EditorPresetBuilder); // eslint-disable-next-line @repo/internal/deprecations/deprecation-ticket-required -- Ignored via go/ED-25883 /** * @deprecated Use `apiResolver` instead */ /** * Returns the editor API when resolved. * This occurs when the preset is initially built. */ _defineProperty(this, "apiEmitter", function () {}); _defineProperty(this, "safeEntry", function (plugin) { return Array.isArray(plugin) ? plugin : [plugin, undefined]; }); for (var _len = arguments.length, more = new Array(_len), _key = 0; _key < _len; _key++) { more[_key] = arguments[_key]; } this.data = [].concat(more) || []; this.apiPromise = new Promise(function (r) { return _this.resolver = r; }); this.apiResolver = new APIDispatcher(function (emitter) { return _this.apiEmitter = emitter; }); } return _createClass(EditorPresetBuilder, [{ key: "add", value: function add(nextOrTuple) { return _construct(EditorPresetBuilder, [ /** * re-cast this to NewPlugin as we've done all the type * safety, dependency checking, narrowing, during * `SafePresetCheck & VerifyPluginDependencies` */ nextOrTuple].concat(_toConsumableArray(this.data))); } }, { key: "maybeAdd", value: function maybeAdd(pluginToAdd, shouldAdd) { var pluginOrBuilder = typeof shouldAdd === 'function' ? // @ts-expect-error Argument of type 'SafePresetCheck<ToAddPlugin, StackPlugins>' is not assignable to parameter of type 'ToAddPlugin'. shouldAdd(pluginToAdd, this) : shouldAdd; if (pluginOrBuilder instanceof EditorPresetBuilder) { return pluginOrBuilder; } var nextPluginStack = [ /** * re-cast this to NewPlugin as we've done all the type * safety, dependency checking, narrowing, during * `SafePresetCheck & VerifyPluginDependencies` */ pluginOrBuilder ? pluginToAdd : undefined].concat(_toConsumableArray(this.data)); var nextEditorPresetBuilder = _construct(EditorPresetBuilder, _toConsumableArray(nextPluginStack)); return nextEditorPresetBuilder; } }, { key: "has", value: function has(plugin) { return this.data.some(function (pluginPreset) { if (Array.isArray(pluginPreset)) { return pluginPreset[0] === plugin; } return pluginPreset === plugin; }); } }, { key: "build", value: function build() { var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, pluginInjectionAPI = _ref.pluginInjectionAPI, maybeExcludePlugins = _ref.excludePlugins; var excludePlugins = new Set(maybeExcludePlugins ? maybeExcludePlugins : []); var editorPlugins = this.processEditorPlugins({ pluginInjectionAPI: pluginInjectionAPI, excludePlugins: excludePlugins }); if (pluginInjectionAPI) { var _this$resolver; // The pluginInjectionAPI API doesn't have information enough to build a proper type for the API. // It is returning a generic type but on top of the Proxy system // So, we can safely recast it here this.apiEmitter(pluginInjectionAPI.api()); // Deprecated approach (_this$resolver = this.resolver) === null || _this$resolver === void 0 || _this$resolver.call(this, pluginInjectionAPI.api()); } return this.removeExcludedPlugins(editorPlugins, excludePlugins); } }, { key: "verifyDuplicatedPlugins", value: function verifyDuplicatedPlugins() { var cache = new Set(); this.data.filter(Boolean).forEach(function (pluginEntry) { var _ref2 = Array.isArray(pluginEntry) ? pluginEntry : [pluginEntry, undefined], _ref3 = _slicedToArray(_ref2, 2), pluginFn = _ref3[0], _ = _ref3[1]; if (cache.has(pluginFn)) { throw new Error("".concat(pluginFn, " is already included!")); } cache.add(pluginFn); }); return true; } }, { key: "processEditorPlugins", value: function processEditorPlugins(_ref4) { var _this2 = this; var pluginInjectionAPI = _ref4.pluginInjectionAPI, excludePlugins = _ref4.excludePlugins; this.verifyDuplicatedPlugins(); var seen = new Set(); var pluginsDataCopy = this.data.slice(); var plugins = pluginsDataCopy.reverse().filter(Boolean).map(function (entry) { var _this2$safeEntry = _this2.safeEntry(entry), _this2$safeEntry2 = _slicedToArray(_this2$safeEntry, 2), fn = _this2$safeEntry2[0], config = _this2$safeEntry2[1]; if (seen.has(fn)) { return null; } seen.add(fn); if (typeof fn !== 'function') { return null; } var plugin = pluginInjectionAPI ? fn({ config: config, api: pluginInjectionAPI.api() }) : fn({ config: config }); if (plugin && excludePlugins !== null && excludePlugins !== void 0 && excludePlugins.has(plugin.name)) { return null; } if (!pluginInjectionAPI) { return plugin; } pluginInjectionAPI.onEditorPluginInitialized(plugin); return plugin; }).filter(Boolean); return plugins; } }, { key: "removeExcludedPlugins", value: function removeExcludedPlugins(plugins, excludes) { if (excludes) { return plugins.filter(function (plugin) { return !plugin || !excludes.has(plugin.name); }); } return plugins; } }]); }(); /** * WARNING: Internal object * * Dedicated wrapper around EventDispatcher for public API around the editor API. * This only has the public method `on` which is used to listen to updates to the editor API. * * This shouldn't really be used externally - the `editorAPI` should be accessed via `usePreset`. */ var APIDispatcher = /*#__PURE__*/function () { function APIDispatcher(emitter) { var _this3 = this; _classCallCheck(this, APIDispatcher); _defineProperty(this, "eventDispatcher", new EventDispatcher()); _defineProperty(this, "key", 'api-resolved-event'); // Need to store the last event created in case we subscribe after the fact _defineProperty(this, "initialEvent", undefined); this.emitter = emitter; this.emitter(function (v) { _this3.initialEvent = v; _this3.eventDispatcher.emit(_this3.key, v); }); } /** * Used to observe Editor API events * * @param cb Callback to listen to the editor API. * This will also emit the last event if the stream has already started. * @returns Cleanup function to cleanup the listener */ return _createClass(APIDispatcher, [{ key: "on", value: function on(cb) { var _this4 = this; if (this.initialEvent !== undefined) { // Keeps timing consistent with old approach - certain products // ie. ai-mate relied on this. Promise.resolve().then(function () { return cb(_this4.initialEvent); }); } this.eventDispatcher.on(this.key, cb); return function () { _this4.eventDispatcher.off(_this4.key, cb); }; } }, { key: "destroy", value: function destroy() { this.eventDispatcher.destroy(); } }]); }();