UNPKG

@atlaskit/editor-common

Version:

A package that contains common classes and components for editor and renderer

357 lines (355 loc) • 13.4 kB
import _slicedToArray from "@babel/runtime/helpers/slicedToArray"; import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { bind, bindAll } from 'bind-event-listener'; import { akEditorDefaultLayoutWidth, akEditorFullWidthLayoutWidth, akEditorGutterPadding, akEditorGutterPaddingDynamic, akEditorGutterPaddingReduced, akEditorFullPageNarrowBreakout } from '@atlaskit/editor-shared-styles'; import { fg } from '@atlaskit/platform-feature-flags'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '../analytics'; import { LAYOUT_COLUMN_PADDING, LAYOUT_SECTION_MARGIN } from '../styles'; import { getBrowserInfo } from '../utils/browser'; import Resizer from './Resizer'; import { ResizerBreakoutModeLabel } from './ResizerBreakoutModeLabel'; import { SNAP_GAP, useBreakoutGuidelines } from './useBreakoutGuidelines'; var breakoutSupportedNodes = ['layoutSection', 'expand', 'codeBlock']; var getHandleStyle = function getHandleStyle(node, hidden) { var layoutMarginOffset = 12; if (hidden) { return { left: { display: 'none' }, right: { display: 'none' } }; } switch (node) { case 'codeBlock': return { left: { left: '-12px' }, right: { right: '-12px' } }; // expand and layout section elements have a negative margin applied default: var handleOffset = fg('platform_editor_nested_dnd_styles_changes') ? LAYOUT_SECTION_MARGIN * 2 + layoutMarginOffset : LAYOUT_COLUMN_PADDING * 2; return { left: { left: "-".concat(handleOffset, "px"), height: 'calc(100% - 8px)', bottom: '0px', top: 'unset' }, right: { right: "-".concat(handleOffset, "px"), height: 'calc(100% - 8px)', bottom: '0px', top: 'unset' } }; } }; export var ignoreResizerMutations = function ignoreResizerMutations(mutation) { if (mutation.target instanceof Element) { return mutation.target.classList.contains('resizer-item') || mutation.type === 'attributes' && mutation.attributeName === 'style'; } return mutation.type === 'attributes' && mutation.attributeName === 'style'; }; var resizingStyles = { left: '50%', transform: 'translateX(-50%)', display: 'grid' }; // Apply grid to stop drag handles rendering inside .resizer-item affecting its height var defaultStyles = { display: 'grid' }; var RESIZE_STEP_VALUE = 10; var RESIZER_ENABLE_HANDLES = { left: true, right: true }; /** * BreakoutResizer is a common component used to resize nodes that support the 'Breakout' mark, so it requires * correct ADF support. * * use experiment platform_editor_advanced_layouts * @param root0 * @param root0.editorView * @param root0.nodeType * @param root0.getPos * @param root0.getRef * @param root0.disabled * @param root0.getEditorWidth * @param root0.parentRef * @param root0.displayGuidelines * @param root0.editorAnalyticsApi * @param root0.displayGapCursor * @param root0.onResizeStart * @param root0.dynamicFullWidthGuidelineOffset * @param root0.hidden Hide the resizer handles without outright unrendering them * @returns BreakoutResizer component * @example */ var BreakoutResizer = function BreakoutResizer(_ref) { var editorView = _ref.editorView, nodeType = _ref.nodeType, getPos = _ref.getPos, getRef = _ref.getRef, disabled = _ref.disabled, getEditorWidth = _ref.getEditorWidth, parentRef = _ref.parentRef, displayGuidelines = _ref.displayGuidelines, editorAnalyticsApi = _ref.editorAnalyticsApi, displayGapCursor = _ref.displayGapCursor, onResizeStart = _ref.onResizeStart, dynamicFullWidthGuidelineOffset = _ref.dynamicFullWidthGuidelineOffset, _ref$hidden = _ref.hidden, hidden = _ref$hidden === void 0 ? false : _ref$hidden; var _useState = useState({ minWidth: undefined, maxWidth: undefined, isResizing: false }), _useState2 = _slicedToArray(_useState, 2), _useState2$ = _useState2[0], minWidth = _useState2$.minWidth, maxWidth = _useState2$.maxWidth, isResizing = _useState2$.isResizing, setResizingState = _useState2[1]; var areResizeMetaKeysPressed = useRef(false); var resizerRef = useRef(null); var _useBreakoutGuideline = useBreakoutGuidelines(getEditorWidth, isResizing && editorExperiment('single_column_layouts', true), dynamicFullWidthGuidelineOffset), snaps = _useBreakoutGuideline.snaps, currentLayout = _useBreakoutGuideline.currentLayout, guidelines = _useBreakoutGuideline.guidelines, setCurrentWidth = _useBreakoutGuideline.setCurrentWidth; var browser = getBrowserInfo(); useEffect(function () { if (displayGuidelines) { displayGuidelines(guidelines || []); } }, [displayGuidelines, guidelines]); // Relying on re-renders caused by selection changes inside/around node var isSelectionInNode = useMemo(function () { var pos = getPos(); if (pos === undefined) { return false; } var node = editorView.state.doc.nodeAt(pos); if (node === null) { return false; } var endPos = pos + node.nodeSize; var startPos = pos; var _editorView$state$sel = editorView.state.selection, $from = _editorView$state$sel.$from, $to = _editorView$state$sel.$to; return $from.pos >= startPos && endPos >= $to.pos; }, [editorView.state.doc, editorView.state.selection, getPos]); var handleResizeStart = useCallback(function () { onResizeStart === null || onResizeStart === void 0 || onResizeStart(); var newMinWidth; var newMaxWidth; var widthState = getEditorWidth(); var dispatch = editorView.dispatch, state = editorView.state; displayGapCursor(false); if (widthState !== undefined && widthState.lineLength !== undefined && widthState.width !== undefined) { var padding = widthState.width <= akEditorFullPageNarrowBreakout && editorExperiment('platform_editor_preview_panel_responsiveness', true, { exposure: true }) ? akEditorGutterPaddingReduced : akEditorGutterPaddingDynamic(); newMaxWidth = Math.min(widthState.width - padding * 2 - akEditorGutterPadding, akEditorFullWidthLayoutWidth); newMinWidth = Math.min(widthState.lineLength, akEditorDefaultLayoutWidth, newMaxWidth); } setResizingState({ isResizing: true, minWidth: newMinWidth, maxWidth: newMaxWidth }); dispatch(state.tr.setMeta('is-resizer-resizing', true)); }, [onResizeStart, getEditorWidth, editorView, displayGapCursor]); var handleResize = useCallback(function (originalState, delta) { if (editorExperiment('single_column_layouts', true)) { var newWidth = originalState.width + delta.width; setCurrentWidth(newWidth); } }, [setCurrentWidth]); var handleResizeStop = useCallback(function (originalState, delta) { var newWidth = originalState.width + delta.width; var pos = getPos(); if (pos === undefined) { return; } var state = editorView.state, dispatch = editorView.dispatch; var breakout = state.schema.marks.breakout; var node = state.doc.nodeAt(pos); var newTr = state.tr; if (node && breakoutSupportedNodes.includes(node.type.name)) { if (currentLayout && ['wide', 'full-width'].includes(currentLayout) && editorExperiment('single_column_layouts', true)) { newTr.setNodeMarkup(pos, node.type, node.attrs, [breakout.create({ mode: currentLayout, width: null })]); } else { var newBreakoutWidth = Math.max(newWidth, akEditorDefaultLayoutWidth); newTr.setNodeMarkup(pos, node.type, node.attrs, [breakout.create({ width: newBreakoutWidth })]); var breakoutResizePayload = { action: ACTION.RESIZED, actionSubject: ACTION_SUBJECT.ELEMENT, eventType: EVENT_TYPE.TRACK, attributes: { nodeType: node.type.name, prevWidth: originalState.width, newWidth: newBreakoutWidth } }; editorAnalyticsApi === null || editorAnalyticsApi === void 0 || editorAnalyticsApi.attachAnalyticsEvent(breakoutResizePayload)(newTr); } } newTr.setMeta('is-resizer-resizing', false).setMeta('scrollIntoView', false); displayGapCursor(true); dispatch(newTr); setResizingState({ isResizing: false, minWidth: undefined, maxWidth: undefined }); setCurrentWidth(null); }, [getPos, editorView, displayGapCursor, setCurrentWidth, currentLayout, editorAnalyticsApi]); var handleEscape = useCallback(function () { editorView === null || editorView === void 0 || editorView.focus(); }, [editorView]); var handleLayoutSizeChangeOnKeypress = useCallback(function (step) { if (!parentRef) { return; } var resizerItem = parentRef.closest('.resizer-item'); if (!(resizerItem instanceof HTMLElement)) { return; } var newWidth = resizerItem.offsetWidth + step; if (maxWidth && newWidth > maxWidth || minWidth && newWidth < minWidth) { return; } handleResizeStop({ width: resizerItem.offsetWidth, x: 0, y: 0, height: 0 }, { width: step, height: 0 }); }, [handleResizeStop, maxWidth, minWidth, parentRef]); var resizeHandleKeyDownHandler = useCallback(function (event) { var isBracketKey = event.code === 'BracketRight' || event.code === 'BracketLeft'; var metaKey = browser.mac ? event.metaKey : event.ctrlKey; if (event.altKey || metaKey || event.shiftKey) { areResizeMetaKeysPressed.current = true; } if (event.altKey && metaKey) { if (isBracketKey) { event.preventDefault(); handleLayoutSizeChangeOnKeypress(event.code === 'BracketRight' ? RESIZE_STEP_VALUE : -RESIZE_STEP_VALUE); } } else if (!areResizeMetaKeysPressed.current) { handleEscape(); } }, [handleEscape, handleLayoutSizeChangeOnKeypress, browser]); var resizeHandleKeyUpHandler = useCallback(function (event) { if (event.altKey || event.metaKey) { areResizeMetaKeysPressed.current = false; } return; }, [areResizeMetaKeysPressed]); var resizerGlobalKeyDownHandler = useCallback(function (event) { if (!resizerRef.current) { return; } var resizeHandleThumbEl = resizerRef.current.getResizerThumbEl(); var metaKey = browser.mac ? event.metaKey : event.ctrlKey; var isTargetResizeHandle = event.target instanceof HTMLElement && event.target.classList.contains('resizer-handle-thumb'); if (event.altKey && event.shiftKey && metaKey && event.code === 'KeyR' || isTargetResizeHandle && (event.altKey || metaKey || event.shiftKey)) { event.preventDefault(); if (!resizeHandleThumbEl) { return; } resizeHandleThumbEl.focus(); resizeHandleThumbEl.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' }); } }, [resizerRef, browser]); useLayoutEffect(function () { if (!resizerRef.current || !editorView) { return; } var resizeHandleThumbEl = resizerRef.current.getResizerThumbEl(); if (!resizeHandleThumbEl) { return; } var editorViewDom = editorView.dom; var unbindEditorViewDom = bind(editorViewDom, { type: 'keydown', listener: resizerGlobalKeyDownHandler }); var unbindResizeHandle = bindAll(resizeHandleThumbEl, [{ type: 'keydown', listener: resizeHandleKeyDownHandler }, { type: 'keyup', listener: resizeHandleKeyUpHandler }]); return function () { unbindEditorViewDom(); unbindResizeHandle(); }; }, [editorView, resizerGlobalKeyDownHandler, resizeHandleKeyDownHandler, resizeHandleKeyUpHandler]); if (disabled) { return /*#__PURE__*/React.createElement("div", { "data-testid": "breakout-resizer-editor-view-wrapper", ref: function ref(_ref2) { return getRef && getRef(_ref2); } }); } return /*#__PURE__*/React.createElement(Resizer, { ref: resizerRef, enable: expValEquals('platform_editor_perf_lint_cleanup', 'isEnabled', true) ? RESIZER_ENABLE_HANDLES : { left: true, right: true }, snap: snaps || undefined, snapGap: SNAP_GAP, handleStyles: getHandleStyle(nodeType, hidden), minWidth: minWidth, maxWidth: maxWidth // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop , style: isResizing ? resizingStyles : defaultStyles, handleResizeStart: handleResizeStart, handleResizeStop: handleResizeStop, handleResize: handleResize, childrenDOMRef: getRef, resizeRatio: 2, isHandleVisible: isSelectionInNode, handleSize: "clamped", handleHighlight: "full-height", handlePositioning: "adjacent", handleAlignmentMethod: "sticky", labelComponent: currentLayout && editorExperiment('single_column_layouts', true) && ['full-width', 'wide'].includes(currentLayout || '') && /*#__PURE__*/React.createElement(ResizerBreakoutModeLabel, { layout: currentLayout }) }); }; export { BreakoutResizer };