@atlaskit/editor-plugin-synced-block
Version:
SyncedBlock plugin for @atlaskit/editor-core
205 lines (201 loc) • 10.2 kB
JavaScript
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _createClass from "@babel/runtime/helpers/createClass";
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
import React, { useEffect, useState } from 'react';
import { ACTION_SUBJECT, ACTION_SUBJECT_ID } from '@atlaskit/editor-common/analytics';
import { ErrorBoundary } from '@atlaskit/editor-common/error-boundary';
import { BodiedSyncBlockSharedCssClassName } from '@atlaskit/editor-common/sync-block';
import { isOfflineMode } from '@atlaskit/editor-plugin-connectivity';
import { DOMSerializer } from '@atlaskit/editor-prosemirror/model';
import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
import { getUnpublishedSourceType } from '../ui/getUnpublishedSourceType';
import { SyncBlockLabel } from '../ui/SyncBlockLabel';
import { isEmptySourceSyncBlock } from './isEmptySourceSyncBlock';
var toDOM = function toDOM(node) {
return ['div', {
class: "".concat(BodiedSyncBlockSharedCssClassName.prefix, " bodiedSyncBlockView-content-wrap"),
localid: node.attrs.localId,
resourceid: node.attrs.resourceId
}, ['div', {
class: BodiedSyncBlockSharedCssClassName.content,
contenteditable: 'true'
}, 0]];
};
export var SourceSyncBlockLabel = function SourceSyncBlockLabel(_ref) {
var localId = _ref.localId,
resourceId = _ref.resourceId,
sourceManager = _ref.sourceManager;
var isSamePageSyncEnabled = isExperimentEnabled('editor-synced-block-same-page-sync');
var _useState = useState(function () {
var _sourceManager$getLoc;
return isSamePageSyncEnabled && (sourceManager === null || sourceManager === void 0 || (_sourceManager$getLoc = sourceManager.getLocalSourceSnapshot(resourceId)) === null || _sourceManager$getLoc === void 0 ? void 0 : _sourceManager$getLoc.status) === 'unpublished';
}),
_useState2 = _slicedToArray(_useState, 2),
isUnpublished = _useState2[0],
setIsUnpublished = _useState2[1];
var _useState3 = useState(),
_useState4 = _slicedToArray(_useState3, 2),
sourceInfo = _useState4[0],
setSourceInfo = _useState4[1];
useEffect(function () {
if (!isSamePageSyncEnabled) {
return;
}
var isMounted = true;
void (sourceManager === null || sourceManager === void 0 ? void 0 : sourceManager.getSyncBlockSourceInfo(localId).then(function (nextSourceInfo) {
if (isMounted) {
setSourceInfo(nextSourceInfo);
}
}));
var unsubscribe = sourceManager === null || sourceManager === void 0 ? void 0 : sourceManager.subscribeToLocalSource(resourceId, function (snapshot) {
return setIsUnpublished((snapshot === null || snapshot === void 0 ? void 0 : snapshot.status) === 'unpublished');
});
return function () {
isMounted = false;
unsubscribe === null || unsubscribe === void 0 || unsubscribe();
};
}, [isSamePageSyncEnabled, localId, resourceId, sourceManager]);
return /*#__PURE__*/React.createElement(SyncBlockLabel, {
isSource: true,
localId: localId,
unpublishedInfo: isSamePageSyncEnabled && isUnpublished ? {
sourceType: getUnpublishedSourceType({
sourceAri: sourceInfo === null || sourceInfo === void 0 ? void 0 : sourceInfo.sourceAri,
sourceProduct: sourceInfo === null || sourceInfo === void 0 ? void 0 : sourceInfo.productType
}),
variant: 'source'
} : undefined
});
};
export var BodiedSyncBlock = /*#__PURE__*/function () {
function BodiedSyncBlock(node, view, getPos, api, nodeViewPortalProviderAPI, sourceManager) {
var _this = this;
_classCallCheck(this, BodiedSyncBlock);
this.node = node;
this.view = view;
this.getPos = getPos;
this.api = api;
this.nodeViewPortalProviderAPI = nodeViewPortalProviderAPI;
// eslint-disable-next-line @atlaskit/platform/no-direct-document-usage -- NodeView serialization must target active runtime document
var _DOMSerializer$render = DOMSerializer.renderSpec(document, toDOM(this.node)),
dom = _DOMSerializer$render.dom,
contentDOM = _DOMSerializer$render.contentDOM;
// eslint-disable-next-line @atlaskit/editor/no-as-casting
this.dom = dom;
// eslint-disable-next-line @atlaskit/editor/no-as-casting
this.contentDOM = contentDOM;
// Render the label into a separate container so portal implementations that
// write directly into the target do not clobber contentDOM. This also covers
// SSR, where the portal's renderToStaticMarkup + innerHTML would clobber it.
// eslint-disable-next-line @atlaskit/platform/no-direct-document-usage -- NodeView DOM must be created against active runtime document
var labelContainer = document.createElement('div');
this.dom.appendChild(labelContainer);
this.labelKey = crypto.randomUUID();
this.nodeViewPortalProviderAPI.render(function () {
var _this$api;
return /*#__PURE__*/React.createElement(ErrorBoundary, {
component: ACTION_SUBJECT.SYNCED_BLOCK,
componentId: ACTION_SUBJECT_ID.SYNCED_BLOCK_LABEL,
dispatchAnalyticsEvent: (_this$api = _this.api) === null || _this$api === void 0 || (_this$api = _this$api.analytics) === null || _this$api === void 0 ? void 0 : _this$api.actions.fireAnalyticsEvent,
fallbackComponent: null
}, /*#__PURE__*/React.createElement(SourceSyncBlockLabel, {
localId: node.attrs.localId,
resourceId: node.attrs.resourceId,
sourceManager: sourceManager
}));
}, labelContainer, this.labelKey);
this.updateContentEditable({});
this.updateEmptyClass();
this.handleConnectivityModeChange();
this.handleViewModeChange();
// Cache is populated in state.init() and updated in appendTransaction,
// so no additional updateSyncBlockData call is needed here.
}
return _createClass(BodiedSyncBlock, [{
key: "updateContentEditable",
value: function updateContentEditable(_ref2) {
var _this$api2, _this$api3;
var nextConnectivityMode = _ref2.nextConnectivityMode,
nextViewMode = _ref2.nextViewMode;
var connectivityMode = nextConnectivityMode !== null && nextConnectivityMode !== void 0 ? nextConnectivityMode : (_this$api2 = this.api) === null || _this$api2 === void 0 || (_this$api2 = _this$api2.connectivity) === null || _this$api2 === void 0 || (_this$api2 = _this$api2.sharedState) === null || _this$api2 === void 0 || (_this$api2 = _this$api2.currentState()) === null || _this$api2 === void 0 ? void 0 : _this$api2.mode;
var viewMode = nextViewMode !== null && nextViewMode !== void 0 ? nextViewMode : (_this$api3 = this.api) === null || _this$api3 === void 0 || (_this$api3 = _this$api3.editorViewMode) === null || _this$api3 === void 0 || (_this$api3 = _this$api3.sharedState) === null || _this$api3 === void 0 || (_this$api3 = _this$api3.currentState()) === null || _this$api3 === void 0 ? void 0 : _this$api3.mode;
var isOnline = !isOfflineMode(connectivityMode);
var isEditMode = viewMode !== 'view';
var shouldBeEditable = isOnline && isEditMode;
this.contentDOM.setAttribute('contenteditable', shouldBeEditable ? 'true' : 'false');
}
}, {
key: "handleConnectivityModeChange",
value: function handleConnectivityModeChange() {
var _this$api4,
_this2 = this;
if ((_this$api4 = this.api) !== null && _this$api4 !== void 0 && _this$api4.connectivity) {
this.cleanupConnectivityModeListener = this.api.connectivity.sharedState.onChange(function (_ref3) {
var nextSharedState = _ref3.nextSharedState;
_this2.updateContentEditable({
nextConnectivityMode: nextSharedState.mode
});
});
}
}
}, {
key: "handleViewModeChange",
value: function handleViewModeChange() {
var _this$api5,
_this3 = this;
if ((_this$api5 = this.api) !== null && _this$api5 !== void 0 && _this$api5.editorViewMode) {
this.cleanupViewModeListener = this.api.editorViewMode.sharedState.onChange(function (_ref4) {
var nextSharedState = _ref4.nextSharedState;
_this3.updateContentEditable({
nextViewMode: nextSharedState === null || nextSharedState === void 0 ? void 0 : nextSharedState.mode
});
});
}
}
/**
* Marks the block as empty so the source placeholder can be shown purely from document state.
* See `isEmptySourceSyncBlock` for why the DOM is not used to decide this.
*/
}, {
key: "updateEmptyClass",
value: function updateEmptyClass() {
this.dom.classList.toggle(BodiedSyncBlockSharedCssClassName.empty, isEmptySourceSyncBlock(this.node));
}
}, {
key: "update",
value: function update(node) {
if (this.node.type !== node.type) {
return false;
}
// Cache updates are handled in appendTransaction where we can
// filter out non-user changes (remote collab, table auto-scale, etc.)
this.node = node;
this.updateEmptyClass();
return true;
}
}, {
key: "ignoreMutation",
value: function ignoreMutation(mutation) {
if (mutation.type === 'selection') {
return false;
}
return true;
}
}, {
key: "destroy",
value: function destroy() {
var _this$cleanupConnecti, _this$cleanupViewMode;
(_this$cleanupConnecti = this.cleanupConnectivityModeListener) === null || _this$cleanupConnecti === void 0 || _this$cleanupConnecti.call(this);
(_this$cleanupViewMode = this.cleanupViewModeListener) === null || _this$cleanupViewMode === void 0 || _this$cleanupViewMode.call(this);
this.nodeViewPortalProviderAPI.remove(this.labelKey);
}
}]);
}();
export var bodiedSyncBlockNodeView = function bodiedSyncBlockNodeView(props) {
var api = props.api,
nodeViewPortalProviderAPI = props.pmPluginFactoryParams.nodeViewPortalProviderAPI,
syncBlockStore = props.syncBlockStore;
return function (node, view, getPos) {
return new BodiedSyncBlock(node, view, getPos, api, nodeViewPortalProviderAPI, syncBlockStore === null || syncBlockStore === void 0 ? void 0 : syncBlockStore.sourceManager);
};
};