UNPKG

@atlaskit/editor-plugin-table

Version:

Table plugin for the @atlaskit/editor

372 lines (366 loc) 16.6 kB
/* eslint-disable @atlaskit/design-system/prefer-primitives */ import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import { ACTION_SUBJECT, EVENT_TYPE, INPUT_METHOD, TABLE_ACTION } from '@atlaskit/editor-common/analytics'; import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks'; import { CellSelection } from '@atlaskit/editor-tables'; import { getSelectionRect } from '@atlaskit/editor-tables/utils'; import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { clearHoverSelection, toggleActiveTableMenu } from '../../../pm-plugins/commands'; import { toggleDragMenuWithAnalytics } from '../../../pm-plugins/drag-and-drop/commands-with-analytics'; import { getPluginState as getTablePluginState } from '../../../pm-plugins/plugin-factory'; import { getRowHeights, getRowsParams } from '../../../pm-plugins/utils/row-controls'; import { getSelectedRowIndexes, isRowSelectionWithMergedFirstColumn } from '../../../pm-plugins/utils/selection'; import { TableCssClassName as ClassName } from '../../../types'; import { dragRowControlsWidth, dropTargetExtendedWidth } from '../../consts'; import { DragHandle } from '../../DragHandle'; import RowDropTarget from '../RowDropTarget'; const getSelectedRows = selection => { if (!(selection instanceof CellSelection)) { return []; } if (expValEquals('platform_editor_table_menu_updates', 'isEnabled', true)) { // New behaviour: also treat a row selection that sits to the right of a merged first-column // cell as a full row selection. if (!selection.isRowSelection() && !isRowSelectionWithMergedFirstColumn(selection)) { return []; } const rect = getSelectionRect(selection); return rect ? getSelectedRowIndexes(rect) : []; } // Old behaviour: only standard row selections are recognised. if (selection.isRowSelection()) { const rect = getSelectionRect(selection); if (!rect) { return []; } return getSelectedRowIndexes(rect); } return []; }; export const DragControls = ({ tableRef, tableNode, tableWidth, hoveredCell, tableActive, editorView, isInDanger, isResizing, isTableHovered, hoverRows, selectRow, selectRows, updateCellHoverLocation, api, selection }) => { var _tableNode$attrs$loca, _tableNode$attrs; const [isDragging, setIsDragging] = useState(false); const rowHeights = getRowHeights(tableRef); const rowsParams = getRowsParams(rowHeights); const heights = rowHeights.map(height => `${height - 1}px`).join(' '); const selectedRowIndexes = getSelectedRows(selection !== null && selection !== void 0 ? selection : editorView.state.selection); const currentNodeLocalId = (_tableNode$attrs$loca = tableNode === null || tableNode === void 0 ? void 0 : (_tableNode$attrs = tableNode.attrs) === null || _tableNode$attrs === void 0 ? void 0 : _tableNode$attrs.localId) !== null && _tableNode$attrs$loca !== void 0 ? _tableNode$attrs$loca : ''; useEffect(() => { return monitorForElements({ canMonitor({ source }) { const { type, localId, indexes } = source.data; if (!indexes || !localId || type !== 'table-row') { return false; } const { tableNode } = getTablePluginState(editorView.state); // If the draggable localId is the same as the current selected table localId then we will allow the monitor // watch for changes return localId === (tableNode === null || tableNode === void 0 ? void 0 : tableNode.attrs.localId); }, onDragStart() { setIsDragging(true); }, onDrop() { setIsDragging(false); } }); }, [editorView]); const toggleDragMenuHandler = useCallback((trigger, event, handleIndex) => { var _api$analytics2; if (event !== null && event !== void 0 && event.shiftKey) { return; } // Use the clicked handle index because `hoveredCell` can point at a merged cell's first row. const rowIndex = handleIndex !== null && handleIndex !== void 0 ? handleIndex : hoveredCell === null || hoveredCell === void 0 ? void 0 : hoveredCell.rowIndex; if (expValEquals('platform_editor_table_menu_updates', 'isEnabled', true)) { if (rowIndex !== undefined && api) { const { activeTableMenu: currentActiveTableMenu } = getTablePluginState(editorView.state); const isSameActiveMenu = (currentActiveTableMenu === null || currentActiveTableMenu === void 0 ? void 0 : currentActiveTableMenu.type) === 'row' && currentActiveTableMenu.index === rowIndex; api.core.actions.execute(({ tr }) => { if (!isSameActiveMenu) { var _api$analytics, _api$analytics$action; (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : (_api$analytics$action = _api$analytics.actions) === null || _api$analytics$action === void 0 ? void 0 : _api$analytics$action.attachAnalyticsEvent({ action: TABLE_ACTION.DRAG_MENU_OPENED, actionSubject: ACTION_SUBJECT.TABLE, actionSubjectId: null, eventType: EVENT_TYPE.TRACK, attributes: { inputMethod: trigger === 'keyboard' ? INPUT_METHOD.KEYBOARD : INPUT_METHOD.MOUSE, direction: 'row' } })(tr); } toggleActiveTableMenu({ type: 'row', index: rowIndex, openedBy: trigger }, currentActiveTableMenu, api)({ tr }); return tr; }); } return; } toggleDragMenuWithAnalytics(api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions)(undefined, 'row', rowIndex, trigger)(editorView.state, editorView.dispatch); }, [editorView, hoveredCell === null || hoveredCell === void 0 ? void 0 : hoveredCell.rowIndex, api]); const rowIndex = hoveredCell === null || hoveredCell === void 0 ? void 0 : hoveredCell.rowIndex; const handleMouseOut = useCallback(() => { if (tableActive) { const { state, dispatch } = editorView; clearHoverSelection()(state, dispatch); } }, [editorView, tableActive]); const handleMouseMove = useCallback(e => { const target = e.nativeEvent.target instanceof Element ? e.nativeEvent.target : null; const isParentDragControls = target === null || target === void 0 ? void 0 : target.closest(`.${ClassName.DRAG_ROW_CONTROLS}`); const rowIndex = target === null || target === void 0 ? void 0 : target.getAttribute('data-start-index'); // avoid updating if event target is not related if (!isParentDragControls || !rowIndex) { return; } updateCellHoverLocation(Number(rowIndex)); }, [updateCellHoverLocation]); const rowIndexes = useMemo(() => { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return [rowIndex]; }, [rowIndex]); const handleMouseOver = useCallback(() => { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion hoverRows([rowIndex]); }, [hoverRows, rowIndex]); const handleClick = useCallback(e => { const isClickOutsideSelectedRows = // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion selectedRowIndexes.length >= 1 && !selectedRowIndexes.includes(rowIndex); if (!selectedRowIndexes || selectedRowIndexes.length === 0 || isClickOutsideSelectedRows) { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion selectRow(rowIndex, e.shiftKey); } // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion if (selectedRowIndexes.length > 1 && selectedRowIndexes.includes(rowIndex) && !e.shiftKey) { selectRows(selectedRowIndexes); } }, [rowIndex, selectRow, selectRows, selectedRowIndexes]); const generateHandleByType = (type, appearance, gridRow, indexes) => { const isHover = type === 'hover'; const previewHeight = rowHeights.reduce((sum, v, i) => sum + v * (indexes.includes(i) ? 1 : 0), 0); return /*#__PURE__*/React.createElement("div", { key: type, style: { gridRow, // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766 gridColumn: '2', // DragHandle uses `transform: rotate(90)`, which doesn't affect its parent (this div) causing the width of this element to be the true height of the drag handle // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766 display: 'flex', // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766 width: '9px', // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766 height: '100%', // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766 position: 'relative', // eslint-disable-next-line @atlaskit/design-system/ensure-design-token-usage/preview, @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766 right: '-0.5px', // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop pointerEvents: 'none' } // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop , className: expValEquals('platform_editor_table_sticky_header_improvements', 'cohort', 'test_with_overflow') ? ClassName.DRAG_ROW_FLOATING_DRAG_HANDLE : undefined, "data-testid": `table-floating-row-${isHover ? rowIndex : selectedRowIndexes[0]}-drag-handle`, "data-row-index": rowIndex, "data-selected-row-index": selectedRowIndexes[0], "data-handle-appearance": appearance }, /*#__PURE__*/React.createElement(DragHandle, { api: api, isDragMenuTarget: !isHover, direction: "row", tableLocalId: currentNodeLocalId, indexes: indexes, forceDefaultHandle: !isHover, previewWidth: tableWidth, previewHeight: previewHeight, appearance: appearance, hoveredCell: hoveredCell, onClick: handleClick, onMouseOver: handleMouseOver, onMouseOut: handleMouseOut, onBlur: expValEquals('platform_editor_table_a11y_eslint_fix', 'isEnabled', true) ? handleMouseOut : undefined, onFocus: expValEquals('platform_editor_table_a11y_eslint_fix', 'isEnabled', true) ? handleMouseOver : undefined, toggleDragMenu: toggleDragMenuHandler, editorView: editorView })); }; const rowHandles = () => { const handles = []; const isRowSelected = selectedRowIndexes.length > 0; const isEntireTableSelected = rowHeights.length > selectedRowIndexes.length; if (!tableActive) { return null; } const selectedAppearance = isRowSelected && isEntireTableSelected ? isInDanger ? 'danger' : 'selected' : 'placeholder'; // placeholder / selected need to always render at least one handle // so it can be focused via keyboard shortcuts const selectedGridRow = expValEquals('platform_editor_table_menu_updates', 'isEnabled', true) ? // New behaviour: always position the placeholder in the first row to avoid an invalid // `NaN / span 0` grid placement (which makes the handle disappear) when no rows are selected. selectedAppearance === 'placeholder' ? '1 / span 1' : `${selectedRowIndexes[0] + 1} / span ${selectedRowIndexes.length}` : // Old behaviour. `${selectedRowIndexes[0] + 1} / span ${selectedRowIndexes.length}`; handles.push(generateHandleByType('selected', selectedAppearance, selectedGridRow, selectedRowIndexes)); if (hoveredCell && isTableHovered && rowIndex !== undefined && !selectedRowIndexes.includes(rowIndex) && rowIndex < rowHeights.length) { handles.push(generateHandleByType('hover', 'default', `${rowIndex + 1} / span 1`, rowIndexes)); } return handles; }; if (isResizing) { return null; } return /*#__PURE__*/React.createElement("div", { // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766 className: ClassName.DRAG_ROW_CONTROLS, style: { gridTemplateRows: heights, gridTemplateColumns: isDragging ? // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values -- Ignored via go/DSP-18766 `${dropTargetExtendedWidth}px ${dragRowControlsWidth}px ${tableWidth}px` : // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values `0px ${dragRowControlsWidth}px 0px`, // eslint-disable-next-line @atlaskit/design-system/ensure-design-token-usage/preview left: isDragging ? // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values -- Ignored via go/DSP-18766 `-${dropTargetExtendedWidth + 2}px` : "var(--ds-space-negative-025, -2px)" }, onMouseMove: handleMouseMove, contentEditable: false }, rowsParams.map(({ startIndex, endIndex }, index) => /*#__PURE__*/ // Ignored via go/ees005 // eslint-disable-next-line react/no-array-index-key React.createElement(Fragment, { key: index }, /*#__PURE__*/React.createElement("div", { style: { gridRow: `${index + 1} / span 1`, // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766 gridColumn: '2' }, "data-start-index": startIndex, "data-end-index": endIndex // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766 , className: ClassName.DRAG_ROW_FLOATING_INSERT_DOT_WRAPPER, contentEditable: false // Ignored via go/ees005 // eslint-disable-next-line react/no-array-index-key , key: `insert-dot-${index}` }, /*#__PURE__*/React.createElement("div", { className: ClassName.DRAG_ROW_FLOATING_INSERT_DOT })), isDragging && /*#__PURE__*/React.createElement(RowDropTarget // Ignored via go/ees005 // eslint-disable-next-line react/no-array-index-key , { key: `drop-target-${index}`, index: index, localId: currentNodeLocalId // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , style: { // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766 gridColumn: '1 / span 3', gridRow: `${index + 1} / span 1`, // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766 height: '100%', // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766 pointerEvents: 'auto', // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766 position: 'relative', // eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766 left: "var(--ds-space-negative-100, -8px)" } }))), rowHandles()); }; export const DragControlsWithSelection = ({ editorView, tableRef, tableNode, tableWidth, tableActive, hoveredCell, isInDanger, isTableHovered, isResizing, hoverRows, selectRow, selectRows, updateCellHoverLocation, api }) => { const { selection } = useSharedPluginStateWithSelector(api, ['selection'], states => { var _states$selectionStat; return { selection: (_states$selectionStat = states.selectionState) === null || _states$selectionStat === void 0 ? void 0 : _states$selectionStat.selection }; }); return /*#__PURE__*/React.createElement(DragControls, { editorView: editorView, tableRef: tableRef, tableNode: tableNode, tableWidth: tableWidth, tableActive: tableActive, hoveredCell: hoveredCell, isInDanger: isInDanger, isTableHovered: isTableHovered, isResizing: isResizing, hoverRows: hoverRows, selectRow: selectRow, selectRows: selectRows, updateCellHoverLocation: updateCellHoverLocation, api: api, selection: selection }); };