UNPKG

@atlaskit/editor-plugin-synced-block

Version:

SyncedBlock plugin for @atlaskit/editor-core

94 lines (91 loc) 5.58 kB
import React, { useCallback, useEffect, useRef } 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 { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; 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; 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 isUnsyncedBlock = isUnpublishedBlock || (syncBlockFetchResult === null || syncBlockFetchResult === void 0 ? void 0 : (_syncBlockFetchResult5 = syncBlockFetchResult.syncBlockInstance) === null || _syncBlockFetchResult5 === void 0 ? void 0 : (_syncBlockFetchResult6 = _syncBlockFetchResult5.error) === null || _syncBlockFetchResult6 === void 0 ? void 0 : _syncBlockFetchResult6.type) === SyncBlockError.NotFound; // Evaluated unconditionally so the experiment exposure is tracked correctly // (recorded on every render, not lazily on first click). const isSyncBlockActivationEnabled = expValEquals('platform_editor_sync_block_activation', 'isEnabled', true); // 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); useEffect(() => { const containerEl = rendererRef.current; if (!containerEl || !isSyncBlockActivationEnabled) { 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; }, [isSyncBlockActivationEnabled]); 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 })), /*#__PURE__*/React.createElement(SyncBlockLabel, { isSource: false, title: title, contentUpdatedAt: contentUpdatedAt, localId: localId, isUnsyncedBlock: isUnsyncedBlock })); }; export const SyncBlockRendererWrapper = /*#__PURE__*/React.memo(SyncBlockRendererWrapperComponent);