@atlaskit/editor-plugin-table
Version:
Table plugin for the @atlaskit/editor
314 lines (311 loc) • 13.3 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-next';
import { browser as browserLegacy, getBrowserInfo } from '@atlaskit/editor-common/browser';
import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
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 { getPluginState } from '../../pm-plugins/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,
toggleDragMenu,
hoveredCell,
onClick,
editorView,
intl: {
formatMessage
},
hoveredColumns,
hoveredRows
}) => {
const dragHandleDivRef = useRef(null);
const [previewContainer, setPreviewContainer] = useState(null);
const {
state,
state: {
selection
}
} = editorView;
if (hoveredColumns === undefined || hoveredRows === undefined) {
const {
hoveredColumns: hoveredColumnsState,
hoveredRows: hoveredRowsState
} = getPluginState(state);
hoveredColumns = hoveredColumnsState;
hoveredRows = hoveredRowsState;
}
const {
isDragMenuOpen = false
} = getDnDPluginState(state);
const [isHovered, setIsHovered] = useState(false);
const isRow = direction === 'row';
const isColumn = direction === 'column';
// Added !isDragMenuOpen check so when hover 'Delete column/row' from drag menu
// the handle of the next column/row does not show the 'hovered' state icon
const isRowHandleHovered = isRow && hoveredRows.length > 0 && !isDragMenuOpen;
const isColumnHandleHovered = isColumn && hoveredColumns.length > 0 && !isDragMenuOpen;
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: expValEquals('platform_editor_table_drag_handle_hover', 'isEnabled', true) ? isHovered : isColumnHandleHovered || isRowHandleHovered,
hasMergedCells
};
useEffect(() => {
const dragHandleDivRefCurrent = dragHandleDivRef.current;
const browser = expValEquals('platform_editor_hydratable_ui', 'isEnabled', true) ? getBrowserInfo() : browserLegacy;
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 = expValEquals('platform_editor_hydratable_ui', 'isEnabled', true) ? getBrowserInfo() : browserLegacy;
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": expValEquals('platform_editor_enghealth_table_plugin_lable_rule', 'isEnabled', true) ? formatMessage(messages.dragHandleZone) : undefined
}), /*#__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"
// eslint-disable-next-line @atlassian/a11y/mouse-events-have-key-events
,
onMouseOver: e => {
if (expValEquals('platform_editor_table_drag_handle_hover', 'isEnabled', true)) {
setIsHovered(true);
}
onMouseOver && onMouseOver(e);
}
// eslint-disable-next-line @atlassian/a11y/mouse-events-have-key-events
,
onMouseOut: e => {
if (expValEquals('platform_editor_table_drag_handle_hover', 'isEnabled', true)) {
setIsHovered(false);
}
onMouseOut && onMouseOut(e);
},
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));
};
const DragHandleComponentWithSharedState = ({
isDragMenuTarget,
tableLocalId,
direction,
appearance,
indexes,
forceDefaultHandle,
previewHeight,
previewWidth,
onMouseOver,
onMouseOut,
toggleDragMenu,
hoveredCell,
onClick,
editorView,
intl,
api
}) => {
const {
hoveredColumns,
hoveredRows
} = useSharedPluginStateWithSelector(api, ['table'], states => {
var _states$tableState, _states$tableState2;
return {
hoveredColumns: (_states$tableState = states.tableState) === null || _states$tableState === void 0 ? void 0 : _states$tableState.hoveredColumns,
hoveredRows: (_states$tableState2 = states.tableState) === null || _states$tableState2 === void 0 ? void 0 : _states$tableState2.hoveredRows
};
});
return /*#__PURE__*/React.createElement(DragHandleComponent, {
isDragMenuTarget: isDragMenuTarget,
tableLocalId: tableLocalId,
direction: direction,
appearance: appearance,
indexes: indexes,
forceDefaultHandle: forceDefaultHandle,
previewWidth: previewWidth,
previewHeight: previewHeight,
onMouseOver: onMouseOver,
onMouseOut: onMouseOut,
toggleDragMenu: toggleDragMenu,
hoveredCell: hoveredCell,
onClick: onClick,
editorView: editorView,
intl: intl,
hoveredColumns: hoveredColumns,
hoveredRows: hoveredRows
});
};
export const DragHandle = injectIntl(DragHandleComponentWithSharedState);