/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ import type { AnyContextConfigPairOrUpdater, AnyContextSymbol, ContextConfig, ContextConfigPair, ContextConfigUpdater, ContextRecord, } from './types'; import {$getEditor, createState, type LexicalEditor} from 'lexical'; let activeContext: undefined | EditorContext; type WithContext = { [K in Ctx]?: undefined | ContextRecord; }; /** * @experimental * * The LexicalEditor with context */ export type EditorContext = { editor: LexicalEditor; } & WithContext; /** * @experimental * * @param contextRecord The ContextRecord * @param cfg The configuration * @returns The value or defaultValue of cfg */ export function getContextValue( contextRecord: undefined | ContextRecord, cfg: ContextConfig, ): V { const {key} = cfg; return contextRecord && key in contextRecord ? (contextRecord[key] as V) : cfg.defaultValue; } /** * @experimental * * Read and delete cfg from this layer of context * * @param contextRecord The ContextRecord * @param cfg The configuration * @returns The value of the configuration that was removed */ export function popOwnContextValue( contextRecord: ContextRecord, cfg: ContextConfig, ): undefined | V { const rval = getOwnContextValue(contextRecord, cfg); delete contextRecord[cfg.key]; return rval; } /** * @experimental * * Get the value without a default * * @param contextRecord The ContextRecord * @param cfg The configuration * @returns The current value in this context or `undefined` if not set */ export function getOwnContextValue( contextRecord: ContextRecord, cfg: ContextConfig, ): undefined | V { const {key} = cfg; return key in contextRecord ? (contextRecord[key] as V) : undefined; } function getEditorContext(editor: LexicalEditor): undefined | EditorContext { return activeContext && activeContext.editor === editor ? activeContext : undefined; } /** * @experimental * * @param sym The symbol for this ContextRecord (e.g. DOMRenderContextSymbol) * @param editor The editor * @returns The current context or undefined */ export function getContextRecord( sym: Ctx, editor: LexicalEditor, ): undefined | ContextRecord { const editorContext = getEditorContext(editor); return editorContext && editorContext[sym]; } function toPair( contextRecord: undefined | ContextRecord, pairOrUpdater: ContextConfigPair | ContextConfigUpdater, ): ContextConfigPair { if ('cfg' in pairOrUpdater) { const {cfg, updater} = pairOrUpdater; return [cfg, updater(getContextValue(contextRecord, cfg))]; } return pairOrUpdater; } /** * Construct a new context from a parent context and pairs * * @param pairs The pairs and updaters to build the context from * @param parent The parent context * @returns The new context */ export function contextFromPairs( pairs: readonly AnyContextConfigPairOrUpdater[], parent: undefined | ContextRecord, ): undefined | ContextRecord { let rval = parent; for (const pairOrUpdater of pairs) { const [k, v] = toPair(rval, pairOrUpdater); const key = k.key; if (rval === parent && getContextValue(rval, k) === v) { continue; } // If we haven't branched away from `parent` yet, create a fresh child // context so we never mutate the caller's parent record. Subsequent // pairs in this loop accumulate into the same child. Inside the loop // `rval` is non-null after the first iteration, since createChildContext // never returns null/undefined. const ctx: ContextRecord = rval === parent || rval === undefined ? createChildContext(parent) : rval; ctx[key] = v; rval = ctx; } return rval; } function createChildContext( parent: undefined | ContextRecord, ): ContextRecord { return Object.create(parent || null); } /** * Create a context config pair that sets a value in the render context. * @experimental */ export function contextValue( cfg: ContextConfig, value: V, ): ContextConfigPair { return [cfg, value]; } /** * Create a context config updater that transforms a value in the render context. * @experimental */ export function contextUpdater( cfg: ContextConfig, updater: (prev: V) => V, ): ContextConfigUpdater { return {cfg, updater}; } /** * @internal * @experimental * @__NO_SIDE_EFFECTS__ */ export function $withFullContext( sym: Ctx, contextRecord: ContextRecord, f: () => T, editor: LexicalEditor = $getEditor(), ): T { const prevDOMContext = activeContext; const parentEditorContext = getEditorContext(editor); try { activeContext = {...parentEditorContext, editor, [sym]: contextRecord}; return f(); } finally { activeContext = prevDOMContext; } } /** * @internal * @experimental * @__NO_SIDE_EFFECTS__ */ export function $withContext( sym: Ctx, $defaults: (editor: LexicalEditor) => undefined | ContextRecord = () => undefined, ) { return ( cfg: readonly AnyContextConfigPairOrUpdater[], editor = $getEditor(), ): ((f: () => T) => T) => { return f => { const parentEditorContext = getEditorContext(editor); const parentContextRecord = parentEditorContext && parentEditorContext[sym]; const contextRecord = contextFromPairs( cfg, parentContextRecord || $defaults(editor), ); if (!contextRecord || contextRecord === parentContextRecord) { return f(); } return $withFullContext(sym, contextRecord, f, editor); }; }; } /** * @experimental * @internal * @__NO_SIDE_EFFECTS__ */ export function createContextState( tag: Tag, name: string, getDefaultValue: () => V, isEqual?: (a: V, b: V) => boolean, ): ContextConfig { return Object.assign( createState(Symbol(name), {isEqual, parse: getDefaultValue}), {[tag]: true} as const, ); }