@atlaskit/editor-plugin-extension
Version:
editor-plugin-extension plugin for @atlaskit/editor-core
236 lines (233 loc) • 10.5 kB
JavaScript
import { messages } from '@atlaskit/editor-common/extensions';
import commonMessages from '@atlaskit/editor-common/messages';
import { BODIED_EXT_MBE_MARGIN_TOP } from '@atlaskit/editor-common/styles';
import { getChildrenInfo, getNodeName, isReferencedSource } from '@atlaskit/editor-common/utils';
import { findParentNodeOfType, hasParentNodeOfType } from '@atlaskit/editor-prosemirror/utils';
import ContentWidthNarrowIcon from '@atlaskit/icon/core/content-width-narrow';
import ContentWidthWideIcon from '@atlaskit/icon/core/content-width-wide';
import DeleteIcon from '@atlaskit/icon/core/delete';
import EditIcon from '@atlaskit/icon/core/edit';
import ExpandHorizontalIcon from '@atlaskit/icon/core/expand-horizontal';
import LegacyEditIcon from '@atlaskit/icon/glyph/editor/edit';
import CenterIcon from '@atlaskit/icon/glyph/editor/media-center';
import FullWidthIcon from '@atlaskit/icon/glyph/editor/media-full-width';
import WideIcon from '@atlaskit/icon/glyph/editor/media-wide';
import RemoveIcon from '@atlaskit/icon/glyph/editor/remove';
import { fg } from '@atlaskit/platform-feature-flags';
import { editExtension } from '../editor-actions/actions';
import { removeDescendantNodes, removeExtension, updateExtensionLayout } from '../editor-commands/commands';
import { pluginKey as macroPluginKey } from './macro/plugin-key';
import { getPluginState } from './plugin-factory';
import { getSelectedExtension } from './utils';
const isLayoutSupported = (state, selectedExtNode) => {
const {
schema: {
nodes: {
bodiedExtension,
extension,
layoutSection,
table,
expand,
multiBodiedExtension
}
},
selection
} = state;
if (!selectedExtNode) {
return false;
}
const isMultiBodiedExtension = selectedExtNode.node.type === multiBodiedExtension;
const isNonEmbeddedBodiedExtension = selectedExtNode.node.type === bodiedExtension && !hasParentNodeOfType([multiBodiedExtension].filter(Boolean))(selection);
const isNonEmbeddedExtension = selectedExtNode.node.type === extension && !hasParentNodeOfType([bodiedExtension, table, expand, multiBodiedExtension].filter(Boolean))(selection);
// if selection belongs to layout supported extension category
// and not inside a layoutSection
return !!((isMultiBodiedExtension || isNonEmbeddedBodiedExtension || isNonEmbeddedExtension) && !hasParentNodeOfType([layoutSection])(selection));
};
const breakoutOptions = (state, formatMessage, extensionState, breakoutEnabled, editorAnalyticsAPI
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/max-params
) => {
const nodeWithPos = getSelectedExtension(state, true);
// we should only return breakout options when breakouts are enabled and the node supports them
if (nodeWithPos && breakoutEnabled && isLayoutSupported(state, nodeWithPos)) {
const {
layout
} = nodeWithPos.node.attrs;
return [{
type: 'button',
icon: ContentWidthNarrowIcon,
iconFallback: CenterIcon,
onClick: updateExtensionLayout('default', editorAnalyticsAPI),
selected: layout === 'default',
title: formatMessage(commonMessages.layoutFixedWidth),
tabIndex: null
}, {
type: 'button',
icon: ContentWidthWideIcon,
iconFallback: WideIcon,
onClick: updateExtensionLayout('wide', editorAnalyticsAPI),
selected: layout === 'wide',
title: formatMessage(commonMessages.layoutWide),
tabIndex: null
}, {
type: 'button',
icon: ExpandHorizontalIcon,
iconFallback: FullWidthIcon,
onClick: updateExtensionLayout('full-width', editorAnalyticsAPI),
selected: layout === 'full-width',
title: formatMessage(commonMessages.layoutFullWidth),
tabIndex: null
}];
}
return [];
};
const editButton = (formatMessage, extensionState, applyChangeToContextPanel, editorAnalyticsAPI
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/max-params
) => {
if (!extensionState.showEditButton) {
return [];
}
return [{
id: 'editor.extension.edit',
type: 'button',
icon: EditIcon,
iconFallback: LegacyEditIcon,
testId: 'extension-toolbar-edit-button',
// Taking the latest `updateExtension` from plugin state to avoid race condition @see ED-8501
onClick: (state, dispatch, view) => {
const macroState = macroPluginKey.getState(state);
const {
updateExtension
} = getPluginState(state);
editExtension(macroState && macroState.macroProvider, applyChangeToContextPanel, editorAnalyticsAPI, updateExtension)(state, dispatch, view);
return true;
},
title: formatMessage(messages.edit),
tabIndex: null,
focusEditoronEnter: true
}];
};
export const getToolbarConfig = ({
breakoutEnabled = true,
hoverDecoration,
applyChangeToContextPanel,
editorAnalyticsAPI
}) => (state, intl) => {
const {
formatMessage
} = intl;
const extensionState = getPluginState(state);
if (extensionState && !extensionState.showContextPanel && extensionState.element) {
const nodeType = [state.schema.nodes.extension, state.schema.nodes.inlineExtension, state.schema.nodes.bodiedExtension, state.schema.nodes.multiBodiedExtension];
const editButtonArray = editButton(formatMessage, extensionState, applyChangeToContextPanel, editorAnalyticsAPI);
const breakoutButtonArray = breakoutOptions(state, formatMessage, extensionState, breakoutEnabled, editorAnalyticsAPI);
const extensionObj = getSelectedExtension(state, true);
// Check if we need to show confirm dialog for delete button
let confirmDialog;
if (isReferencedSource(state, extensionObj === null || extensionObj === void 0 ? void 0 : extensionObj.node)) {
confirmDialog = () => {
const localSourceName = formatMessage(messages.unnamedSource);
return {
title: formatMessage(messages.deleteElementTitle),
okButtonLabel: formatMessage(messages.confirmDeleteLinkedModalOKButton),
message: formatMessage(messages.confirmDeleteLinkedModalMessage, {
nodeName: getNodeName(state, extensionObj === null || extensionObj === void 0 ? void 0 : extensionObj.node) || localSourceName
}),
isReferentialityDialog: true,
getChildrenInfo: () => getChildrenInfo(state, extensionObj === null || extensionObj === void 0 ? void 0 : extensionObj.node),
checkboxLabel: formatMessage(messages.confirmModalCheckboxLabel),
onConfirm: (isChecked = false) => clickWithCheckboxHandler(isChecked, extensionObj === null || extensionObj === void 0 ? void 0 : extensionObj.node)
};
};
}
return {
title: 'Extension floating controls',
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
getDomRef: () => extensionState.element.parentElement || undefined,
nodeType,
onPositionCalculated: (editorView, nextPos) => {
const {
state: {
schema,
selection
}
} = editorView;
const extensionNode = getSelectedExtension(state, true);
const possibleMbeParent = findParentNodeOfType(schema.nodes.extensionFrame)(selection);
// We only want to use calculated position in case of a bodiedExtension present inside an MBE node
const isBodiedExtensionInsideMBE = possibleMbeParent && (extensionNode === null || extensionNode === void 0 ? void 0 : extensionNode.node.type.name) === 'bodiedExtension';
if (fg('platform_editor_legacy_content_macro')) {
if (!extensionNode) {
return nextPos;
}
const isInsideEditableExtensionArea = !!editorView.dom.closest('.extension-editable-area');
if (!isBodiedExtensionInsideMBE && !isInsideEditableExtensionArea) {
return nextPos;
}
} else {
if (!isBodiedExtensionInsideMBE) {
return nextPos;
}
}
const scrollWrapper = editorView.dom.closest('.fabric-editor-popup-scroll-parent') || document.body;
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
const nestedBodiedExtensionDomElement = editorView.nodeDOM(extensionNode.pos);
const nestedBodiedExtensionRect = nestedBodiedExtensionDomElement === null || nestedBodiedExtensionDomElement === void 0 ? void 0 : nestedBodiedExtensionDomElement.getBoundingClientRect();
const wrapperBounds = scrollWrapper.getBoundingClientRect();
const toolbarTopPos = nestedBodiedExtensionRect.bottom - wrapperBounds.top + scrollWrapper.scrollTop + BODIED_EXT_MBE_MARGIN_TOP;
return {
top: toolbarTopPos,
left: nextPos.left
};
},
items: [...editButtonArray, ...breakoutButtonArray, {
type: 'separator',
hidden: editButtonArray.length === 0 && breakoutButtonArray.length === 0
}, {
type: 'extensions-placeholder',
separator: 'end'
}, {
type: 'copy-button',
supportsViewMode: !fg('platform_editor_remove_copy_button_from_view_mode'),
items: [{
state,
formatMessage: intl.formatMessage,
nodeType
}]
}, {
type: 'separator'
}, {
id: 'editor.extension.delete',
type: 'button',
icon: DeleteIcon,
iconFallback: RemoveIcon,
appearance: 'danger',
onClick: removeExtension(editorAnalyticsAPI),
onMouseEnter: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, true),
onMouseLeave: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, false),
onFocus: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, true),
onBlur: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, false),
focusEditoronEnter: true,
title: formatMessage(commonMessages.remove),
tabIndex: null,
confirmDialog
}],
scrollable: true
};
}
return;
};
const clickWithCheckboxHandler = (isChecked, node) => (state, dispatch) => {
if (!node) {
return false;
}
if (!isChecked) {
removeExtension()(state, dispatch);
} else {
removeDescendantNodes(node)(state, dispatch);
}
return true;
};