UNPKG

@atlaskit/editor-core

Version:

A package contains Atlassian editor core functionality

942 lines (917 loc) 62.5 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _typeof = require("@babel/runtime/helpers/typeof"); Object.defineProperty(exports, "__esModule", { value: true }); exports.ReactEditorView = ReactEditorView; exports.default = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); var _react = _interopRequireWildcard(require("react")); var _reactIntl = require("react-intl"); var _v = _interopRequireDefault(require("uuid/v4")); var _analytics = require("@atlaskit/editor-common/analytics"); var _coreUtils = require("@atlaskit/editor-common/core-utils"); var _eventDispatcher = require("@atlaskit/editor-common/event-dispatcher"); var _hooks = require("@atlaskit/editor-common/hooks"); var _isPerformanceApiAvailable = require("@atlaskit/editor-common/is-performance-api-available"); var _nodeVisibility = require("@atlaskit/editor-common/node-visibility"); var _normalizeFeatureFlags = require("@atlaskit/editor-common/normalize-feature-flags"); var _measureRender = require("@atlaskit/editor-common/performance/measure-render"); var _navigation = require("@atlaskit/editor-common/performance/navigation"); var _ssrMeasures = require("@atlaskit/editor-common/performance/ssr-measures"); var _preset = require("@atlaskit/editor-common/preset"); var _processRawValue = require("@atlaskit/editor-common/process-raw-value"); var _uiReact = require("@atlaskit/editor-common/ui-react"); var _analytics2 = require("@atlaskit/editor-common/utils/analytics"); var _document = require("@atlaskit/editor-common/utils/document"); var _model = require("@atlaskit/editor-prosemirror/model"); var _state2 = require("@atlaskit/editor-prosemirror/state"); var _view = require("@atlaskit/editor-prosemirror/view"); var _editorSsrRenderer = require("@atlaskit/editor-ssr-renderer"); var _platformFeatureFlags = require("@atlaskit/platform-feature-flags"); var _interactionIdContext = require("@atlaskit/react-ufo/interaction-id-context"); var _interactionMetrics = require("@atlaskit/react-ufo/interaction-metrics"); var _expValEquals = require("@atlaskit/tmp-editor-statsig/exp-val-equals"); var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure"); var _useProviders = require("../composable-editor/hooks/useProviders"); var _featureFlagsFromProps = require("../utils/feature-flags-from-props"); var _getNodesCount = require("../utils/getNodesCount"); var _getNodesCountWithExtensionKeys = require("../utils/getNodesCountWithExtensionKeys"); var _getNodesVisibleInViewport = require("../utils/getNodesVisibleInViewport"); var _isChromeless = require("../utils/is-chromeless"); var _isFullPage = require("../utils/is-full-page"); var _RenderTracking = require("../utils/performance/components/RenderTracking"); var _measureEnum = _interopRequireDefault(require("../utils/performance/measure-enum")); var _consts = require("./consts"); var _createEditor = require("./create-editor"); var _createPluginsList = _interopRequireDefault(require("./create-plugins-list")); var _createSchema = require("./create-schema"); var _filterPluginsForReconfigure = require("./filter-plugins-for-reconfigure"); var _messages = require("./messages"); var _focusEditorElement = require("./ReactEditorView/focusEditorElement"); var _getUAPrefix = require("./ReactEditorView/getUAPrefix"); var _handleEditorFocus = require("./ReactEditorView/handleEditorFocus"); var _useDispatchTransaction = require("./ReactEditorView/useDispatchTransaction"); var _useFireFullWidthEvent = require("./ReactEditorView/useFireFullWidthEvent"); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); } function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead var EDIT_AREA_ID = 'ak-editor-textarea'; var SSR_TRACE_SEGMENT_NAME = 'reactEditorView'; var bootStartTime = (0, _isPerformanceApiAvailable.isPerformanceAPIAvailable)() ? performance.now() : undefined; // `markdown↔rich` toggles drop different node/mark sets, so the unique // name set is enough to detect when a destructive rebuild is needed. function sameNames(a, b) { var setA = new Set(a); var setB = new Set(b); if (setA.size !== setB.size) { return false; } var _iterator = _createForOfIteratorHelper(setA), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var name = _step.value; if (!setB.has(name)) { return false; } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } return true; } function schemaShapeChanged(current, next) { return !sameNames(Object.keys(current.nodes), next.nodes.map(function (n) { return n.name; })) || !sameNames(Object.keys(current.marks), next.marks.map(function (m) { return m.name; })); } function ReactEditorView(props) { var _pluginInjectionAPI$c, _media, _linking, _document$querySelect, _props$render, _props$render2; // Should be always the first statement in the component var firstRenderStartTimestampRef = (0, _react.useRef)(performance.now()); var preset = props.preset, _props$editorProps = props.editorProps, onSSRMeasure = _props$editorProps.onSSRMeasure, nextAppearance = _props$editorProps.appearance, disabled = _props$editorProps.disabled, editorPropFeatureFlags = _props$editorProps.featureFlags, errorReporterHandler = _props$editorProps.errorReporterHandler, defaultValue = _props$editorProps.defaultValue, shouldFocus = _props$editorProps.shouldFocus, __livePage = _props$editorProps.__livePage, onEditorCreated = props.onEditorCreated, onEditorDestroyed = props.onEditorDestroyed; var ssrEditorStateRef = (0, _react.useRef)(undefined); var editorRef = (0, _react.useRef)(null); var viewRef = (0, _react.useRef)(); var focusTimeoutId = (0, _react.useRef)(); // ProseMirror is instantiated prior to the initial React render cycle, // so we allow transactions by default, to avoid discarding the initial one. var canDispatchTransactions = (0, _react.useRef)(true); // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead var editorId = (0, _react.useRef)((0, _v.default)()); var eventDispatcher = (0, _react.useMemo)(function () { return new _eventDispatcher.EventDispatcher(); }, []); var config = (0, _react.useRef)({ nodes: [], marks: [], pmPlugins: [], contentComponents: [], pluginHooks: [], primaryToolbarComponents: [], secondaryToolbarComponents: [], onEditorViewStateUpdatedCallbacks: [] }); var contentTransformer = (0, _react.useRef)(undefined); var featureFlags = (0, _react.useMemo)(function () { return (0, _featureFlagsFromProps.createFeatureFlagsFromProps)(editorPropFeatureFlags); }, [editorPropFeatureFlags]); var getEditorState = (0, _react.useCallback)(function () { var _ssrEditorStateRef$cu, _viewRef$current; return (_ssrEditorStateRef$cu = ssrEditorStateRef.current) !== null && _ssrEditorStateRef$cu !== void 0 ? _ssrEditorStateRef$cu : (_viewRef$current = viewRef.current) === null || _viewRef$current === void 0 ? void 0 : _viewRef$current.state; }, []); var getEditorView = (0, _react.useCallback)(function () { return viewRef.current; }, []); var dispatch = (0, _react.useMemo)(function () { return (0, _eventDispatcher.createDispatch)(eventDispatcher); }, [eventDispatcher]); var errorReporter = (0, _react.useMemo)(function () { return (0, _createEditor.createErrorReporter)(errorReporterHandler); }, [errorReporterHandler]); var handleAnalyticsEvent = (0, _react.useCallback)(function (payload) { (0, _analytics.fireAnalyticsEvent)(props.createAnalyticsEvent)(payload); }, [props.createAnalyticsEvent]); var dispatchAnalyticsEvent = (0, _react.useCallback)(function (payload) { var dispatch = (0, _eventDispatcher.createDispatch)(eventDispatcher); dispatch(_analytics2.analyticsEventKey, { payload: payload }); }, [eventDispatcher]); var pluginInjectionAPI = (0, _react.useRef)(new _preset.EditorPluginInjectionAPI({ getEditorState: getEditorState, getEditorView: getEditorView, fireAnalyticsEvent: handleAnalyticsEvent, appearance: nextAppearance })); var parseDoc = (0, _react.useCallback)(function (schema, api, options) { if (!options.doc) { return undefined; } // if the collabEdit API is set, skip this validation due to potential pm validation errors // from docs that end up with invalid marks after processing (See #hot-111702 for more details) if ((0, _coreUtils.isSSR)() || (api === null || api === void 0 ? void 0 : api.collabEdit) !== undefined || options.props.editorProps.skipValidation) { return (0, _processRawValue.processRawValueWithoutValidation)(schema, options.doc, dispatchAnalyticsEvent); } else { return (0, _processRawValue.processRawValue)(schema, options.doc, options.props.providerFactory, options.props.editorProps.sanitizePrivateContent, contentTransformer.current, dispatchAnalyticsEvent); } }, [dispatchAnalyticsEvent]); var createEditorState = (0, _react.useCallback)(function (options) { var _api$editorViewMode; var schema; if (viewRef.current) { if (options.resetting) { /** * ReactEditorView currently does NOT handle dynamic schema, * We are reusing the existing schema, and rely on #reconfigureState * to update `this.config` */ schema = viewRef.current.state.schema; } else { /** * There's presently a number of issues with changing the schema of a * editor inflight. A significant issue is that we lose the ability * to keep track of a user's history as the internal plugin state * keeps a list of Steps to undo/redo (which are tied to the schema). * Without a good way to do work around this, we prevent this for now. */ // eslint-disable-next-line no-console console.warn('The editor does not support changing the schema dynamically.'); return viewRef.current.state; } } else { config.current = (0, _createEditor.processPluginsList)((0, _createPluginsList.default)(options.props.preset, 'allowBlockType' in props.editorProps ? props.editorProps : {}, pluginInjectionAPI.current)); if ((0, _expValEquals.expValEquals)('platform_editor_appearance_shared_state', 'isEnabled', true)) { var _config$current$pmPlu; (_config$current$pmPlu = config.current.pmPlugins).push.apply(_config$current$pmPlu, (0, _toConsumableArray2.default)(pluginInjectionAPI.current.getInternalPMPlugins())); } schema = (0, _createSchema.createSchema)(config.current); } var contentTransformerProvider = options.props.editorProps.contentTransformerProvider; var plugins = (0, _createEditor.createPMPlugins)({ schema: schema, dispatch: dispatch, errorReporter: errorReporter, editorConfig: config.current, eventDispatcher: eventDispatcher, providerFactory: options.props.providerFactory, portalProviderAPI: props.portalProviderAPI, nodeViewPortalProviderAPI: props.nodeViewPortalProviderAPI, dispatchAnalyticsEvent: dispatchAnalyticsEvent, featureFlags: featureFlags, getIntl: function getIntl() { return props.intl; }, onEditorStateUpdated: pluginInjectionAPI.current.onEditorViewUpdated }); contentTransformer.current = contentTransformerProvider ? contentTransformerProvider(schema) : undefined; var api = pluginInjectionAPI.current.api(); // If we have a doc prop, we need to process it into a PMNode var doc = parseDoc(schema, api, options); var isViewMode = (api === null || api === void 0 || (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.sharedState.currentState().mode) === 'view'; var selection; if (doc) { if (isViewMode) { var emptySelection = new _state2.TextSelection(doc.resolve(0)); return _state2.EditorState.create({ schema: schema, plugins: plugins, doc: doc, selection: emptySelection }); } else { selection = options.selectionAtStart ? _state2.Selection.atStart(doc) : _state2.Selection.atEnd(doc); } } // Workaround for ED-3507: When media node is the last element, scrollIntoView throws an error var patchedSelection = selection ? _state2.Selection.findFrom(selection.$head, -1, true) || undefined : undefined; return _state2.EditorState.create({ schema: schema, plugins: plugins, doc: doc, selection: patchedSelection }); }, [errorReporter, featureFlags, parseDoc, props.intl, props.portalProviderAPI, props.nodeViewPortalProviderAPI, props.editorProps, dispatchAnalyticsEvent, eventDispatcher, dispatch]); var initialEditorState = (0, _react.useMemo)(function () { if ((0, _coreUtils.isSSR)()) { // We don't need to create initial state in SSR, it would be done by EditorSSRRenderer, // so we can save some CPU time here. return undefined; } return createEditorState({ props: props, doc: defaultValue, // ED-4759: Don't set selection at end for full-page editor - should be at start. selectionAtStart: (0, _isFullPage.isFullPage)(nextAppearance) }); }, // This is only used for the initial state - afterwards we will have `viewRef` available for use // eslint-disable-next-line react-hooks/exhaustive-deps []); var getCurrentEditorState = (0, _react.useCallback)(function () { var _viewRef$current$stat, _viewRef$current2; return (_viewRef$current$stat = (_viewRef$current2 = viewRef.current) === null || _viewRef$current2 === void 0 ? void 0 : _viewRef$current2.state) !== null && _viewRef$current$stat !== void 0 ? _viewRef$current$stat : initialEditorState; }, [initialEditorState]); var blur = (0, _react.useCallback)(function () { if (!viewRef.current) { return; } if (viewRef.current.dom instanceof HTMLElement && viewRef.current.hasFocus()) { viewRef.current.dom.blur(); } // The selectionToDOM method uses the document selection to determine currently selected node // We need to mimic blurring this as it seems doing the above is not enough. // @ts-expect-error var sel = viewRef.current.root.getSelection(); if (sel) { sel.removeAllRanges(); } }, []); var resetEditorState = (0, _react.useCallback)(function (_ref) { var _props$editorProps$on, _props$editorProps2; var doc = _ref.doc, shouldScrollToBottom = _ref.shouldScrollToBottom; if (!viewRef.current) { return; } // We cannot currently guarantee when all the portals will have re-rendered during a reconfigure // so we blur here to stop ProseMirror from trying to apply selection to detached nodes or // nodes that haven't been re-rendered to the document yet. blur(); var newEditorState = createEditorState({ props: props, doc: doc, resetting: true, selectionAtStart: !shouldScrollToBottom }); viewRef.current.updateState(newEditorState); (_props$editorProps$on = (_props$editorProps2 = props.editorProps).onChange) === null || _props$editorProps$on === void 0 || _props$editorProps$on.call(_props$editorProps2, viewRef.current, { source: 'local', isDirtyChange: false }); }, [blur, createEditorState, props]); // Initialise phase // Using constructor hook so we setup and dispatch analytics before anything else (0, _hooks.useConstructor)(function () { var _props$intl; // This needs to be before initialising editorState because // we dispatch analytics events in plugin initialisation eventDispatcher.on(_analytics2.analyticsEventKey, handleAnalyticsEvent); eventDispatcher.on('resetEditorState', resetEditorState); dispatchAnalyticsEvent({ action: _analytics.ACTION.STARTED, actionSubject: _analytics.ACTION_SUBJECT.EDITOR, attributes: { platform: _analytics.PLATFORMS.WEB, featureFlags: featureFlags ? (0, _normalizeFeatureFlags.getEnabledFeatureFlagKeys)(featureFlags) : [], accountLocale: (_props$intl = props.intl) === null || _props$intl === void 0 ? void 0 : _props$intl.locale, browserLocale: window.navigator.language }, eventType: _analytics.EVENT_TYPE.UI }); }); (0, _react.useLayoutEffect)(function () { if ((0, _coreUtils.isSSR)()) { return; } // Transaction dispatching is already enabled by default prior to // mounting, but we reset it here, just in case the editor view // instance is ever recycled (mounted again after unmounting) with // the same key. // AND since React 18 effects may run multiple times so we need to ensure // this is reset so that transactions are still allowed. // Although storing mounted state is an anti-pattern in React, // we do so here so that we can intercept and abort asynchronous // ProseMirror transactions when a dismount is imminent. canDispatchTransactions.current = true; return function () { // We can ignore any transactions from this point onwards. // This serves to avoid potential runtime exceptions which could arise // from an async dispatched transaction after it's unmounted. canDispatchTransactions.current = false; }; }, []); // Cleanup (0, _react.useLayoutEffect)(function () { if ((0, _coreUtils.isSSR)()) { // No cleanup in SSR should happened because SSR doesn't render a real editor. return; } return function () { var focusTimeoutIdCurrent = focusTimeoutId.current; if (focusTimeoutIdCurrent) { clearTimeout(focusTimeoutIdCurrent); } if (viewRef.current) { // Destroy the state if the Editor is being unmounted var editorState = viewRef.current.state; editorState.plugins.forEach(function (plugin) { var state = plugin.getState(editorState); if (state && state.destroy) { state.destroy(); } }); } eventDispatcher.destroy(); // this.view will be destroyed when React unmounts in handleEditorViewRef }; }, [eventDispatcher]); // Bumped after `reconfigureState` so the render prop re-reads the // in-place-mutated `config.current` (contentComponents / toolbar // components from the rebuilt preset). var _useState = (0, _react.useState)(0), _useState2 = (0, _slicedToArray2.default)(_useState, 2), bumpConfigVersion = _useState2[1]; // Preset reference last processed by reconfigureState. Used to skip the // destructive work (plugin filter, schema rebuild) when reconfigure is // called with the same preset. var lastProcessedPresetRef = (0, _react.useRef)(null); var reconfigureState = (0, _react.useCallback)(function (props) { if (!viewRef.current) { return; } // We cannot currently guarantee when all the portals will have re-rendered during a reconfigure // so we blur here to stop ProseMirror from trying to apply selection to detached nodes or // nodes that haven't been re-rendered to the document yet. blur(); // Snapshot plugin names registered before createPluginsList runs, so // we can tell which plugins are newly added by the new preset vs. // which ones already coexisted with the current schema. var previousPluginNames = new Set(pluginInjectionAPI.current.getRegisteredPluginNames()); var editorPlugins = (0, _createPluginsList.default)(props.preset, 'allowBlockType' in props.editorProps ? props.editorProps : {}, pluginInjectionAPI.current); // Capture once, before either downstream block updates the ref — // both the filter and the schema rebuild are destructive and only // want to run when the preset has actually changed. var presetChanged = lastProcessedPresetRef.current !== props.preset; // Build a candidate config from the *unfiltered* plugin list so we can // decide whether the schema rebuild path will run. Both the rebuild // decision and the drop-filter decision below depend on this answer, // so it has to be computed up-front. var buildConfig = function buildConfig(plugins) { var c = (0, _createEditor.processPluginsList)(plugins); if ((0, _expValEquals.expValEquals)('platform_editor_appearance_shared_state', 'isEnabled', true)) { var _c$pmPlugins; (_c$pmPlugins = c.pmPlugins).push.apply(_c$pmPlugins, (0, _toConsumableArray2.default)(pluginInjectionAPI.current.getInternalPMPlugins())); } return c; }; var nextConfig = buildConfig(editorPlugins); // `state.reconfigure` preserves the original schema, so a preset // toggle that should change schema (markdown↔rich) needs a fresh // `EditorState`. Resets all plugin state including undo history. // // Compare schema *shape* (node + mark name sets) rather than preset // identity: consumers commonly recreate the preset object on every // parent re-render, and a destructive rebuild on a no-op identity // change tears down all plugin state (e.g. unmounts the AI palette). var shouldRebuildSchema = presetChanged && schemaShapeChanged(viewRef.current.state.schema, nextConfig) && (0, _expValEqualsNoExposure.expValEqualsNoExposure)('cc-markdown-mode', 'isEnabled', true); // `state.reconfigure` keeps the original schema, so switching presets // can leave the editor inconsistent in two ways: // 1. The new preset may add plugins that reference schema nodes or // marks the original schema doesn't have. // 2. Plugins registered by a previous preset can linger in the // injection API even when the new preset doesn't re-register // them, so listeners still fire against a state that no longer // has their pmPlugin. // // When the schema is being rebuilt below, the new schema is built // from the *unfiltered* plugin list — so dropping plugins whose // nodes/marks the OLD schema lacks would wrongly remove the very // plugins the rebuild is meant to admit. Skip the drop step in that // case (purpose 1) but always reconcile the injection API // (purpose 2). When NOT rebuilding, run both — even under the // `cc-markdown-mode` experiment, otherwise no-op preset identity // changes would silently leave a broken plugin/schema mismatch. if (presetChanged && (0, _platformFeatureFlags.fg)('platform_editor_reconfigure_filter_plugins')) { var dropped = []; if (!shouldRebuildSchema) { var _result = (0, _filterPluginsForReconfigure.filterPluginsForReconfigure)(editorPlugins, viewRef.current.state.schema, previousPluginNames); if (_result.dropped.length > 0) { editorPlugins = _result.kept; // Plugin list changed — rebuild candidate config to match. nextConfig = buildConfig(editorPlugins); } dropped = _result.dropped; } var keptPluginNames = new Set(editorPlugins.map(function (p) { return p === null || p === void 0 ? void 0 : p.name; }).filter(function (n) { return Boolean(n); })); var evictedFromApi = pluginInjectionAPI.current.retainPlugins(keptPluginNames); if (dropped.length > 0 || evictedFromApi.length > 0) { // eslint-disable-next-line no-console console.warn('[reconfigureState] Cleanup summary:', { dropped: dropped, evictedFromApi: evictedFromApi }); } } config.current = nextConfig; var state = viewRef.current.state; var newState; if (shouldRebuildSchema) { var newSchema = (0, _createSchema.createSchema)(config.current); var newDoc; try { newDoc = _model.Node.fromJSON(newSchema, state.doc.toJSON()); } catch (e) { // eslint-disable-next-line no-console console.error('[reconfigureState] Failed to migrate doc to new schema; resetting to empty doc', e); var empty = newSchema.topNodeType.createAndFill(); if (!empty) { throw new Error('reconfigureState: doc migration failed and new schema cannot create an empty top node'); } newDoc = empty; } var newSelection; try { newSelection = _state2.Selection.fromJSON(newDoc, state.selection.toJSON()); } catch (_unused) { // Old selection's positions / node types may not map onto the new schema. newSelection = _state2.Selection.atStart(newDoc); } var plugins = (0, _createEditor.createPMPlugins)({ schema: newSchema, dispatch: dispatch, errorReporter: errorReporter, editorConfig: config.current, eventDispatcher: eventDispatcher, providerFactory: props.providerFactory, portalProviderAPI: props.portalProviderAPI, nodeViewPortalProviderAPI: props.nodeViewPortalProviderAPI, dispatchAnalyticsEvent: dispatchAnalyticsEvent, featureFlags: featureFlags, getIntl: function getIntl() { return props.intl; }, onEditorStateUpdated: pluginInjectionAPI.current.onEditorViewUpdated }); newState = _state2.EditorState.create({ schema: newSchema, doc: newDoc, selection: newSelection, plugins: plugins }); } else { var _plugins = (0, _createEditor.createPMPlugins)({ schema: state.schema, dispatch: dispatch, errorReporter: errorReporter, editorConfig: config.current, eventDispatcher: eventDispatcher, providerFactory: props.providerFactory, portalProviderAPI: props.portalProviderAPI, nodeViewPortalProviderAPI: props.nodeViewPortalProviderAPI, dispatchAnalyticsEvent: dispatchAnalyticsEvent, featureFlags: featureFlags, getIntl: function getIntl() { return props.intl; }, onEditorStateUpdated: pluginInjectionAPI.current.onEditorViewUpdated }); newState = state.reconfigure({ plugins: _plugins }); } if (presetChanged) { lastProcessedPresetRef.current = props.preset; } // need to update the state first so when the view builds the nodeviews it is // using the latest plugins viewRef.current.updateState(newState); var result = viewRef.current.update(_objectSpread(_objectSpread({}, viewRef.current.props), {}, { state: newState })); // The new collab-edit plugin instance starts with `isReady=false`. // The rebind path in editor-plugin-collab-edit's initialize.ts is // gated on `provider.getInitPayload`, which the Confluence NCS // provider does not implement, so the placeholder spinner would // never clear. Re-seeding here is safe: the prior state must have // had `isReady=true` for the user to have triggered the toggle. // // Must run AFTER `view.update({ state: newState })`: that call resets // the view's state to the captured `newState` reference, so a // dispatch placed before it would advance `view.state` to a value // that `update` then silently overwrites — discarding the meta and // leaving `isReady=false`. if (shouldRebuildSchema) { // `state.collabEditPlugin$` is the property PM derives from the // collab plugin's PluginKey; cast through `unknown` to read it. var collabState = viewRef.current.state.collabEditPlugin$; if (collabState && collabState.isReady !== true) { viewRef.current.dispatch(viewRef.current.state.tr.setMeta('collabInitialised', true)); } } // EDITOR-6702: gated until we have a broader gate; reconfigure is a // low-level path so use NoExposure. if ((0, _expValEqualsNoExposure.expValEqualsNoExposure)('cc-markdown-mode', 'isEnabled', true)) { // Force a render so PluginSlot picks up the new preset's content // components against the new state. bumpConfigVersion(function (v) { return v + 1; }); } return result; }, [blur, dispatchAnalyticsEvent, eventDispatcher, dispatch, errorReporter, featureFlags]); var onEditorViewUpdated = (0, _react.useCallback)(function (_ref2) { var _config$current; var originalTransaction = _ref2.originalTransaction, transactions = _ref2.transactions, oldEditorState = _ref2.oldEditorState, newEditorState = _ref2.newEditorState; (_config$current = config.current) === null || _config$current === void 0 || _config$current.onEditorViewStateUpdatedCallbacks.forEach(function (entry) { entry.callback({ originalTransaction: originalTransaction, transactions: transactions, oldEditorState: oldEditorState, newEditorState: newEditorState }); }); }, []); var _dispatchTransaction = (0, _useDispatchTransaction.useDispatchTransaction)({ onChange: props.editorProps.onChange, dispatchAnalyticsEvent: dispatchAnalyticsEvent, onEditorViewUpdated: onEditorViewUpdated, isRemoteReplaceDocumentTransaction: (_pluginInjectionAPI$c = pluginInjectionAPI.current.api()) === null || _pluginInjectionAPI$c === void 0 || (_pluginInjectionAPI$c = _pluginInjectionAPI$c.collabEdit) === null || _pluginInjectionAPI$c === void 0 || (_pluginInjectionAPI$c = _pluginInjectionAPI$c.actions) === null || _pluginInjectionAPI$c === void 0 ? void 0 : _pluginInjectionAPI$c.isRemoteReplaceDocumentTransaction }); // Ignored via go/ees007 // eslint-disable-next-line @atlaskit/editor/enforce-todo-comment-format // TODO: Remove these when we deprecate these props from editor-props - smartLinks is unfortunately still used in some places, we can sidestep this problem if we move everyone across to ComposableEditor and deprecate Editor var UNSAFE_cards = props.editorProps.UNSAFE_cards; var smartLinks = props.editorProps.smartLinks; // Temporary to replace provider factory while migration to `ComposableEditor` occurs (0, _useProviders.useProviders)({ editorApi: pluginInjectionAPI.current.api(), contextIdentifierProvider: props.editorProps.contextIdentifierProvider, mediaProvider: (_media = props.editorProps.media) === null || _media === void 0 ? void 0 : _media.provider, mentionProvider: props.editorProps.mentionProvider, cardProvider: ((_linking = props.editorProps.linking) === null || _linking === void 0 || (_linking = _linking.smartLinks) === null || _linking === void 0 ? void 0 : _linking.provider) || smartLinks && smartLinks.provider || UNSAFE_cards && UNSAFE_cards.provider, emojiProvider: props.editorProps.emojiProvider, autoformattingProvider: props.editorProps.autoformattingProvider, taskDecisionProvider: props.editorProps.taskDecisionProvider }); var getDirectEditorProps = (0, _react.useCallback)(function (state) { var stateToUse = state !== null && state !== void 0 ? state : getCurrentEditorState(); if (!stateToUse) { // This should not be happened, because initialState is only inavailable in SSR, // but in SSR this function should never be called. // In SSR we should use EditorSSRRenderer instead usual ProseMirror editor. throw new Error('No editor state found'); } return { state: stateToUse, dispatchTransaction: function dispatchTransaction(tr) { // Block stale transactions: // Prevent runtime exceptions from async transactions that would attempt to // update the DOM after React has unmounted the Editor. if (canDispatchTransactions.current) { _dispatchTransaction(viewRef.current, tr); } }, // Disables the contentEditable attribute of the editor if the editor is disabled editable: function editable(_state) { return !disabled; }, attributes: { 'data-gramm': 'false' } }; }, [_dispatchTransaction, disabled, getCurrentEditorState]); var createEditorView = (0, _react.useCallback)(function (node) { // Creates the editor-view from this.editorState. If an editor has been mounted // previously, this will contain the previous state of the editor. var view = new _view.EditorView({ mount: node }, getDirectEditorProps()); viewRef.current = view; (0, _measureRender.measureRender)(_measureEnum.default.PROSEMIRROR_RENDERED, function (_ref3) { var duration = _ref3.duration, startTime = _ref3.startTime, distortedDuration = _ref3.distortedDuration; var proseMirrorRenderedSeverity = (0, _analytics2.getAnalyticsEventSeverity)(duration, _consts.PROSEMIRROR_RENDERED_NORMAL_SEVERITY_THRESHOLD, _consts.PROSEMIRROR_RENDERED_DEGRADED_SEVERITY_THRESHOLD); if (viewRef.current) { var _nodesAndExtensionKey, _pluginInjectionAPI$c2; var nodesAndExtensionKeys = (0, _expValEquals.expValEquals)('platform_editor_prosemirror_rendered_data', 'isEnabled', true) ? (0, _getNodesCountWithExtensionKeys.getNodesCountWithExtensionKeys)(viewRef.current.state.doc) : undefined; var nodes = (_nodesAndExtensionKey = nodesAndExtensionKeys === null || nodesAndExtensionKeys === void 0 ? void 0 : nodesAndExtensionKeys.nodes) !== null && _nodesAndExtensionKey !== void 0 ? _nodesAndExtensionKey : (0, _getNodesCount.getNodesCount)(viewRef.current.state.doc); var ttfb = (0, _navigation.getResponseEndTime)(); var requestToResponseTime = (0, _navigation.getRequestToResponseTime)(); var contextIdentifier = (_pluginInjectionAPI$c2 = pluginInjectionAPI.current.api().base) === null || _pluginInjectionAPI$c2 === void 0 ? void 0 : _pluginInjectionAPI$c2.sharedState.currentState(); var nodesInViewport = (0, _getNodesVisibleInViewport.getNodesVisibleInViewport)(viewRef.current.dom); var nodeSize = viewRef.current.state.doc.nodeSize; var _ref4 = (0, _expValEquals.expValEquals)('cc_editor_insm_doc_size_stats', 'isEnabled', true) ? { totalNodes: Object.values(nodes).reduce(function (acc, curr) { return acc + curr; }, 0), // Computed on client for dimension bucketing in Statsig nodeSizeBucket: function () { switch (true) { case nodeSize < 10000: return '<10000'; case nodeSize < 20000: return '<20000'; case nodeSize < 30000: return '<30000'; case nodeSize < 40000: return '<40000'; case nodeSize < 50000: return '<50000'; default: return '50000+'; } }() } : {}, totalNodes = _ref4.totalNodes, nodeSizeBucket = _ref4.nodeSizeBucket; if ((0, _expValEquals.expValEquals)('platform_editor_prosemirror_rendered_data', 'isEnabled', true)) { var _nodesAndExtensionKey2; var extensionKeys = (_nodesAndExtensionKey2 = nodesAndExtensionKeys === null || nodesAndExtensionKeys === void 0 ? void 0 : nodesAndExtensionKeys.extensionKeys) !== null && _nodesAndExtensionKey2 !== void 0 ? _nodesAndExtensionKey2 : {}; var interaction = (0, _interactionMetrics.getActiveInteraction)(); var pageLoadType = interaction === null || interaction === void 0 ? void 0 : interaction.type; var pageType = interaction === null || interaction === void 0 ? void 0 : interaction.routeName; var timings = function () { if (requestToResponseTime === undefined && bootStartTime === undefined) { return undefined; } var timingValues = {}; if (requestToResponseTime !== undefined) { timingValues['requestStart->responseEnd'] = Math.round(requestToResponseTime); } if (bootStartTime !== undefined) { timingValues.bootToRender = Math.round(startTime - bootStartTime); } return timingValues; }(); var attributes = { duration: duration, startTime: startTime, nodes: nodes, nodesInViewport: nodesInViewport, nodeSize: nodeSize, nodeSizeBucket: nodeSizeBucket, totalNodes: totalNodes, ttfb: ttfb, severity: proseMirrorRenderedSeverity, objectId: contextIdentifier === null || contextIdentifier === void 0 ? void 0 : contextIdentifier.objectId, distortedDuration: distortedDuration, pageLoadType: pageLoadType, pageType: pageType, timings: timings, extensionKeys: extensionKeys, ufoInteractionId: (0, _interactionIdContext.getInteractionId)().current }; dispatchAnalyticsEvent({ action: _analytics.ACTION.PROSEMIRROR_RENDERED, actionSubject: _analytics.ACTION_SUBJECT.EDITOR, attributes: attributes, eventType: _analytics.EVENT_TYPE.OPERATIONAL }); } else { var _attributes = { duration: duration, startTime: startTime, nodes: nodes, nodesInViewport: nodesInViewport, nodeSize: nodeSize, nodeSizeBucket: nodeSizeBucket, totalNodes: totalNodes, ttfb: ttfb, severity: proseMirrorRenderedSeverity, objectId: contextIdentifier === null || contextIdentifier === void 0 ? void 0 : contextIdentifier.objectId, distortedDuration: distortedDuration }; dispatchAnalyticsEvent({ action: _analytics.ACTION.PROSEMIRROR_RENDERED, actionSubject: _analytics.ACTION_SUBJECT.EDITOR, attributes: _attributes, eventType: _analytics.EVENT_TYPE.OPERATIONAL }); } } }); pluginInjectionAPI.current.onEditorViewUpdated({ newEditorState: viewRef.current.state, oldEditorState: undefined }); return view; }, [getDirectEditorProps, dispatchAnalyticsEvent]); var _useState3 = (0, _react.useState)(undefined), _useState4 = (0, _slicedToArray2.default)(_useState3, 2), editorView = _useState4[0], setEditorView = _useState4[1]; // Detects if the editor is nested inside an extension - ie. it is a Legacy Content Extension (LCE) var isNestedEditor = (0, _react.useRef)(null); var isNestedEditorCalculated = (0, _react.useRef)(false); if (editorRef.current !== null && !isNestedEditorCalculated.current) { var _editorRef$current; isNestedEditor.current = !!((_editorRef$current = editorRef.current) !== null && _editorRef$current !== void 0 && _editorRef$current.closest('.extension-editable-area')); isNestedEditorCalculated.current = true; } var originalScrollToRestore = _react.default.useRef(!isNestedEditor.current && (0, _isFullPage.isFullPage)(props.editorProps.appearance) ? (_document$querySelect = document.querySelector('[data-editor-scroll-container]')) === null || _document$querySelect === void 0 ? void 0 : _document$querySelect.scrollTop : undefined); var mitigateScrollJump = // The feature gate here is being used to avoid potential bugs with the scroll restoration code // moving it to the end of the expression negates the point of the feature gate // eslint-disable-next-line @atlaskit/platform/no-preconditioning (0, _isFullPage.isFullPage)(props.editorProps.appearance) && originalScrollToRestore.current && originalScrollToRestore.current !== 0; (0, _react.useLayoutEffect)(function () { var _editorView$props$edi, _editorView$props; if ((0, _coreUtils.isSSR)()) { // We don't need to focus anything in SSR. return; } if (shouldFocus && editorView !== null && editorView !== void 0 && (_editorView$props$edi = (_editorView$props = editorView.props).editable) !== null && _editorView$props$edi !== void 0 && _editorView$props$edi.call(_editorView$props, editorView.state)) { if (!mitigateScrollJump) { var liveDocWithContent = (__livePage || (0, _expValEquals.expValEquals)('platform_editor_no_cursor_on_edit_page_init', 'isEnabled', true)) && !(0, _document.isEmptyDocument)(editorView.state.doc); if (!liveDocWithContent) { focusTimeoutId.current = (0, _handleEditorFocus.handleEditorFocus)(editorView); } if ((0, _isChromeless.isChromeless)(props.editorProps.appearance)) { focusTimeoutId.current = (0, _handleEditorFocus.handleEditorFocus)(editorView); } if ((0, _expValEquals.expValEquals)('platform_editor_no_cursor_on_edit_page_init', 'isEnabled', true) && (0, _platformFeatureFlags.fg)('cc_editor_focus_before_editor_on_load')) { if (!disabled && shouldFocus && !(0, _document.isEmptyDocument)(editorView.state.doc)) { (0, _focusEditorElement.focusEditorElement)(editorId.current); } } } } }, [editorView, shouldFocus, __livePage, mitigateScrollJump, disabled, props.editorProps.appearance]); var scrollElement = _react.default.useRef(); var possibleListeners = _react.default.useRef([]); (0, _react.useEffect)(function () { if ((0, _coreUtils.isSSR)()) { // No event listeners should be attached to scroll element in SSR. return; } return function () { if (scrollElement.current) { // eslint-disable-next-line react-hooks/exhaustive-deps var _iterator2 = _createForOfIteratorHelper(possibleListeners.current), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var _scrollElement$curren; var possibleListener = _step2.value; // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners (_scrollElement$curren = scrollElement.current) === null || _scrollElement$curren === void 0 || _scrollElement$curren.removeEventListener.apply(_scrollElement$curren, (0, _toConsumableArray2.default)(possibleListener)); } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } } scrollElement.current = null; }; }, []); var handleEditorViewRef = (0, _react.useCallback)(function (node) { if (node) { // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage scrollElement.current = document.querySelector('[data-editor-scroll-container]'); var cleanupListeners = function cleanupListeners() { // eslint-disable-next-line react-hooks/exhaustive-deps var _iterator3 = _createForOfIteratorHelper(possibleListeners.current), _step3; try { for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { var _scrollElement$curren2; var possibleListener = _step3.value; // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners (_scrollElement$curren2 = scrollElement.current) === null || _scrollElement$curren2 === void 0 || _scrollElement$curren2.removeEventListener.apply(_scrollElement$curren2, (0, _toConsumableArray2.default)(possibleListener)); } } catch (err) { _iterator3.e(err); } finally { _iterator3.f(); } }; if (scrollElement.current) { var wheelAbortHandler = function wheelAbortHandler() { var activeInteraction = (0, _interactionMetrics.getActiveInteraction)(); if (activeInteraction && ['edit-page', 'live-edit'].includes(activeInteraction.ufoName)) { (0, _interactionMetrics.abortAll)('new_interaction', "wheel-on-editor-element"); } cleanupListeners(); }; // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners scrollElement.current.addEventListener('wheel', wheelAbortHandler); possibleListeners.current.push(['wheel', wheelAbortHandler]); var scrollAbortHandler = function scrollAbortHandler() { var activeInteraction = (0, _interactionMetrics.getActiveInteraction)(); if (activeInteraction && ['edit-page', 'live-edit'].includes(activeInteraction.ufoName)) { (0, _interactionMetrics.abortAll)('new_interaction', "scroll-on-editor-element"); } cleanupListeners(); }; // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners scrollElement.current.addEventListener('scroll', scrollAbortHandler); possibleListeners.current.push(['scroll', scrollAbortHandler]); } } if (!viewRef.current && node) { (0, _nodeVisibility.nodeVisibilityManager)(node).initialiseNodeObserver(); var view = createEditorView(node); if (mitigateScrollJump) { var _scrollElement = document.querySelector('[data-editor-scroll-container]'); _scrollElement === null || _scrollElement === void 0 || _scrollElement.scrollTo({ top: originalScrollToRestore.current, behavior: 'instant' }); } onEditorCreated({ view: view, config: config.current, eventDispatcher: eventDispatcher, transformer: contentTransformer.current }); _react.default.startTransition(function () { // Force React to re-render so consumers get a reference to the editor view setEditorView(view); }); } else if (viewRef.current && !node) { // When the appearance is changed, React will call handleEditorViewRef with node === null // to destroy the old EditorView, before calling this method again with node === div to // create the new EditorView onEditorDestroyed({ view: viewRef.current, config: config.current, eventDispatcher: eventDispatcher, transformer: contentTransformer.current }); var wasAnalyticsDisconnected = !eventDispatcher.has(_analytics2.analyticsEventKey, handleAnalyticsEvent); // If we disabled event listening for some reason we should re-enable it temporarily while we destroy // the view for any analytics that occur there. if (wasAnalyticsDisconnected) { eventDispatcher.on(_analytics2.analyticsEventKey, handleAnalyticsEvent); viewRef.current.destroy(); // Destroys the dom node & all node views eventDispatcher.off(_analytics2.analyticsEventKey, handleAnalyticsEvent); } else { viewRef.current.destroy(); // Destroys the dom node & all node views } (0, _nodeVisibility.nodeVisibilityManager)(viewRef.current.dom).disconnect(); viewRef.current = undefined; } }, [createEditorView, onEditorCreated, eventDispatcher, onEditorDestroyed, handleAnalyticsEvent, mitigateScrollJump]); var isPageAppearance = (0, _isFullPage.isFul