@atlaskit/editor-plugin-table
Version:
Table plugin for the @atlaskit/editor
300 lines (295 loc) • 11.6 kB
JavaScript
import _extends from "@babel/runtime/helpers/extends";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import React, { Component } from 'react';
import { createPortal } from 'react-dom';
import { INPUT_METHOD } from '@atlaskit/editor-common/analytics';
import { tableMessages as messages } from '@atlaskit/editor-common/messages';
import { Popup } from '@atlaskit/editor-common/ui';
import { closestElement } from '@atlaskit/editor-common/utils';
import { akEditorTableNumberColumnWidth } from '@atlaskit/editor-shared-styles';
import { CellSelection } from '@atlaskit/editor-tables/cell-selection';
import { getSelectionRect, isTableSelected } from '@atlaskit/editor-tables/utils';
import { clearHoverSelection, hoverColumns, hoverRows } from '../../pm-plugins/commands';
import { deleteColumnsWithAnalytics, deleteRowsWithAnalytics } from '../../pm-plugins/commands/commands-with-analytics';
import { getPluginState as getTablePluginState } from '../../pm-plugins/plugin-factory';
import { getColumnDeleteButtonParams, getColumnsWidths } from '../../pm-plugins/utils/column-controls';
import { getRowDeleteButtonParams, getRowHeights } from '../../pm-plugins/utils/row-controls';
import { TableCssClassName as ClassName } from '../../types';
import { stickyRowZIndex } from '../consts';
import DeleteButton from './DeleteButton';
import getPopupOptions from './getPopUpOptions';
function getSelectionType(selection) {
if (!isTableSelected(selection) && selection instanceof CellSelection) {
if (selection.isRowSelection()) {
return 'row';
}
if (selection.isColSelection()) {
return 'column';
}
}
return;
}
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/react/no-class-components
class FloatingDeleteButton extends Component {
constructor(props) {
super(props);
_defineProperty(this, "wrapper", null);
_defineProperty(this, "updateWrapper", () => {
const tableWrapper = closestElement(this.props.tableRef, `.${ClassName.TABLE_NODE_WRAPPER}`);
if (tableWrapper) {
this.wrapper = tableWrapper;
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
this.wrapper.addEventListener('scroll', this.onWrapperScrolled);
this.setState({
scrollLeft: tableWrapper.scrollLeft
});
} else {
if (this.wrapper) {
// unsubscribe if we previously had one and it just went away
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
this.wrapper.removeEventListener('scroll', this.onWrapperScrolled);
// and reset scroll position
this.setState({
scrollLeft: 0
});
}
this.wrapper = null;
}
});
_defineProperty(this, "onWrapperScrolled", e => {
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
const wrapper = e.target;
this.setState({
scrollLeft: wrapper.scrollLeft
});
});
_defineProperty(this, "handleMouseEnter", () => {
const {
state,
dispatch
} = this.props.editorView;
switch (this.state.selectionType) {
case 'row':
{
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return hoverRows(this.state.indexes, true)(state, dispatch, this.props.editorView);
}
case 'column':
{
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return hoverColumns(this.state.indexes, true)(state, dispatch, this.props.editorView);
}
}
return false;
});
_defineProperty(this, "handleMouseLeave", () => {
const {
state,
dispatch
} = this.props.editorView;
return clearHoverSelection()(state, dispatch);
});
/**
*
*
* @private
* @memberof FloatingDeleteButton
*/
_defineProperty(this, "handleClick", event => {
event.preventDefault();
const {
editorAnalyticsAPI
} = this.props;
let {
state,
dispatch
} = this.props.editorView;
const {
pluginConfig: {
isHeaderRowRequired
}
} = getTablePluginState(state);
const rect = getSelectionRect(state.selection);
if (rect) {
switch (this.state.selectionType) {
case 'column':
{
deleteColumnsWithAnalytics(editorAnalyticsAPI, this.props.api)(INPUT_METHOD.BUTTON, rect)(state, dispatch, this.props.editorView);
return;
}
case 'row':
{
deleteRowsWithAnalytics(editorAnalyticsAPI)(INPUT_METHOD.BUTTON, rect, !!isHeaderRowRequired)(state, dispatch);
return;
}
}
}
({
state,
dispatch
} = this.props.editorView);
clearHoverSelection()(state, dispatch);
});
this.state = {
selectionType: undefined,
top: 0,
left: 0,
indexes: [],
scrollLeft: 0
};
}
shouldComponentUpdate(_, nextState) {
return this.state.selectionType !== nextState.selectionType || this.state.left !== nextState.left || this.state.top !== nextState.top || this.state.scrollLeft !== nextState.scrollLeft;
}
componentDidMount() {
this.updateWrapper();
}
componentDidUpdate() {
this.updateWrapper();
}
componentWillUnmount() {
if (this.wrapper) {
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners
this.wrapper.removeEventListener('scroll', this.onWrapperScrolled);
}
}
/**
* We derivate the button state from the properties passed.
* We do this in here because we need this information in different places
* and this prevent to do multiple width calculations in the same component.
*/
static getDerivedStateFromProps(nextProps, prevState) {
const selectionType = getSelectionType(nextProps.selection);
const inStickyMode = nextProps.stickyHeaders && nextProps.stickyHeaders.sticky;
const rect = getSelectionRect(nextProps.selection);
// only tie row delete to sticky header if it's the only thing
// in the selection, otherwise the row delete will hover around
// the rest of the selection
const firstRowInSelection = rect && rect.top === 0 && rect.bottom === 1;
const shouldStickyButton = inStickyMode && firstRowInSelection;
const stickyTop = nextProps.stickyHeaders ? nextProps.stickyHeaders.top + nextProps.stickyHeaders.padding : 0;
if (selectionType) {
switch (selectionType) {
case 'column':
{
// Calculate the button position and indexes for columns
const columnsWidths = getColumnsWidths(nextProps.editorView);
const deleteBtnParams = getColumnDeleteButtonParams(columnsWidths, nextProps.editorView.state.selection);
if (deleteBtnParams) {
return {
...deleteBtnParams,
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
top: inStickyMode ? nextProps.stickyHeaders.top : 0,
position: inStickyMode ? 'sticky' : undefined,
selectionType
};
}
return null;
}
case 'row':
{
// Calculate the button position and indexes for rows
if (nextProps.tableRef) {
const rowHeights = getRowHeights(nextProps.tableRef);
const offsetTop = inStickyMode ? -rowHeights[0] : 0;
const deleteBtnParams = getRowDeleteButtonParams(rowHeights, nextProps.editorView.state.selection, shouldStickyButton ? stickyTop : offsetTop);
if (deleteBtnParams) {
return {
...deleteBtnParams,
position: shouldStickyButton ? 'sticky' : undefined,
left: 0,
selectionType: selectionType
};
}
}
return null;
}
}
}
// Clean state if no type
if (prevState.selectionType !== selectionType) {
return {
selectionType: undefined,
top: 0,
left: 0,
indexes: []
};
}
// Do nothing if doesn't change anything
return null;
}
render() {
const {
mountPoint,
boundariesElement,
tableRef
} = this.props;
const {
selectionType
} = this.state;
if (!selectionType || !tableRef) {
return null;
}
const tableContainerWrapper = closestElement(tableRef, `.${ClassName.TABLE_CONTAINER}`);
const button = /*#__PURE__*/React.createElement(DeleteButton, {
removeLabel: selectionType === 'column' ? messages.removeColumns : messages.removeRows,
onClick: this.handleClick,
onMouseEnter: this.handleMouseEnter,
onMouseLeave: this.handleMouseLeave
});
const popupOpts = getPopupOptions({
left: this.state.left,
top: this.state.top,
selectionType: this.state.selectionType,
tableWrapper: this.wrapper
});
const mountTo = tableContainerWrapper || mountPoint;
if (this.state.position === 'sticky' && mountTo) {
const headerRow = tableRef.querySelector('tr.sticky');
if (headerRow) {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const rect = headerRow.getBoundingClientRect();
const calculatePosition = popupOpts.onPositionCalculated || (pos => pos);
const pos = calculatePosition({
left: this.state.left,
top: this.state.top
});
return /*#__PURE__*/createPortal( /*#__PURE__*/React.createElement("div", {
style: {
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
position: 'fixed',
// eslint-disable-next-line @atlaskit/design-system/ensure-design-token-usage/preview
top: pos.top,
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop, @atlaskit/ui-styling-standard/no-imported-style-values -- Ignored via go/DSP-18766
zIndex: stickyRowZIndex,
// eslint-disable-next-line @atlaskit/design-system/ensure-design-token-usage/preview
left: rect.left + (pos.left || 0) - (this.state.selectionType === 'column' ? this.state.scrollLeft : 0) - (
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values -- Ignored via go/DSP-18766
this.props.isNumberColumnEnabled ? akEditorTableNumberColumnWidth : 0)
}
}, button), mountTo);
}
}
return /*#__PURE__*/React.createElement(Popup, _extends({
target: tableRef,
mountTo: mountTo,
boundariesElement: tableContainerWrapper || boundariesElement,
scrollableElement: this.wrapper || undefined,
forcePlacement: true,
allowOutOfBounds: true
// Ignored via go/ees005
// eslint-disable-next-line react/jsx-props-no-spreading
}, popupOpts), button);
}
}
_defineProperty(FloatingDeleteButton, "displayName", 'FloatingDeleteButton');
export default FloatingDeleteButton;