@atlaskit/editor-plugin-synced-block
Version:
SyncedBlock plugin for @atlaskit/editor-core
226 lines (224 loc) • 11.3 kB
JavaScript
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'
};
};