@atlaskit/editor-plugin-synced-block
Version:
SyncedBlock plugin for @atlaskit/editor-core
131 lines (129 loc) • 8.03 kB
JavaScript
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { bind } from 'bind-event-listener';
import { SyncBlockSharedCssClassName } from '@atlaskit/editor-common/sync-block';
import { SyncBlockError, useFetchSyncBlockData, useFetchSyncBlockTitle } from '@atlaskit/editor-synced-block-provider';
import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
import { getUnpublishedSourceType } from './getUnpublishedSourceType';
import { SyncBlockLabel } from './SyncBlockLabel';
const SyncBlockRendererWrapperDataId = 'sync-block-plugin-renderer-wrapper';
const SyncBlockRendererWrapperComponent = ({
syncedBlockRenderer,
syncBlockStore,
node,
resourceId,
localId,
api
}) => {
var _api$analytics, _api$analytics$action, _syncBlockFetchResult, _syncBlockFetchResult2, _syncBlockFetchResult3, _syncBlockFetchResult4, _syncBlockFetchResult5, _syncBlockFetchResult6, _syncBlockFetchResult7, _localSourceInfo$sour, _syncBlockFetchResult8, _syncBlockFetchResult9, _localSourceInfo$prod;
const isSamePageSyncEnabled = isExperimentEnabled('editor-synced-block-same-page-sync');
const syncBlockFetchResult = useFetchSyncBlockData(syncBlockStore, resourceId, localId, api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : (_api$analytics$action = _api$analytics.actions) === null || _api$analytics$action === void 0 ? void 0 : _api$analytics$action.fireAnalyticsEvent);
const title = useFetchSyncBlockTitle(syncBlockStore, node);
const contentUpdatedAt = syncBlockFetchResult === null || syncBlockFetchResult === void 0 ? void 0 : (_syncBlockFetchResult = syncBlockFetchResult.syncBlockInstance) === null || _syncBlockFetchResult === void 0 ? void 0 : (_syncBlockFetchResult2 = _syncBlockFetchResult.data) === null || _syncBlockFetchResult2 === void 0 ? void 0 : _syncBlockFetchResult2.contentUpdatedAt;
const isUnpublishedBlock = ((_syncBlockFetchResult3 = syncBlockFetchResult.syncBlockInstance) === null || _syncBlockFetchResult3 === void 0 ? void 0 : (_syncBlockFetchResult4 = _syncBlockFetchResult3.data) === null || _syncBlockFetchResult4 === void 0 ? void 0 : _syncBlockFetchResult4.status) === 'unpublished';
const localSameDocumentSource = isSamePageSyncEnabled ? (_syncBlockFetchResult5 = syncBlockFetchResult.syncBlockInstance) === null || _syncBlockFetchResult5 === void 0 ? void 0 : _syncBlockFetchResult5.localSameDocumentSource : undefined;
const localSourceBlockInstanceId = localSameDocumentSource === null || localSameDocumentSource === void 0 ? void 0 : localSameDocumentSource.sourceBlockInstanceId;
const [localSourceInfo, setLocalSourceInfo] = useState();
useEffect(() => {
var _syncBlockStore$sourc;
let isMounted = true;
if (!localSourceBlockInstanceId) {
setLocalSourceInfo(undefined);
return;
}
void ((_syncBlockStore$sourc = syncBlockStore.sourceManager) === null || _syncBlockStore$sourc === void 0 ? void 0 : _syncBlockStore$sourc.getSyncBlockSourceInfo(localSourceBlockInstanceId).then(sourceInfo => {
if (isMounted) {
setLocalSourceInfo(sourceInfo);
}
}));
return () => {
isMounted = false;
};
}, [localSourceBlockInstanceId, syncBlockStore.sourceManager]);
const isUnsyncedBlock = isUnpublishedBlock && !localSameDocumentSource || (syncBlockFetchResult === null || syncBlockFetchResult === void 0 ? void 0 : (_syncBlockFetchResult6 = syncBlockFetchResult.syncBlockInstance) === null || _syncBlockFetchResult6 === void 0 ? void 0 : (_syncBlockFetchResult7 = _syncBlockFetchResult6.error) === null || _syncBlockFetchResult7 === void 0 ? void 0 : _syncBlockFetchResult7.type) === SyncBlockError.NotFound;
// Prevent editing in the contentEditable renderer wrapper. We set
// contentEditable="true" to enable text selection (creating an editable
// island inside ProseMirror's contentEditable="false" nodeview wrapper),
// but users must not be able to type into or modify the renderer content.
const preventInput = useCallback(e => {
e.preventDefault();
}, []);
// Browsers suppress link navigation on a plain click inside a
// contentEditable region (they place the caret instead), which breaks links
// rendered in the content — e.g. the "when the page is published" link in
// the unpublished error card. Re-trigger navigation for _blank anchors
// ourselves. A native listener (rather than an onClick on the div) is used
// so we delegate link clicks without making the container itself interactive.
const rendererRef = useRef(null);
const preventReferenceEditing = isExperimentEnabled('platform_editor_blocks_patch_11');
useEffect(() => {
const container = rendererRef.current;
if (!preventReferenceEditing || !container) {
return;
}
return bind(container, {
// Block native edits without intercepting ProseMirror's node deletion.
type: 'beforeinput',
listener: event => event.preventDefault()
});
}, [preventReferenceEditing]);
useEffect(() => {
const containerEl = rendererRef.current;
if (!containerEl) {
return;
}
const unbind = bind(containerEl, {
type: 'click',
listener: e => {
// Let the browser handle modified/non-primary clicks (open-in-new-tab,
// etc.) natively so we don't open a second tab on top of it.
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return;
}
const target = e.target instanceof Element ? e.target : null;
const anchor = target === null || target === void 0 ? void 0 : target.closest('a[href]');
if (!(anchor instanceof HTMLAnchorElement) || e.defaultPrevented || anchor.target !== '_blank') {
return;
}
window.open(anchor.href, '_blank', 'noopener,noreferrer');
}
});
return unbind;
}, []);
return /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
ref: rendererRef,
"data-testid": SyncBlockRendererWrapperDataId
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop
,
className: SyncBlockSharedCssClassName.renderer,
contentEditable: true,
suppressContentEditableWarning: true
// Prevent the contentEditable div from being keyboard-focusable.
// It is only used to enable text selection, not as an input target.
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
,
tabIndex: -1,
onBeforeInput: preventInput,
onPaste: preventInput
// eslint-disable-next-line @atlaskit/design-system/no-direct-use-of-web-platform-drag-and-drop
,
onDrop: preventInput
}, syncedBlockRenderer({
syncBlockFetchResult,
api,
localId
})), /*#__PURE__*/React.createElement(SyncBlockLabel, {
isSource: false,
title: title,
contentUpdatedAt: contentUpdatedAt,
localId: localId,
isUnsyncedBlock: isUnsyncedBlock,
unpublishedInfo: localSameDocumentSource && isUnpublishedBlock ? {
sourceType: getUnpublishedSourceType({
sourceAri: (_localSourceInfo$sour = localSourceInfo === null || localSourceInfo === void 0 ? void 0 : localSourceInfo.sourceAri) !== null && _localSourceInfo$sour !== void 0 ? _localSourceInfo$sour : (_syncBlockFetchResult8 = syncBlockFetchResult.syncBlockInstance) === null || _syncBlockFetchResult8 === void 0 ? void 0 : (_syncBlockFetchResult9 = _syncBlockFetchResult8.data) === null || _syncBlockFetchResult9 === void 0 ? void 0 : _syncBlockFetchResult9.sourceAri,
sourceProduct: (_localSourceInfo$prod = localSourceInfo === null || localSourceInfo === void 0 ? void 0 : localSourceInfo.productType) !== null && _localSourceInfo$prod !== void 0 ? _localSourceInfo$prod : localSameDocumentSource.sourceProduct
}),
variant: 'local-reference'
} : undefined
}));
};
export const SyncBlockRendererWrapper = /*#__PURE__*/React.memo(SyncBlockRendererWrapperComponent);