@atlaskit/editor-core
Version:
A package contains Atlassian editor core functionality
942 lines (917 loc) • 62.5 kB
JavaScript
"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