UNPKG

@atlaskit/editor-plugin-table

Version:

Table plugin for the @atlaskit/editor

249 lines (247 loc) 10.7 kB
/* eslint-disable @atlaskit/design-system/no-html-button */ import React, { useEffect, useMemo, useRef, useState } from 'react'; import classnames from 'classnames'; import ReactDOM from 'react-dom'; import { injectIntl } from 'react-intl'; import { getBrowserInfo } from '@atlaskit/editor-common/browser'; import { tableMessages as messages } from '@atlaskit/editor-common/messages'; import { TextSelection } from '@atlaskit/editor-prosemirror/state'; import { findTable, TableMap } from '@atlaskit/editor-tables'; import { draggable } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { getPluginState as getDnDPluginState } from '../../pm-plugins/drag-and-drop/plugin-factory'; import { findDuplicatePosition, hasMergedCellsInSelection } from '../../pm-plugins/utils/merged-cells'; import { TableCssClassName as ClassName } from '../../types'; import { dragTableInsertColumnButtonSize } from '../consts'; import { DragPreview } from '../DragPreview'; import { HandleIconComponent } from './HandleIconComponent'; const DragHandleComponent = ({ isDragMenuTarget, tableLocalId, direction = 'row', appearance = 'default', indexes, forceDefaultHandle = false, previewWidth, previewHeight, onMouseOver, onMouseOut, onFocus, onBlur, toggleDragMenu, hoveredCell, onClick, editorView, intl: { formatMessage } }) => { const dragHandleDivRef = useRef(null); const [previewContainer, setPreviewContainer] = useState(null); const { state, state: { selection } } = editorView; const { isDragMenuOpen = false } = getDnDPluginState(state); const [isHovered, setIsHovered] = useState(false); const isRow = direction === 'row'; const isColumn = direction === 'column'; const hasMergedCells = useMemo(() => { const table = findTable(selection); if (!table) { return false; } const map = TableMap.get(table === null || table === void 0 ? void 0 : table.node); if (!map.hasMergedCells() || indexes.length < 1) { return false; } const { mapByColumn, mapByRow } = map; // this handle when hover to first column or row which has merged cells. if (hoveredCell && hoveredCell.rowIndex !== undefined && hoveredCell.colIndex !== undefined && selection instanceof TextSelection) { const { rowIndex, colIndex } = hoveredCell; const mergedPositionInRow = findDuplicatePosition(mapByRow[rowIndex]); const mergedPositionInCol = findDuplicatePosition(mapByColumn[colIndex]); const hasMergedCellsInFirstRowOrColumn = direction === 'column' ? mergedPositionInRow.includes(mapByRow[0][colIndex]) : mergedPositionInCol.includes(mapByColumn[0][rowIndex]); const isHoveredOnFirstRowOrColumn = direction === 'column' ? hoveredCell.rowIndex === 0 && hasMergedCellsInFirstRowOrColumn : hoveredCell.colIndex === 0 && hasMergedCellsInFirstRowOrColumn; if (isHoveredOnFirstRowOrColumn) { const mergedSizes = direction === 'column' ? mapByRow[0].filter(el => el === mapByRow[0][colIndex]).length : mapByColumn[0].filter(el => el === mapByColumn[0][rowIndex]).length; const mergedSelection = hasMergedCellsInSelection(direction === 'column' ? [colIndex, colIndex + mergedSizes - 1] : [rowIndex, rowIndex + mergedSizes - 1], direction)(selection); return mergedSelection; } } return hasMergedCellsInSelection(indexes, direction)(selection); }, [indexes, selection, direction, hoveredCell]); const handleIconProps = { forceDefaultHandle, isHandleHovered: isHovered, hasMergedCells }; useEffect(() => { const dragHandleDivRefCurrent = dragHandleDivRef.current; const browser = getBrowserInfo(); if (dragHandleDivRefCurrent) { return draggable({ element: dragHandleDivRefCurrent, canDrag: () => { return !hasMergedCells; }, getInitialData() { return { localId: tableLocalId, type: `table-${direction}`, indexes }; }, onGenerateDragPreview: ({ nativeSetDragImage }) => { setCustomNativeDragPreview({ getOffset: ({ container }) => { const rect = container.getBoundingClientRect(); if (browser.safari) { // See: https://product-fabric.atlassian.net/browse/ED-21442 // We need to ensure that the preview is not overlaying screen content when the snapshot is taken, otherwise // safari will composite the screen text elements into the bitmap snapshot. The container is a wrapper which is already // positioned fixed at top/left 0. // IMPORTANT: we must not exceed more then the width of the container off-screen otherwise not preview will // be generated. container.style.left = `-${rect.width - 0.0001}px`; } if (isRow) { return { x: 12, y: rect.height / 2 }; } else { return { x: rect.width / 2 + 4, y: 12 }; } }, render: function render({ container }) { setPreviewContainer(container); return () => setPreviewContainer(null); }, nativeSetDragImage }); } }); } }, [tableLocalId, direction, indexes, isRow, editorView.state.selection, hasMergedCells]); const showDragMenuAnchorId = isRow ? 'drag-handle-button-row' : 'drag-handle-button-column'; const browser = getBrowserInfo(); return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", { type: "button" // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766 , className: ClassName.DRAG_HANDLE_BUTTON_CLICKABLE_ZONE, "data-testid": "table-drag-handle-clickable-zone-button", style: { height: isRow ? // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values -- Ignored via go/DSP-18766 `calc(100% - ${dragTableInsertColumnButtonSize}px)` : `${"var(--ds-space-200, 16px)"}`, // 16px here because it's the size of drag handle button's large side width: isRow ? `${"var(--ds-space-200, 16px)"}` // 16px here because it's the size of drag handle button's large side : // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values -- Ignored via go/DSP-18766 `calc(100% - ${dragTableInsertColumnButtonSize}px)`, left: isRow ? `${"var(--ds-space-050, 4px)"}` : undefined, bottom: isColumn ? `${"var(--ds-space-0, 0px)"}` : undefined, alignSelf: isColumn ? 'none' : 'center', zIndex: isColumn ? '-1' : 'auto', // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop pointerEvents: 'auto' }, onMouseUp: e => { // should toggle menu if current drag menu open. // return focus to editor so copying table selections whilst still works, i cannot call e.preventDefault in a mousemove event as this stops dragstart events from firing // -> this is bad for a11y but is the current standard new copy/paste keyboard shortcuts should be introduced instead editorView.focus(); if (isDragMenuOpen) { toggleDragMenu && toggleDragMenu('mouse', e); } }, onClick: onClick, "aria-label": formatMessage(messages.dragHandleZone) }), /*#__PURE__*/React.createElement("button", { type: "button", id: isDragMenuTarget ? showDragMenuAnchorId : undefined // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766 , className: classnames(ClassName.DRAG_HANDLE_BUTTON_CONTAINER, appearance, { [ClassName.DRAG_HANDLE_DISABLED]: hasMergedCells }), ref: dragHandleDivRef, style: { transform: isColumn ? 'none' : 'rotate(90deg)', alignSelf: isColumn ? 'none' : 'center' }, "data-testid": "table-drag-handle-button", "aria-label": formatMessage(isRow ? messages.rowDragHandle : messages.columnDragHandle), "aria-expanded": isDragMenuOpen && isDragMenuTarget ? 'true' : 'false', "aria-haspopup": "menu", onMouseOver: e => { setIsHovered(true); onMouseOver && onMouseOver(e); }, onMouseOut: e => { setIsHovered(false); onMouseOut && onMouseOut(e); }, onFocus: expValEquals('platform_editor_table_a11y_eslint_fix', 'isEnabled', true) ? e => { onFocus && onFocus(e); } : undefined, onBlur: expValEquals('platform_editor_table_a11y_eslint_fix', 'isEnabled', true) ? e => { onBlur && onBlur(e); } : undefined, onMouseUp: e => { // return focus to editor so copying table selections whilst still works, i cannot call e.preventDefault in a mousemove event as this stops dragstart events from firing // -> this is bad for a11y but is the current standard new copy/paste keyboard shortcuts should be introduced instead editorView.focus(); toggleDragMenu && toggleDragMenu('mouse', e); }, onClick: onClick, onKeyDown: e => { if (e.key === 'Enter' || e.key === ' ') { toggleDragMenu && toggleDragMenu('keyboard'); } } }, appearance !== 'placeholder' ? // cannot block pointer events in Firefox as it breaks Dragging functionality browser.gecko ? /*#__PURE__*/ // Ignored via go/ees005 // eslint-disable-next-line react/jsx-props-no-spreading React.createElement(HandleIconComponent, handleIconProps) : /*#__PURE__*/ // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766 React.createElement("span", { style: { pointerEvents: 'none' } }, /*#__PURE__*/React.createElement(HandleIconComponent // Ignored via go/ees005 // eslint-disable-next-line react/jsx-props-no-spreading , handleIconProps)) : null), previewContainer && previewWidth !== undefined && previewHeight !== undefined && /*#__PURE__*/ReactDOM.createPortal( /*#__PURE__*/React.createElement(DragPreview, { direction: direction, width: previewWidth, height: previewHeight }), previewContainer)); }; export const DragHandle = injectIntl(DragHandleComponent);