@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
166 lines (155 loc) • 6.09 kB
JavaScript
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _createClass from "@babel/runtime/helpers/createClass";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import { isEmptyDocument } from '../utils';
import { DynamicBitArray } from './dynamic-bit-array';
import { LIMITED_MODE_DEFAULT_DOC_SIZE_THRESHOLD, LIMITED_MODE_DEFAULT_NODE_COUNT_THRESHOLD } from './limited-mode-document-thresholds';
export var NodeAnchorProvider = /*#__PURE__*/function () {
function NodeAnchorProvider() {
var limitedMode = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
var emptyDoc = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
_classCallCheck(this, NodeAnchorProvider);
_defineProperty(this, "cache", new WeakMap());
_defineProperty(this, "count", BigInt(0));
_defineProperty(this, "existingPos", new DynamicBitArray());
_defineProperty(this, "limitedMode", false);
_defineProperty(this, "emptyDoc", false);
this.limitedMode = limitedMode;
this.emptyDoc = emptyDoc;
}
return _createClass(NodeAnchorProvider, [{
key: "isEmptyDoc",
value: function isEmptyDoc() {
return this.emptyDoc;
}
}, {
key: "setEmptyDoc",
value: function setEmptyDoc(isEmpty) {
this.emptyDoc = isEmpty;
}
}, {
key: "isLimitedMode",
value: function isLimitedMode() {
return this.limitedMode;
}
// We use pos to generate unique ids for each node at a specific position
// This is to ensure the same ADF will always generate the same DOM initially
}, {
key: "getOrGenerateId",
value: function getOrGenerateId(node, pos) {
if (this.limitedMode) {
return undefined;
}
if (this.cache.has(node)) {
return this.cache.get(node);
}
var idSuffix = '';
if (this.existingPos.get(pos)) {
idSuffix = "-".concat((this.count++).toString(36));
} else {
this.existingPos.set(pos, true);
}
var anchorName = "--anchor-".concat(node.type.name, "-").concat(pos).concat(idSuffix);
this.cache.set(node, anchorName);
return anchorName;
}
}, {
key: "getIdForNode",
value: function getIdForNode(node) {
if (this.limitedMode) {
return undefined;
}
return this.cache.get(node);
}
}, {
key: "setIdForNode",
value: function setIdForNode(node, id) {
if (this.limitedMode) {
return;
}
this.cache.set(node, id);
}
// After set to limited mode, we clear the cache to free up memory
// and prevent further ids from being generated
// Once in limited mode, we won't exit it
}, {
key: "setLimitedMode",
value: function setLimitedMode() {
this.limitedMode = true;
this.cache = new WeakMap();
this.existingPos = new DynamicBitArray();
this.count = BigInt(0);
}
}]);
}();
var nodeIdProviderMap = new WeakMap();
/**
* Determines whether limited mode should be enabled.
* This logic mirrors the limited mode plugin implementation, but lives here to avoid a circular dependency.
* If it changes, update the matching logic in `editor-plugin-limited-mode/src/pm-plugins/main.ts`.
*
* Limited mode is activated when ANY of the following conditions are met:
* 1. Document size exceeds `LIMITED_MODE_DEFAULT_DOC_SIZE_THRESHOLD` — checked first as O(1)
* 2. Node count exceeds `LIMITED_MODE_DEFAULT_NODE_COUNT_THRESHOLD`
* 3. Document contains a legacy-content macro (LCM)
*
* Performance optimisations:
* - Doc size is checked first (O(1)) - if it exceeds threshold, we skip traversal entirely.
* - If we find an LCM during traversal, we exit early since limited mode will be enabled.
*/
var isLimitedModeEnabled = function isLimitedModeEnabled(editorView) {
var doc = editorView.state.doc;
var nodeCountThreshold = LIMITED_MODE_DEFAULT_NODE_COUNT_THRESHOLD;
var docSizeThreshold = LIMITED_MODE_DEFAULT_DOC_SIZE_THRESHOLD;
// Early exit: doc size exceeds threshold - O(1), no traversal needed
if (doc.nodeSize > docSizeThreshold) {
return true;
}
// Single traversal for node count and LCM detection
var nodeCount = 0;
var hasLcm = false;
doc.descendants(function (node) {
var _node$attrs;
nodeCount += 1;
if (((_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.extensionKey) === 'legacy-content') {
hasLcm = true;
// Early exit: LCM found — limited mode will be enabled
return false;
}
});
// LCM condition takes precedence (if we early exited traversal, this is why)
if (hasLcm) {
return true;
}
// Check node count threshold
if (nodeCount > nodeCountThreshold) {
return true;
}
return false;
};
// Get the NodeIdProvider for a specific EditorView instance.
// This allows access to the node ids anywhere.
export var getNodeIdProvider = function getNodeIdProvider(editorView) {
if (!nodeIdProviderMap.has(editorView)) {
// if the limited mode flag is on, enable limited mode based on the threshold
// only for the first time
var limitedMode = isLimitedModeEnabled(editorView);
var isEmptyDoc = isEmptyDocument(editorView.state.doc);
var provider = new NodeAnchorProvider(limitedMode, isEmptyDoc);
nodeIdProviderMap.set(editorView, provider);
return provider;
}
var nodeIdProvider = nodeIdProviderMap.get(editorView);
// in some cases we need to re-check limited mode state
// Confluence editor can start with an empty doc and then load content later
// so we need to check first time from an empty doc to a non-empty doc
if (nodeIdProvider.isEmptyDoc() && !isEmptyDocument(editorView.state.doc)) {
// set empty doc to false regardless of limited mode state
nodeIdProvider.setEmptyDoc(false);
if (!nodeIdProvider.isLimitedMode() && isLimitedModeEnabled(editorView)) {
nodeIdProvider.setLimitedMode();
}
}
// This is based on the fact that editorView is a singleton.
return nodeIdProviderMap.get(editorView);
};