@atlaskit/editor-plugin-card
Version:
Card plugin for @atlaskit/editor-core
400 lines (393 loc) • 14 kB
JavaScript
import _extends from "@babel/runtime/helpers/extends";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
/**
* @jsxRuntime classic
* @jsx jsx
*/
import React from 'react';
// eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled, @typescript-eslint/consistent-type-imports
import { jsx } from '@emotion/react';
import { calcColumnsFromPx, calcMediaPxWidth, calcPctFromPx, calcPxFromColumns, handleSides, imageAlignmentMap, Resizer, snapTo, wrappedLayouts, wrapperStyle } from '@atlaskit/editor-common/ui';
import { NodeSelection } from '@atlaskit/editor-prosemirror/state';
import { findParentNodeOfTypeClosestToPos, hasParentNodeOfType } from '@atlaskit/editor-prosemirror/utils';
import { akEditorBreakoutPadding, akEditorMediaResizeHandlerPadding, akEditorWideLayoutWidth, breakoutWideScaleRatio, DEFAULT_EMBED_CARD_HEIGHT, DEFAULT_EMBED_CARD_WIDTH } from '@atlaskit/editor-shared-styles';
import { fg } from '@atlaskit/platform-feature-flags';
import { embedHeaderHeight } from '@atlaskit/smart-card';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
// eslint-disable-next-line @repo/internal/react/no-class-components
export default class ResizableEmbedCard extends React.Component {
constructor(...args) {
super(...args);
_defineProperty(this, "state", {
offsetLeft: this.calcOffsetLeft()
});
_defineProperty(this, "calcNewSize", (newWidth, stop) => {
const {
layout,
view: {
state
}
} = this.props;
const newPct = calcPctFromPx(newWidth, this.props.lineLength) * 100;
this.setState({
resizedPctWidth: newPct
});
let newLayout = hasParentNodeOfType(state.schema.nodes.table)(state.selection) ? layout : this.calcUnwrappedLayout(newPct, newWidth);
if (newPct <= 100) {
if (this.wrappedLayout && (stop ? newPct !== 100 : true)) {
newLayout = layout;
}
return {
width: newPct,
layout: newLayout
};
} else {
return {
width: this.props.pctWidth || null,
layout: newLayout
};
}
});
_defineProperty(this, "calcUnwrappedLayout", (pct, width) => {
if (pct <= 100) {
return 'center';
}
if (width <= this.wideLayoutWidth) {
return 'wide';
}
return 'full-width';
});
_defineProperty(this, "calcColumnLeftOffset", () => {
const {
offsetLeft
} = this.state;
return this.insideInlineLike ? calcColumnsFromPx(offsetLeft, this.props.lineLength, this.props.gridSize) : 0;
});
_defineProperty(this, "calcPxWidth", useLayout => {
const {
layout,
pctWidth,
lineLength,
containerWidth,
fullWidthMode,
getPos,
view: {
state
}
} = this.props;
const {
resizedPctWidth
} = this.state;
const pos = typeof getPos === 'function' ? getPos() : undefined;
return calcMediaPxWidth({
origWidth: DEFAULT_EMBED_CARD_WIDTH,
origHeight: DEFAULT_EMBED_CARD_HEIGHT,
pctWidth,
state,
containerWidth: {
width: containerWidth,
lineLength
},
isFullWidthModeEnabled: fullWidthMode,
layout: useLayout || layout,
pos: pos,
resizedPctWidth
});
});
_defineProperty(this, "handleResizeStart", () => {
const {
view,
getPos
} = this.props;
if (typeof getPos === 'function') {
const pos = getPos();
if (typeof pos === 'number') {
const tr = view.state.tr.setSelection(NodeSelection.create(view.state.doc, pos));
tr.setMeta('scrollIntoView', false);
view.dispatch(tr);
}
}
});
_defineProperty(this, "highlights", (newWidth, snapPoints) => {
const snapWidth = snapTo(newWidth, snapPoints);
const {
layoutColumn,
table,
expand,
nestedExpand,
bodiedSyncBlock
} = this.props.view.state.schema.nodes;
// Hide resizing guideline when embed is nested
if (this.$pos && !!findParentNodeOfTypeClosestToPos(this.$pos, editorExperiment('platform_synced_block', true) ? [layoutColumn, table, expand, nestedExpand, bodiedSyncBlock] : [layoutColumn, table, expand, nestedExpand])) {
return [];
}
if (snapWidth > this.wideLayoutWidth) {
return ['full-width'];
}
const {
layout,
lineLength,
gridSize
} = this.props;
const columns = calcColumnsFromPx(snapWidth, lineLength, gridSize);
const columnWidth = Math.round(columns);
const highlight = [];
if (layout === 'wrap-left' || layout === 'align-start') {
highlight.push(0, columnWidth);
} else if (layout === 'wrap-right' || layout === 'align-end') {
highlight.push(gridSize, gridSize - columnWidth);
} else if (this.insideInlineLike) {
highlight.push(Math.round(columns + this.calcColumnLeftOffset()));
} else {
highlight.push(Math.floor((gridSize - columnWidth) / 2), Math.ceil((gridSize + columnWidth) / 2));
}
return highlight;
});
}
componentDidUpdate(prevProps) {
const offsetLeft = this.calcOffsetLeft();
if (offsetLeft !== this.state.offsetLeft && offsetLeft >= 0) {
this.setState({
offsetLeft
});
}
if (this.props.layout !== prevProps.layout) {
this.checkLayout(prevProps.layout, this.props.layout);
}
}
get wrappedLayout() {
return wrappedLayouts.indexOf(this.props.layout) > -1;
}
/**
* When returning to center layout from a wrapped/aligned layout, it might actually
* be wide or full-width
*/
checkLayout(oldLayout, newLayout) {
const {
resizedPctWidth
} = this.state;
if (wrappedLayouts.indexOf(oldLayout) > -1 && newLayout === 'center' && resizedPctWidth) {
const layout = this.calcUnwrappedLayout(resizedPctWidth, this.calcPxWidth(newLayout));
this.props.updateSize(resizedPctWidth, layout);
}
}
get $pos() {
if (typeof this.props.getPos !== 'function') {
return null;
}
const pos = this.props.getPos();
if (Number.isNaN(pos) || typeof pos !== 'number') {
return null;
}
// need to pass view because we may not get updated props in time
return this.props.view.state.doc.resolve(pos);
}
/**
* The maxmimum number of grid columns this node can resize to.
*/
get gridWidth() {
const {
gridSize
} = this.props;
return !(this.wrappedLayout || this.insideInlineLike) ? gridSize / 2 : gridSize;
}
calcOffsetLeft() {
let offsetLeft = 0;
if (this.wrapper && this.insideInlineLike) {
const currentNode = this.wrapper;
const boundingRect = currentNode.getBoundingClientRect();
const pmRect = this.props.view.dom.getBoundingClientRect();
offsetLeft = boundingRect.left - pmRect.left;
}
return offsetLeft;
}
get wideLayoutWidth() {
const {
lineLength
} = this.props;
if (lineLength) {
return Math.ceil(lineLength * breakoutWideScaleRatio);
} else {
return akEditorWideLayoutWidth;
}
}
// check if is inside of a table
isNestedInTable() {
const {
table
} = this.props.view.state.schema.nodes;
if (!this.$pos) {
return false;
}
return !!findParentNodeOfTypeClosestToPos(this.$pos, table);
}
calcSnapPoints() {
const {
offsetLeft
} = this.state;
const {
containerWidth,
lineLength
} = this.props;
const snapTargets = [];
for (let i = 0; i < this.gridWidth; i++) {
snapTargets.push(calcPxFromColumns(i, lineLength, this.gridWidth) - offsetLeft);
}
// full width
snapTargets.push(lineLength - offsetLeft);
const minimumWidth = calcPxFromColumns(this.wrappedLayout || this.insideInlineLike ? 1 : 2, lineLength, this.props.gridSize);
const snapPoints = snapTargets.filter(width => width >= minimumWidth);
const $pos = this.$pos;
if (!$pos) {
return snapPoints;
}
const isTopLevel = $pos.parent.type.name === 'doc';
if (isTopLevel) {
snapPoints.push(this.wideLayoutWidth);
const fullWidthPoint = containerWidth - akEditorBreakoutPadding;
if (fullWidthPoint > this.wideLayoutWidth) {
snapPoints.push(fullWidthPoint);
}
}
return snapPoints;
}
get insideInlineLike() {
const $pos = this.$pos;
if (!$pos) {
return false;
}
const {
listItem
} = this.props.view.state.schema.nodes;
return !!findParentNodeOfTypeClosestToPos($pos, [listItem]);
}
/**
* Previously height of the box was controlled with paddingTop/paddingBottom trick inside Wrapper.
* It allowed height to be defined by a given percent ratio and so absolute value was defined by actual width.
* Also, it was part of styled component, which was fine because it was static through out life time of component.
*
* Now, two things changed:
* 1. If `height` is present we take it as actual height of the box, and hence we don't need
* (or even can't have, due to lack of width value) paddingTop trick.
* 2. Since `height` can be changing through out life time of a component, we can't have it as part of styled component,
* and hence we use `style` prop.
*/
getHeightDefiningComponent() {
const {
height,
aspectRatio
} = this.props;
let heightDefiningStyles;
if (height) {
heightDefiningStyles = {
height: `${height}px`
};
} else {
// paddingBottom css trick defines ratio of `iframe height (y) + header (32)` to `width (x)`,
// where is `aspectRatio` defines iframe aspectRatio alone
// So, visually:
//
// x
// ┌──────────┐
// │ header │ 32
// ├──────────┤
// │ │
// │ iframe │ y
// │ │
// └──────────┘
//
// aspectRatio = x / y
// paddingBottom = (y + 32) / x
// which can be achieved with css calc() as (1 / (x/y)) * 100)% + 32px
heightDefiningStyles = {
paddingBottom: `calc(${(1 / aspectRatio * 100).toFixed(3)}% + ${embedHeaderHeight}px)`
};
}
return jsx("span", {
"data-testid": 'resizable-embed-card-height-definer',
style: {
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
display: 'block',
/* Fixes extra padding problem in Firefox */
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop, @atlaskit/design-system/use-tokens-typography -- Ignored via go/DSP-18766
fontSize: 0,
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop, @atlaskit/design-system/use-tokens-typography -- Ignored via go/DSP-18766
lineHeight: 0,
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
...heightDefiningStyles
}
});
}
render() {
const {
layout,
pctWidth,
containerWidth,
fullWidthMode,
isResizeDisabled,
children
} = this.props;
const resizerProps = {
width: this.calcPxWidth(),
innerPadding: akEditorMediaResizeHandlerPadding
};
const enable = {};
handleSides.forEach(side => {
if (isResizeDisabled) {
enable[side] = false;
return;
}
const oppositeSide = side === 'left' ? 'right' : 'left';
enable[side] = ['full-width', 'wide', 'center'].concat(`wrap-${oppositeSide}`).concat(`align-${imageAlignmentMap[oppositeSide]}`).indexOf(layout) > -1;
if (side === 'left' && this.insideInlineLike) {
enable[side] = false;
}
});
const nestedInTableHandleStyles = isNestedInTable => {
if (!isNestedInTable) {
return;
}
return {
left: {
left: `calc(${"var(--ds-space-negative-025, -2px)"} * 0.5)`,
paddingLeft: '0px'
},
right: {
right: `calc(${"var(--ds-space-negative-025, -2px)"} * 0.5)`,
paddingRight: '0px'
}
};
};
/* eslint-disable @atlaskit/design-system/consistent-css-prop-usage */
return jsx("div", {
"data-testid": "resizable-embed-card-spacing"
}, jsx("div", {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values -- Ignored via go/DSP-18766
css: wrapperStyle({
layout,
isResized: !!pctWidth,
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values -- Ignored via go/DSP-18766
containerWidth: containerWidth || DEFAULT_EMBED_CARD_WIDTH,
fullWidthMode
})
}, jsx(Resizer
// Ignored via go/ees005
// eslint-disable-next-line react/jsx-props-no-spreading
, _extends({}, this.props, {
enable: enable,
calcNewSize: this.calcNewSize,
snapPoints: this.calcSnapPoints(),
scaleFactor: !this.wrappedLayout && !this.insideInlineLike ? 2 : 1,
highlights: this.highlights,
nodeType: "embed",
onResizeStart: editorExperiment('platform_synced_block', true) && fg('platform_synced_block_patch_9') ? this.handleResizeStart : undefined,
handleStyles: nestedInTableHandleStyles(this.isNestedInTable())
// Ignored via go/ees005
// eslint-disable-next-line react/jsx-props-no-spreading
}, resizerProps), children, this.getHeightDefiningComponent())));
/* eslint-enable @atlaskit/design-system/consistent-css-prop-usage */
}
}
_defineProperty(ResizableEmbedCard, "defaultProps", {
aspectRatio: DEFAULT_EMBED_CARD_WIDTH / DEFAULT_EMBED_CARD_HEIGHT
});