@atlaskit/editor-plugin-media
Version:
Media plugin for @atlaskit/editor-core
375 lines (369 loc) • 12.9 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 { calculateOffsetLeft } from '@atlaskit/editor-common/media-single';
import { calcColumnsFromPx, calcMediaPxWidth, calcPctFromPx, handleSides, imageAlignmentMap, Resizer, snapTo, wrappedLayouts } from '@atlaskit/editor-common/ui';
import { calculateSnapPoints } from '@atlaskit/editor-common/utils';
import { findParentNodeOfTypeClosestToPos, hasParentNodeOfType } from '@atlaskit/editor-prosemirror/utils';
import { akEditorWideLayoutWidth } from '@atlaskit/editor-shared-styles';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { checkMediaType } from '../../pm-plugins/utils/check-media-type';
import { wrapperStyle } from './styled';
// eslint-disable-next-line @repo/internal/react/no-class-components
export default class ResizableMediaSingle extends React.Component {
constructor(...args) {
super(...args);
_defineProperty(this, "hasResized", false);
_defineProperty(this, "state", {
offsetLeft: calculateOffsetLeft(this.insideInlineLike, this.insideLayout, this.props.view.dom, undefined),
isVideoFile: false
});
_defineProperty(this, "displayGrid", (visible, gridType, highlight) => {
var _pluginInjectionApi$g, _pluginInjectionApi$g2;
const {
pluginInjectionApi,
view
} = this.props;
pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$g = pluginInjectionApi.grid) === null || _pluginInjectionApi$g === void 0 ? void 0 : (_pluginInjectionApi$g2 = _pluginInjectionApi$g.actions) === null || _pluginInjectionApi$g2 === void 0 ? void 0 : _pluginInjectionApi$g2.displayGrid(view)({
visible,
gridType,
highlight: highlight
});
});
_defineProperty(this, "calcNewSize", (newWidth, stop) => {
const {
layout,
view: {
state
}
} = this.props;
if (!this.hasResized && expValEquals('platform_editor_media_vc_fixes', 'isEnabled', true)) {
var _this$wrapper;
const mediaDomEl = (_this$wrapper = this.wrapper) === null || _this$wrapper === void 0 ? void 0 : _this$wrapper.querySelector('div[data-prosemirror-node-name="media"]');
if (mediaDomEl) {
const event = new CustomEvent('resized');
mediaDomEl === null || mediaDomEl === void 0 ? void 0 : mediaDomEl.dispatchEvent(event);
}
this.hasResized = true;
}
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 <= akEditorWideLayoutWidth) {
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 {
width: origWidth = 0,
height: origHeight,
layout,
pctWidth,
lineLength,
containerWidth,
fullWidthMode,
getPos,
view: {
state
}
} = this.props;
const {
resizedPctWidth
} = this.state;
const pos = typeof getPos === 'function' ? getPos() : undefined;
return calcMediaPxWidth({
origWidth,
origHeight,
pctWidth,
state,
containerWidth: {
width: containerWidth,
lineLength
},
isFullWidthModeEnabled: fullWidthMode,
layout: useLayout || layout,
pos,
resizedPctWidth
});
});
_defineProperty(this, "highlights", (newWidth, snapPoints) => {
const snapWidth = snapTo(newWidth, snapPoints);
const {
layoutColumn,
table,
expand,
nestedExpand,
panel
} = this.props.view.state.schema.nodes;
if (this.$pos && !!findParentNodeOfTypeClosestToPos(this.$pos, [layoutColumn, table, expand, nestedExpand, panel].filter(Boolean))) {
return [];
}
if (snapWidth > akEditorWideLayoutWidth) {
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;
});
_defineProperty(this, "saveWrapper", wrapper => this.wrapper = wrapper);
}
componentDidUpdate(prevProps) {
const offsetLeft = calculateOffsetLeft(this.insideInlineLike, this.insideLayout, this.props.view.dom, this.wrapper);
if (offsetLeft !== this.state.offsetLeft && offsetLeft >= 0) {
this.setState({
offsetLeft
});
}
// Handle undo, when the actual pctWidth changed,
// we sync up with the internal state.
if (prevProps.pctWidth !== this.props.pctWidth) {
this.setState({
resizedPctWidth: this.props.pctWidth
});
}
return true;
}
get wrappedLayout() {
return wrappedLayouts.indexOf(this.props.layout) > -1;
}
// 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);
}
async componentDidMount() {
const {
viewMediaClientConfig
} = this.props;
if (viewMediaClientConfig) {
await this.checkVideoFile(viewMediaClientConfig);
}
}
UNSAFE_componentWillReceiveProps(nextProps) {
if (this.props.viewMediaClientConfig !== nextProps.viewMediaClientConfig) {
this.checkVideoFile(nextProps.viewMediaClientConfig);
}
if (this.props.layout !== nextProps.layout) {
this.checkLayout(this.props.layout, nextProps.layout);
}
}
async checkVideoFile(viewMediaClientConfig) {
const $pos = this.$pos;
if (!$pos || !viewMediaClientConfig) {
return;
}
const mediaNode = this.props.view.state.doc.nodeAt($pos.pos + 1);
const mediaType = mediaNode ? await checkMediaType(mediaNode, viewMediaClientConfig) : undefined;
const isVideoFile = mediaType !== 'external' && mediaType !== 'image';
if (this.state.isVideoFile !== isVideoFile) {
this.setState({
isVideoFile
});
}
}
/**
* 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();
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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;
}
get insideInlineLike() {
const $pos = this.$pos;
if (!$pos) {
return false;
}
const {
listItem
} = this.props.view.state.schema.nodes;
return !!findParentNodeOfTypeClosestToPos($pos, [listItem]);
}
get insideLayout() {
const $pos = this.$pos;
if (!$pos) {
return false;
}
const {
layoutColumn
} = this.props.view.state.schema.nodes;
return !!findParentNodeOfTypeClosestToPos($pos, [layoutColumn]);
}
render() {
const {
width: origWidth,
height: origHeight,
layout,
pctWidth,
containerWidth,
fullWidthMode,
selected,
children
} = this.props;
const initialWidth = this.calcPxWidth(); // width with padding
let ratio;
if (origWidth) {
ratio = (origHeight / origWidth * 100).toFixed(3);
}
const enable = {};
handleSides.forEach(side => {
const oppositeSide = side === 'left' ? 'right' : 'left';
if (expValEquals('platform_editor_media_vc_fixes', 'isEnabled', true)) {
if (this.props.disableHandles) {
enable[side] = false;
return;
}
}
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 snapPointsProps = {
$pos: this.$pos,
akEditorWideLayoutWidth: akEditorWideLayoutWidth,
allowBreakoutSnapPoints: this.props.allowBreakoutSnapPoints,
containerWidth: this.props.containerWidth,
gridSize: this.props.gridSize,
gridWidth: this.gridWidth,
insideInlineLike: this.insideInlineLike,
insideLayout: this.insideLayout,
isVideoFile: this.state.isVideoFile,
lineLength: this.props.lineLength,
offsetLeft: this.state.offsetLeft,
wrappedLayout: this.wrappedLayout
};
const nestedInTableHandleStyles = isNestedInTable => {
if (!isNestedInTable) {
return;
}
return {
left: {
left: `calc(${"var(--ds-space-025, 2px)"} * -0.5)`,
paddingLeft: '0px'
},
right: {
right: `calc(${"var(--ds-space-025, 2px)"} * -0.5)`,
paddingRight: '0px'
}
};
};
return jsx("div", {
ref: this.saveWrapper
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/design-system/consistent-css-prop-usage -- Ignored via go/DSP-18766
,
css: wrapperStyle({
layout,
isResized: !!pctWidth,
containerWidth: containerWidth || origWidth,
fullWidthMode,
width: origWidth
})
}, jsx(Resizer
// Ignored via go/ees005
// eslint-disable-next-line react/jsx-props-no-spreading
, _extends({}, this.props, {
displayGrid: this.displayGrid,
ratio: ratio,
width: initialWidth,
selected: selected,
enable: enable,
calcNewSize: this.calcNewSize,
snapPoints: calculateSnapPoints(snapPointsProps),
scaleFactor: !this.wrappedLayout && !this.insideInlineLike ? 2 : 1,
highlights: this.highlights,
nodeType: "media",
dispatchAnalyticsEvent: this.props.dispatchAnalyticsEvent
// when cursor is located below a media with caption,
// press “Up“ key will result cursor focus on an invalid position, (on the resize handler)
// This workaround adds an empty div inside the resize handler to prevent the issue.
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
handleComponentFunc: () => jsx("div", {
contentEditable: false
}),
handleStyles: nestedInTableHandleStyles(this.isNestedInTable())
}), children));
}
}