UNPKG

@atlaskit/editor-plugin-breakout

Version:

Breakout plugin for @atlaskit/editor-core

260 lines (259 loc) 10.4 kB
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 }); } });