UNPKG

@atlaskit/editor-plugin-synced-block

Version:

SyncedBlock plugin for @atlaskit/editor-core

182 lines (178 loc) • 9.15 kB
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'; const toDOM = node => ['div', { class: `${BodiedSyncBlockSharedCssClassName.prefix} bodiedSyncBlockView-content-wrap`, localid: node.attrs.localId, resourceid: node.attrs.resourceId }, ['div', { class: BodiedSyncBlockSharedCssClassName.content, contenteditable: 'true' }, 0]]; export const SourceSyncBlockLabel = ({ localId, resourceId, sourceManager }) => { const isSamePageSyncEnabled = isExperimentEnabled('editor-synced-block-same-page-sync'); const [isUnpublished, setIsUnpublished] = useState(() => { var _sourceManager$getLoc; return isSamePageSyncEnabled && (sourceManager === null || sourceManager === void 0 ? void 0 : (_sourceManager$getLoc = sourceManager.getLocalSourceSnapshot(resourceId)) === null || _sourceManager$getLoc === void 0 ? void 0 : _sourceManager$getLoc.status) === 'unpublished'; }); const [sourceInfo, setSourceInfo] = useState(); useEffect(() => { if (!isSamePageSyncEnabled) { return; } let isMounted = true; void (sourceManager === null || sourceManager === void 0 ? void 0 : sourceManager.getSyncBlockSourceInfo(localId).then(nextSourceInfo => { if (isMounted) { setSourceInfo(nextSourceInfo); } })); const unsubscribe = sourceManager === null || sourceManager === void 0 ? void 0 : sourceManager.subscribeToLocalSource(resourceId, snapshot => setIsUnpublished((snapshot === null || snapshot === void 0 ? void 0 : snapshot.status) === 'unpublished')); return () => { isMounted = false; unsubscribe === null || unsubscribe === void 0 ? 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 class BodiedSyncBlock { constructor(node, view, getPos, api, nodeViewPortalProviderAPI, sourceManager) { 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 const { dom, contentDOM } = DOMSerializer.renderSpec(document, toDOM(this.node)); // 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 const labelContainer = document.createElement('div'); this.dom.appendChild(labelContainer); this.labelKey = crypto.randomUUID(); this.nodeViewPortalProviderAPI.render(() => { var _this$api, _this$api$analytics; 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 ? void 0 : (_this$api$analytics = _this$api.analytics) === null || _this$api$analytics === void 0 ? void 0 : _this$api$analytics.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. } updateContentEditable({ nextConnectivityMode, nextViewMode }) { var _this$api2, _this$api2$connectivi, _this$api2$connectivi2, _this$api2$connectivi3, _this$api3, _this$api3$editorView, _this$api3$editorView2, _this$api3$editorView3; const connectivityMode = nextConnectivityMode !== null && nextConnectivityMode !== void 0 ? nextConnectivityMode : (_this$api2 = this.api) === null || _this$api2 === void 0 ? void 0 : (_this$api2$connectivi = _this$api2.connectivity) === null || _this$api2$connectivi === void 0 ? void 0 : (_this$api2$connectivi2 = _this$api2$connectivi.sharedState) === null || _this$api2$connectivi2 === void 0 ? void 0 : (_this$api2$connectivi3 = _this$api2$connectivi2.currentState()) === null || _this$api2$connectivi3 === void 0 ? void 0 : _this$api2$connectivi3.mode; const viewMode = nextViewMode !== null && nextViewMode !== void 0 ? nextViewMode : (_this$api3 = this.api) === null || _this$api3 === void 0 ? void 0 : (_this$api3$editorView = _this$api3.editorViewMode) === null || _this$api3$editorView === void 0 ? void 0 : (_this$api3$editorView2 = _this$api3$editorView.sharedState) === null || _this$api3$editorView2 === void 0 ? void 0 : (_this$api3$editorView3 = _this$api3$editorView2.currentState()) === null || _this$api3$editorView3 === void 0 ? void 0 : _this$api3$editorView3.mode; const isOnline = !isOfflineMode(connectivityMode); const isEditMode = viewMode !== 'view'; const shouldBeEditable = isOnline && isEditMode; this.contentDOM.setAttribute('contenteditable', shouldBeEditable ? 'true' : 'false'); } handleConnectivityModeChange() { var _this$api4; if ((_this$api4 = this.api) !== null && _this$api4 !== void 0 && _this$api4.connectivity) { this.cleanupConnectivityModeListener = this.api.connectivity.sharedState.onChange(({ nextSharedState }) => { this.updateContentEditable({ nextConnectivityMode: nextSharedState.mode }); }); } } handleViewModeChange() { var _this$api5; if ((_this$api5 = this.api) !== null && _this$api5 !== void 0 && _this$api5.editorViewMode) { this.cleanupViewModeListener = this.api.editorViewMode.sharedState.onChange(({ nextSharedState }) => { this.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. */ updateEmptyClass() { this.dom.classList.toggle(BodiedSyncBlockSharedCssClassName.empty, isEmptySourceSyncBlock(this.node)); } 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; } ignoreMutation(mutation) { if (mutation.type === 'selection') { return false; } return true; } destroy() { var _this$cleanupConnecti, _this$cleanupViewMode; (_this$cleanupConnecti = this.cleanupConnectivityModeListener) === null || _this$cleanupConnecti === void 0 ? void 0 : _this$cleanupConnecti.call(this); (_this$cleanupViewMode = this.cleanupViewModeListener) === null || _this$cleanupViewMode === void 0 ? void 0 : _this$cleanupViewMode.call(this); this.nodeViewPortalProviderAPI.remove(this.labelKey); } } export const bodiedSyncBlockNodeView = props => { const { api, pmPluginFactoryParams: { nodeViewPortalProviderAPI }, syncBlockStore } = props; return (node, view, getPos) => { return new BodiedSyncBlock(node, view, getPos, api, nodeViewPortalProviderAPI, syncBlockStore === null || syncBlockStore === void 0 ? void 0 : syncBlockStore.sourceManager); }; };