@atlaskit/editor-plugin-breakout
Version:
Breakout plugin for @atlaskit/editor-core
260 lines (259 loc) • 10.4 kB
JavaScript
import React, { useState } from 'react';
import { breakout } from '@atlaskit/adf-schema';
import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { BreakoutCssClassName } from '@atlaskit/editor-common/styles';
import { usePluginStateEffect } from '@atlaskit/editor-common/use-plugin-state-effect';
import { useSharedPluginStateSelector } from '@atlaskit/editor-common/use-shared-plugin-state-selector';
import { akEditorSwoopCubicBezier } from '@atlaskit/editor-shared-styles';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
import { pluginKey } from './pm-plugins/plugin-key';
import { createResizingPlugin, resizingPluginKey } from './pm-plugins/resizing-plugin';
import { findSupportedNodeForBreakout } from './pm-plugins/utils/find-breakout-node';
import { getBreakoutMode } from './pm-plugins/utils/get-breakout-mode';
import { GuidelineLabel } from './ui/GuidelineLabel';
import LayoutButton from './ui/LayoutButton';
class BreakoutView {
constructor(
/**
* Note: this is actually a PMMark -- however our version
* of the prosemirror and prosemirror types mean using PMNode
* is not problematic.
*/
mark, view, appearance) {
const dom = document.createElement('div');
const contentDOM = document.createElement('div');
contentDOM.className = BreakoutCssClassName.BREAKOUT_MARK_DOM;
contentDOM.setAttribute('data-testid', 'ak-editor-breakout-mark-dom');
dom.className = BreakoutCssClassName.BREAKOUT_MARK;
dom.setAttribute('data-layout', mark.attrs.mode);
dom.setAttribute('data-testid', 'ak-editor-breakout-mark');
dom.appendChild(contentDOM);
dom.style.transform = 'none';
dom.style.display = 'flex';
dom.style.justifyContent = 'center';
contentDOM.style.transition = `min-width 0.5s ${akEditorSwoopCubicBezier}`;
if (editorExperiment('advanced_layouts', true)) {
if (mark.attrs.width) {
contentDOM.style.minWidth = `min(${mark.attrs.width}px, calc(100cqw - var(--ak-editor--breakout-full-page-guttering-padding)))`;
} else {
// original breakout algorithm is in calcBreakoutWidth from platform/packages/editor/editor-common/src/utils/breakout.ts
if (mark.attrs.mode === 'full-width') {
contentDOM.style.minWidth = `max(var(--ak-editor--line-length), min(var(--ak-editor--full-width-layout-width), calc(100cqw - var(--ak-editor--breakout-full-page-guttering-padding))))`;
}
if (mark.attrs.mode === 'wide') {
if (appearance && appearance === 'full-width' && editorExperiment('single_column_layouts', true) && true) {
contentDOM.style.minWidth = `min(var(--ak-editor--breakout-wide-layout-width), calc(100cqw - var(--ak-editor--breakout-full-page-guttering-padding)))`;
} else {
contentDOM.style.minWidth = `max(var(--ak-editor--line-length), min(var(--ak-editor--breakout-wide-layout-width), calc(100cqw - var(--ak-editor--breakout-full-page-guttering-padding))))`;
}
}
}
} else {
// original breakout algorithm is in calcBreakoutWidth from platform/packages/editor/editor-common/src/utils/breakout.ts
if (mark.attrs.mode === 'full-width') {
contentDOM.style.minWidth = `max(var(--ak-editor--line-length), min(var(--ak-editor--full-width-layout-width), calc(100cqw - var(--ak-editor--breakout-full-page-guttering-padding))))`;
}
if (mark.attrs.mode === 'wide') {
contentDOM.style.minWidth = `max(var(--ak-editor--line-length), min(var(--ak-editor--breakout-wide-layout-width), calc(100cqw - var(--ak-editor--breakout-full-page-guttering-padding))))`;
}
}
this.dom = dom;
this.mark = mark;
this.view = view;
this.contentDOM = contentDOM;
}
}
function shouldPluginStateUpdate(newBreakoutNode, currentBreakoutNode) {
if (newBreakoutNode && currentBreakoutNode) {
return newBreakoutNode !== currentBreakoutNode;
}
return newBreakoutNode || currentBreakoutNode ? true : false;
}
function createPlugin(api, {
dispatch
}, appearance) {
return new SafePlugin({
state: {
init() {
return {
breakoutNode: undefined,
activeGuidelineKey: undefined
};
},
apply(tr, pluginState) {
const breakoutNode = findSupportedNodeForBreakout(tr.selection);
if (shouldPluginStateUpdate(breakoutNode, pluginState.breakoutNode)) {
const nextPluginState = {
...pluginState,
breakoutNode
};
dispatch(pluginKey, nextPluginState);
return nextPluginState;
}
return pluginState;
}
},
key: pluginKey,
props: {
nodeViews: {
// Note: When we upgrade to prosemirror 1.27.2 -- we should
// move this to markViews.
// See the following link for more details:
// https://prosemirror.net/docs/ref/#view.EditorProps.nodeViews.
breakout: (mark, view) => {
return new BreakoutView(mark, view, appearance);
}
}
}
});
}
const LayoutButtonWrapper = ({
api,
editorView,
boundariesElement,
scrollableElement,
mountPoint
}) => {
const {
editorDisabled,
isDragging,
isPMDragging,
mode
} = useSharedPluginStateWithSelector(api, ['editorViewMode', 'editorDisabled', 'blockControls'], states => {
var _states$blockControls, _states$blockControls2, _states$editorViewMod, _states$editorDisable;
return {
isDragging: (_states$blockControls = states.blockControlsState) === null || _states$blockControls === void 0 ? void 0 : _states$blockControls.isDragging,
isPMDragging: (_states$blockControls2 = states.blockControlsState) === null || _states$blockControls2 === void 0 ? void 0 : _states$blockControls2.isPMDragging,
mode: (_states$editorViewMod = states.editorViewModeState) === null || _states$editorViewMod === void 0 ? void 0 : _states$editorViewMod.mode,
editorDisabled: (_states$editorDisable = states.editorDisabledState) === null || _states$editorDisable === void 0 ? void 0 : _states$editorDisable.editorDisabled
};
});
const [breakoutNodePresent, setBreakoutNodePresent] = useState(false);
const [breakoutMode, setBreakoutMode] = useState(expValEquals('platform_editor_hydratable_ui', 'isEnabled', true) && !editorView ? undefined :
// Remove ! during platform_editor_hydratable_ui cleanup
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
getBreakoutMode(editorView.state));
usePluginStateEffect(api, ['breakout'], ({
breakoutState
}) => {
if (expValEquals('platform_editor_hydratable_ui', 'isEnabled', true) && !editorView) {
return;
}
if (breakoutState !== null && breakoutState !== void 0 && breakoutState.breakoutNode && !breakoutNodePresent) {
setBreakoutNodePresent(true);
}
if (!(breakoutState !== null && breakoutState !== void 0 && breakoutState.breakoutNode) && breakoutNodePresent) {
setBreakoutNodePresent(false);
}
// Remove ! during platform_editor_hydratable_ui cleanup
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const nextBreakoutMode = getBreakoutMode(editorView.state);
if (nextBreakoutMode !== breakoutMode) {
setBreakoutMode(nextBreakoutMode);
}
});
const interactionState = useSharedPluginStateSelector(api, 'interaction.interactionState');
if (interactionState === 'hasNotHadInteraction') {
return null;
}
if (isDragging || isPMDragging) {
if (editorExperiment('advanced_layouts', true)) {
return null;
}
}
const isViewMode = mode === 'view';
const isEditMode = mode === 'edit';
return !isViewMode && editorDisabled === false ? /*#__PURE__*/React.createElement(LayoutButton, {
editorView: editorView,
mountPoint: mountPoint,
boundariesElement: boundariesElement,
scrollableElement: scrollableElement,
isLivePage: isEditMode,
isBreakoutNodePresent: breakoutNodePresent,
breakoutMode: breakoutMode,
api: api
}) : null;
};
export const breakoutPlugin = ({
config: options,
api
}) => ({
name: 'breakout',
pmPlugins() {
if (expValEquals('platform_editor_breakout_resizing', 'isEnabled', true)) {
return [{
name: 'breakout-resizing',
plugin: ({
getIntl,
nodeViewPortalProviderAPI
}) => createResizingPlugin(api, getIntl, nodeViewPortalProviderAPI, options)
}];
}
return [{
name: 'breakout',
plugin: props => createPlugin(api, props, options === null || options === void 0 ? void 0 : options.appearance)
}];
},
marks() {
return [{
name: 'breakout',
mark: breakout
}];
},
getSharedState(editorState) {
if (!editorState) {
return {
breakoutNode: undefined
};
}
if (expValEquals('platform_editor_breakout_resizing', 'isEnabled', true)) {
const resizingPluginState = resizingPluginKey.getState(editorState);
if (!resizingPluginState) {
return {
breakoutNode: undefined,
activeGuidelineKey: undefined
};
}
return resizingPluginState;
}
const pluginState = pluginKey.getState(editorState);
if (!pluginState) {
return {
breakoutNode: undefined
};
}
return pluginState;
},
contentComponent({
editorView,
popupsMountPoint,
popupsBoundariesElement,
popupsScrollableElement
}) {
if (!editorView) {
return null;
}
if (expValEquals('platform_editor_breakout_resizing', 'isEnabled', true)) {
return /*#__PURE__*/React.createElement(GuidelineLabel, {
api: api,
editorView: editorView,
mountPoint: popupsMountPoint,
boundariesElement: popupsBoundariesElement,
scrollableElement: popupsScrollableElement
});
}
// This is a bit crappy, but should be resolved once we move to a static schema.
if (options && !options.allowBreakoutButton) {
return null;
}
return /*#__PURE__*/React.createElement(LayoutButtonWrapper, {
api: api,
mountPoint: popupsMountPoint,
editorView: editorView,
boundariesElement: popupsBoundariesElement,
scrollableElement: popupsScrollableElement
});
}
});