@atlaskit/editor-plugin-table
Version:
Table plugin for the @atlaskit/editor
249 lines (247 loc) • 10.7 kB
JavaScript
/* 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);