UNPKG

@atlaskit/editor-plugin-synced-block

Version:

SyncedBlock plugin for @atlaskit/editor-core

226 lines (224 loc) 11.3 kB
import React from 'react'; import { INPUT_METHOD } from '@atlaskit/editor-common/analytics'; import commonMessages, { syncBlockMessages as messages, toolbarInsertBlockMessages } from '@atlaskit/editor-common/messages'; import { GapCursorSelection, Side } from '@atlaskit/editor-common/selection'; import { FloatingToolbarButton as Button } from '@atlaskit/editor-common/ui'; import { findDomRefAtPos } from '@atlaskit/editor-prosemirror/utils'; import { akEditorSelectedNodeClassName } from '@atlaskit/editor-shared-styles/consts'; import { SyncBlockError } from '@atlaskit/editor-synced-block-provider'; import CopyIcon from '@atlaskit/icon/core/copy'; import DeleteIcon from '@atlaskit/icon/core/delete'; import EditIcon from '@atlaskit/icon/core/edit'; import LinkBrokenIcon from '@atlaskit/icon/core/link-broken'; import MegaphoneIcon from '@atlaskit/icon/core/megaphone'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { copySyncedBlockReferenceToClipboard, editSyncedBlockSource, removeSyncedBlock, unsync } from '../editor-commands'; import { findSyncBlockOrBodiedSyncBlock, isBodiedSyncBlockNode } from '../pm-plugins/utils/utils'; import { SYNCED_BLOCK_BUTTON_TEST_ID } from '../types'; import { SyncedLocationDropdown, SyncedLocationDropdownWithCount } from './SyncedLocationDropdown'; export const getToolbarConfig = (state, intl, api, syncBlockStore, isLivePage, onGiveFeedback) => { var _syncBlockInstance$er, _api$decorations, _api$connectivity, _api$connectivity$sha; const syncBlockObject = findSyncBlockOrBodiedSyncBlock(state.schema, state.selection); if (!syncBlockObject) { return; } if (syncBlockStore.sourceManager.isPendingCreation(syncBlockObject.node.attrs.resourceId)) { return; } const isSyncBlockActivationEnabled = expValEquals('platform_editor_sync_block_activation', 'isEnabled', true); const syncBlockInstance = syncBlockStore.referenceManager.getFromCache(syncBlockObject.node.attrs.resourceId); const isUnsyncedBlock = (syncBlockInstance === null || syncBlockInstance === void 0 ? void 0 : (_syncBlockInstance$er = syncBlockInstance.error) === null || _syncBlockInstance$er === void 0 ? void 0 : _syncBlockInstance$er.type) === SyncBlockError.NotFound; const isErroredBlock = syncBlockInstance === null || syncBlockInstance === void 0 ? void 0 : syncBlockInstance.error; const { schema: { nodes: { bodiedSyncBlock } } } = state; const isBodiedSyncBlock = isBodiedSyncBlockNode(syncBlockObject.node, bodiedSyncBlock); const { resourceId, localId } = syncBlockObject.node.attrs; const { formatMessage } = intl; const nodeType = syncBlockObject.node.type; const hoverDecoration = api === null || api === void 0 ? void 0 : (_api$decorations = api.decorations) === null || _api$decorations === void 0 ? void 0 : _api$decorations.actions.hoverDecoration; const hoverDecorationProps = (nodeType, className) => ({ onMouseEnter: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, true, className), onMouseLeave: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, false, className), onFocus: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, true, className), onBlur: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, false, className) }); const items = []; if (isUnsyncedBlock) { const deleteButton = { type: 'button', title: formatMessage(commonMessages.delete), onClick: removeSyncedBlock(api), icon: DeleteIcon, testId: SYNCED_BLOCK_BUTTON_TEST_ID.syncedBlockToolbarReferenceDelete, ...hoverDecorationProps(nodeType, akEditorSelectedNodeClassName) }; items.push(deleteButton); } else { // When the synced block activation experiment is enabled, the copy action is // re-framed as the primary "Copy to sync" action: it shows a visible label and // is moved to the first toolbar position (before the synced-location info). See // EDITOR-8060. const copyButton = { id: 'editor.syncedBlock.copy', type: 'button', appearance: 'subtle', icon: CopyIcon, title: isSyncBlockActivationEnabled ? formatMessage(messages.copyToSyncLabel) : formatMessage(messages.copySyncBlockLabel), showTitle: isSyncBlockActivationEnabled, tooltipContent: isSyncBlockActivationEnabled ? undefined : formatMessage(messages.copySyncedBlockTooltip), testId: isSyncBlockActivationEnabled ? SYNCED_BLOCK_BUTTON_TEST_ID.syncedBlockToolbarCopy : undefined, onClick: copySyncedBlockReferenceToClipboard(syncBlockStore, INPUT_METHOD.SYNCED_BLOCK_TB, api, isLivePage), ...hoverDecorationProps(nodeType, akEditorSelectedNodeClassName) }; // In the activation experience the primary "Copy to sync" action leads the // toolbar, ahead of the informational synced-location dropdown. if (isSyncBlockActivationEnabled) { items.push(copyButton); } if (!isErroredBlock) { const syncedLocation = { type: 'custom', fallback: [], render: (_view, _idx, _dispatchAnalyticsEvent, floatingToolbarRenderContext) => { if (isSyncBlockActivationEnabled) { return /*#__PURE__*/React.createElement(SyncedLocationDropdownWithCount, { key: `${resourceId}:${localId}:${isBodiedSyncBlock}`, syncBlockStore: syncBlockStore, resourceId: resourceId, localId: localId, intl: intl, isSource: isBodiedSyncBlock, api: api, floatingToolbarRenderContext: floatingToolbarRenderContext }); } return /*#__PURE__*/React.createElement(SyncedLocationDropdown, { syncBlockStore: syncBlockStore, resourceId: resourceId, localId: localId, intl: intl, isSource: isBodiedSyncBlock, api: api, floatingToolbarRenderContext: floatingToolbarRenderContext }); } }; const unsyncButton = { type: 'custom', fallback: [], render: view => { var _syncBlockInstance$da; return /*#__PURE__*/React.createElement(Button, { areAnyNewToolbarFlagsEnabled: true, disabled: (syncBlockInstance === null || syncBlockInstance === void 0 ? void 0 : (_syncBlockInstance$da = syncBlockInstance.data) === null || _syncBlockInstance$da === void 0 ? void 0 : _syncBlockInstance$da.status) === 'unpublished', icon: /*#__PURE__*/React.createElement(LinkBrokenIcon, { label: "" }), title: formatMessage(messages.unsyncButton) // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , onClick: () => unsync(syncBlockStore, isBodiedSyncBlock, view), testId: isBodiedSyncBlock ? SYNCED_BLOCK_BUTTON_TEST_ID.syncedBlockToolbarSourceUnsync : SYNCED_BLOCK_BUTTON_TEST_ID.syncedBlockToolbarReferenceUnsync }); } }; items.push(syncedLocation, unsyncButton); } // Outside the activation experience the copy button keeps its original // (icon-only, secondary) position after the synced-location info. if (!isSyncBlockActivationEnabled) { items.push(copyButton); } const disabled = !syncBlockStore.referenceManager.getSyncBlockURL(syncBlockObject.node.attrs.resourceId); if (!isBodiedSyncBlock) { const editSourceButton = { id: 'editor.syncedBlock.editSource', type: 'button', disabled, appearance: 'subtle', icon: EditIcon, title: formatMessage(messages.editSourceLabel), showTitle: false, tooltipContent: disabled ? formatMessage(messages.editSourceTooltipDisabled) : formatMessage(messages.editSourceTooltip), onClick: editSyncedBlockSource(syncBlockStore, api), ...hoverDecorationProps(nodeType, akEditorSelectedNodeClassName) }; items.push(editSourceButton); } if (!isSyncBlockActivationEnabled) { const separator = { type: 'separator' }; const deleteButton = { id: 'editor.syncedBlock.delete', type: 'button', appearance: 'danger', title: formatMessage(commonMessages.delete), showTitle: false, tooltipContent: formatMessage(commonMessages.delete), onClick: removeSyncedBlock(api), icon: DeleteIcon, testId: isBodiedSyncBlock ? SYNCED_BLOCK_BUTTON_TEST_ID.syncedBlockToolbarSourceDelete : SYNCED_BLOCK_BUTTON_TEST_ID.syncedBlockToolbarReferenceDelete, ...hoverDecorationProps(nodeType) }; items.push(separator, deleteButton); } else { // testId is required to show focus on trigger button on ESC key press // see hideOnEsc in platform/packages/editor/editor-plugin-floating-toolbar/src/ui/Dropdown.tsx const testId = isBodiedSyncBlock ? SYNCED_BLOCK_BUTTON_TEST_ID.syncedBlockToolbarSourceOverflowTrigger : SYNCED_BLOCK_BUTTON_TEST_ID.syncedBlockToolbarReferenceOverflowTrigger; const overflowMenuConfig = [{ type: 'overflow-dropdown', testId, options: [...(isSyncBlockActivationEnabled && onGiveFeedback ? [{ title: formatMessage(toolbarInsertBlockMessages.feedbackDialog), icon: /*#__PURE__*/React.createElement(MegaphoneIcon, { label: "" }), onClick: () => { api === null || api === void 0 ? void 0 : api.core.actions.execute(({ tr }) => tr.setSelection(new GapCursorSelection(tr.doc.resolve(syncBlockObject.pos + syncBlockObject.node.nodeSize), Side.RIGHT))); onGiveFeedback({ blockType: isBodiedSyncBlock ? 'source' : 'reference', entryPoint: 'overflow-menu' }); return true; } }] : []), { title: formatMessage(commonMessages.delete), onClick: removeSyncedBlock(api), icon: /*#__PURE__*/React.createElement(DeleteIcon, { label: "" }), testId: isBodiedSyncBlock ? SYNCED_BLOCK_BUTTON_TEST_ID.syncedBlockToolbarSourceDelete : SYNCED_BLOCK_BUTTON_TEST_ID.syncedBlockToolbarReferenceDelete, ...hoverDecorationProps(nodeType) }] }]; items.push(...overflowMenuConfig); } } const getDomRef = editorView => { const domAtPos = editorView.domAtPos.bind(editorView); const element = findDomRefAtPos(syncBlockObject.pos, domAtPos); return element; }; return { title: 'Synced Block floating controls', getDomRef, nodeType, items, scrollable: true, groupLabel: formatMessage(messages.syncBlockGroup), visible: (api === null || api === void 0 ? void 0 : (_api$connectivity = api.connectivity) === null || _api$connectivity === void 0 ? void 0 : (_api$connectivity$sha = _api$connectivity.sharedState.currentState()) === null || _api$connectivity$sha === void 0 ? void 0 : _api$connectivity$sha.mode) !== 'offline' }; };