@atlaskit/editor-core
Version:
A package contains Atlassian editor core functionality
351 lines (337 loc) • 13.5 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
import { createDispatch } from '@atlaskit/editor-common/event-dispatcher';
import { processRawFragmentValue, processRawValue } from '@atlaskit/editor-common/process-raw-value';
import { analyticsEventKey } from '@atlaskit/editor-common/utils/analytics';
import { Node } from '@atlaskit/editor-prosemirror/model';
import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
import { findParentNode, safeInsert } from '@atlaskit/editor-prosemirror/utils';
import { getEditorValueWithMedia } from '../utils/action';
import deprecationWarnings from '../utils/deprecation-warnings';
import { findNodePosByFragmentLocalIds } from '../utils/nodes-by-localIds';
import { isEmptyDocument } from './temp-is-empty-document';
import { findNodePosByLocalIds } from './temp-nodes-by-localids';
// eslint-disable-next-line import/order
import { toJSON } from './temp-to-json';
// eslint-disable-next-line import/order
// Please, do not copy or use this kind of code below
// @ts-ignore
const fakePluginKey = {
key: 'nativeCollabProviderPlugin$',
getState: state => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return state['nativeCollabProviderPlugin$'];
}
};
/**
* @deprecated {@link https://hello.atlassian.net/browse/ENGHEALTH-26729 Internal documentation for deprecation (no external access)} Editor actions is no longer supported and will be removed in a future version. Please use the core actions, or Plugin APIs directly instead
* @example If you were using editorActions.getValue() replace with:
const { editorApi, preset } = usePreset(...);
editorApi?.core.actions.requestDocument((doc) => {
// use doc as desired
})
* If you were using editorActions.getNodeByLocalId(localId) replace with:
const { editorApi, preset } = usePreset(...);
const extensionAPI = editorAPI?.extension?.actions?.api();
// Use nodeWithPos as desired
const nodeWithPos = extensionAPI.getNodeWithPosByLocalId(localId);
const node = nodeWithPos.node;
const nodePos = nodeWithPos.pos;
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default class EditorActions {
constructor() {
_defineProperty(this, "listeners", []);
_defineProperty(this, "dispatchAnalyticsEvent", payload => {
if (this.eventDispatcher) {
const dispatch = createDispatch(this.eventDispatcher);
dispatch(analyticsEventKey, {
payload
});
}
});
/**
* If editor is using new collab service,
* we want editor to call the collab provider to
* retrieve the final acknowledged state of the
* editor. The final acknowledged editor state
* refers to the latest state of editor with confirmed
* steps.
*/
_defineProperty(this, "getResolvedEditorState", async reason => {
const {
useNativeCollabPlugin
} = this.getFeatureFlags();
if (!this.editorView) {
throw new Error('Called getResolvedEditorState before editorView is ready');
}
if (!useNativeCollabPlugin) {
const editorValue = await this.getValue();
if (!editorValue) {
throw new Error('editorValue is undefined');
}
return {
content: editorValue,
title: null,
stepVersion: -1
};
}
const editorView = this.editorView;
await getEditorValueWithMedia(editorView);
const collabEditState = fakePluginKey.getState(editorView.state);
return collabEditState === null || collabEditState === void 0 ? void 0 : collabEditState.getFinalAcknowledgedState(reason);
});
}
static from(view, eventDispatcher, transformer) {
const editorActions = new EditorActions();
editorActions._privateRegisterEditor(view, eventDispatcher, transformer);
return editorActions;
}
//#region private
// This method needs to be public for context based helper components.
_privateGetEditorView() {
return this.editorView;
}
_privateGetEventDispatcher() {
return this.eventDispatcher;
}
getFeatureFlags() {
return {};
}
// This method needs to be public for EditorContext component.
_privateRegisterEditor(editorView, eventDispatcher, contentTransformer, getFeatureFlags = () => ({})) {
this.contentTransformer = contentTransformer;
this.eventDispatcher = eventDispatcher;
this.getFeatureFlags = getFeatureFlags;
if (!this.editorView && editorView) {
this.editorView = editorView;
this.listeners.forEach(cb => cb(editorView, eventDispatcher));
} else if (this.editorView !== editorView) {
throw new Error("Editor has already been registered! It's not allowed to re-register editor with the new Editor instance.");
}
if (this.contentTransformer) {
this.contentEncode = this.contentTransformer.encode.bind(this.contentTransformer);
}
}
// This method needs to be public for EditorContext component.
_privateUnregisterEditor() {
this.editorView = undefined;
this.contentTransformer = undefined;
this.contentEncode = undefined;
this.eventDispatcher = undefined;
this.getFeatureFlags = () => ({});
}
_privateSubscribe(cb) {
// If editor is registered and somebody is trying to add a listener,
// just call it first.
if (this.editorView && this.eventDispatcher) {
cb(this.editorView, this.eventDispatcher);
}
this.listeners.push(cb);
}
_privateUnsubscribe(cb) {
this.listeners = this.listeners.filter(c => c !== cb);
}
//#endregion
focus({
scrollIntoView
} = {
scrollIntoView: true
}) {
if (!this.editorView || this.editorView.hasFocus()) {
return false;
}
this.editorView.focus();
if (scrollIntoView !== null && scrollIntoView !== void 0 ? scrollIntoView : true) {
this.editorView.dispatch(this.editorView.state.tr.scrollIntoView());
}
return true;
}
blur() {
if (!this.editorView || !this.editorView.hasFocus()) {
return false;
}
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
this.editorView.dom.blur();
return true;
}
clear() {
if (!this.editorView) {
return false;
}
const editorView = this.editorView;
const {
state
} = editorView;
const tr = editorView.state.tr.setSelection(TextSelection.create(state.doc, 0, state.doc.nodeSize - 2)).deleteSelection();
editorView.dispatch(tr);
return true;
}
// eslint-disable-next-line @repo/internal/deprecations/deprecation-ticket-required -- Ignored via go/ED-25883
/**
* @deprecated This is deprecated and is no longer maintained.
*
* Use the `requestDocument` API from `editorAPI` (ie. `editorApi?.core?.actions.requestDocument( ... ))
* it has inbuilt throttling and is designed for use with `ComposableEditor`.
*
* Docs on its usage are available from: https://atlaskit.atlassian.com/packages/editor/editor-core
*
* WARNING: this may be called repeatedly, async with care
*/
async getValue() {
const {
editorView
} = this;
if (!editorView) {
return;
}
const doc = await getEditorValueWithMedia(editorView);
const json = toJSON(doc);
if (!this.contentEncode) {
return json;
}
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const nodeSanitized = Node.fromJSON(this.editorView.state.schema, json);
try {
return this.contentEncode(nodeSanitized);
} catch (e) {
this.dispatchAnalyticsEvent({
action: ACTION.DOCUMENT_PROCESSING_ERROR,
actionSubject: ACTION_SUBJECT.EDITOR,
eventType: EVENT_TYPE.OPERATIONAL,
attributes: {
errorMessage: `${e instanceof Error && e.name === 'NodeNestingTransformError' ? 'NodeNestingTransformError - Failed to encode one or more nested tables' : undefined}`
}
});
throw e;
}
}
// eslint-disable-next-line @repo/internal/deprecations/deprecation-ticket-required -- Ignored via go/ED-25883
/**
* @deprecated - please use `getNodeWithPosByLocalId` found in the core plugin actions instead
* @example
const { editorApi, preset } = usePreset(...);
const extensionAPI = editorAPI?.extension?.actions?.api();
// Use nodeWithPos as desired
const nodeWithPos = extensionAPI.getNodeWithPosByLocalId(localId);
const node = nodeWithPos.node;
const nodePos = nodeWithPos.pos;
*/
getNodeByLocalId(id) {
var _this$editorView;
if ((_this$editorView = this.editorView) !== null && _this$editorView !== void 0 && _this$editorView.state) {
var _this$editorView2;
const nodes = findNodePosByLocalIds((_this$editorView2 = this.editorView) === null || _this$editorView2 === void 0 ? void 0 : _this$editorView2.state, [id]);
const node = nodes.length >= 1 ? nodes[0] : undefined;
return node === null || node === void 0 ? void 0 : node.node;
}
}
getNodeByFragmentLocalId(id) {
var _this$editorView3;
if ((_this$editorView3 = this.editorView) !== null && _this$editorView3 !== void 0 && _this$editorView3.state) {
var _this$editorView4;
const nodes = findNodePosByFragmentLocalIds((_this$editorView4 = this.editorView) === null || _this$editorView4 === void 0 ? void 0 : _this$editorView4.state, [id]);
return nodes.length > 0 ? nodes[0].node : undefined;
}
}
/**
* This method will return the currently selected `Node` if the selection is a `Node`.
* Otherwise, if the selection is textual or a non-selectable `Node` within another selectable `Node`, the closest selectable parent `Node` will be returned.
*/
getSelectedNode() {
var _this$editorView5, _this$editorView5$sta;
if ((_this$editorView5 = this.editorView) !== null && _this$editorView5 !== void 0 && (_this$editorView5$sta = _this$editorView5.state) !== null && _this$editorView5$sta !== void 0 && _this$editorView5$sta.selection) {
var _findParentNode;
const {
selection
} = this.editorView.state;
if (selection instanceof NodeSelection) {
return selection.node;
}
return (_findParentNode = findParentNode(node => Boolean(node.type.spec.selectable))(selection)) === null || _findParentNode === void 0 ? void 0 : _findParentNode.node;
}
}
isDocumentEmpty() {
// Unlikely case when editorView has been destroyed before calling isDocumentEmpty,
// we treat this case as if document was empty.
if (!this.editorView) {
return true;
}
return isEmptyDocument(this.editorView.state.doc);
}
// eslint-disable-next-line @repo/internal/deprecations/deprecation-ticket-required -- Ignored via go/ED-25883
/**
* @deprecated - please use `replaceDocument` found in the core plugin actions instead
* using this will reset your Editor State which could cause some things to break (like emojis)
* @example - use the `replaceDocument` from the core plugin actions instead
* ```ts
* const { editorApi, preset } = usePreset(...);
// where you need it
editorApi?.core.actions.replaceDocument(value);
return <ComposableEditor preset={preset} ... />
*/
replaceDocument(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
rawValue, shouldScrollToBottom = true,
// eslint-disable-next-line @repo/internal/deprecations/deprecation-ticket-required -- Ignored via go/ED-25883
/** @deprecated [ED-14158] shouldAddToHistory is not being used in this function */
shouldAddToHistory = true) {
deprecationWarnings('EditorActions.replaceDocument', {
shouldAddToHistory
}, [{
property: 'shouldAddToHistory',
description: '[ED-14158] EditorActions.replaceDocument does not use the shouldAddToHistory arg',
type: 'removed'
}]);
if (!this.editorView || rawValue === undefined || rawValue === null) {
return false;
}
if (this.eventDispatcher) {
this.eventDispatcher.emit('resetEditorState', {
doc: rawValue,
shouldScrollToBottom
});
}
return true;
}
replaceSelection(rawValue, tryToReplace, position) {
if (!this.editorView) {
return false;
}
const {
state
} = this.editorView;
if (!rawValue) {
const tr = state.tr.deleteSelection().scrollIntoView();
this.editorView.dispatch(tr);
return true;
}
const {
schema
} = state;
const content = Array.isArray(rawValue) ? processRawFragmentValue(schema, rawValue, undefined, undefined, undefined, this.dispatchAnalyticsEvent) : processRawValue(schema, rawValue, undefined, undefined, undefined, this.dispatchAnalyticsEvent);
if (!content) {
return false;
}
// try to find a place in the document where to insert a node if its not allowed at the cursor position by schema
this.editorView.dispatch(safeInsert(content, position, tryToReplace)(state.tr).scrollIntoView());
return true;
}
appendText(text) {
if (!this.editorView || !text) {
return false;
}
const {
state
} = this.editorView;
const lastChild = state.doc.lastChild;
if (lastChild && lastChild.type !== state.schema.nodes.paragraph) {
return false;
}
const tr = state.tr.insertText(text).scrollIntoView();
this.editorView.dispatch(tr);
return true;
}
}