@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
357 lines (328 loc) • 11.8 kB
JavaScript
/* eslint-disable @atlaskit/ui-styling-standard/use-compiled -- Pre-existing lint debt surfaced by this mechanical type-import-only PR. */
/**
* @jsxRuntime classic
* @jsx jsx
*/
import React from 'react';
// eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled, @typescript-eslint/consistent-type-imports -- Ignored via go/DSP-18766; jsx required at runtime for @jsxRuntime classic
import { css, jsx } from '@emotion/react';
import { akEditorDefaultLayoutWidth, akEditorFullPageMaxWidth, akEditorFullWidthLayoutWidth } from '@atlaskit/editor-shared-styles';
import { fg } from '@atlaskit/platform-feature-flags';
import { nonWrappedLayouts } from '../../utils';
import { calcBreakoutWidth, calcWideWidth } from '../../utils/breakout';
function float(layout) {
switch (layout) {
case 'wrap-right':
return 'right';
case 'wrap-left':
return 'left';
default:
return 'none';
}
}
function getWidthIfFullWidthMode(originalWidth, containerWidth, isInsideOfInlineExtension) {
if (isInsideOfInlineExtension) {
return originalWidth > akEditorFullWidthLayoutWidth ? `${Math.min(containerWidth, akEditorFullWidthLayoutWidth)}px` : `${originalWidth}px`;
}
return originalWidth > akEditorFullWidthLayoutWidth ? '100%' : `${originalWidth}px`;
}
function getWidthIfDefaultMode(originalWidth, containerWidth, isInsideOfInlineExtension) {
if (isInsideOfInlineExtension) {
return originalWidth > akEditorFullPageMaxWidth ? `${Math.min(containerWidth, akEditorDefaultLayoutWidth)}px` : `${originalWidth}px`;
}
return originalWidth > akEditorFullPageMaxWidth ? '100%' : `${originalWidth}px`;
}
/**
* Calculates the image width for non-resized images.
*
* If an image has not been resized using the pctWidth attribute,
* then an image in wide or full-width can not be wider than the image's
* original width.
* @param layout
* @param width
* @param containerWidth
* @param fullWidthMode
* @param isResized
* @param isInsideOfInlineExtension
* @example
*/
export function calcLegacyWidth(layout, width, containerWidth = 0, fullWidthMode, isResized, isInsideOfInlineExtension) {
switch (layout) {
case 'align-start':
case 'align-end':
case 'wrap-right':
case 'wrap-left':
return width > containerWidth / 2 ? 'calc(50% - 12px)' : `${width}px`;
case 'wide':
return isInsideOfInlineExtension ? calcWideWidth(containerWidth, Infinity, `${containerWidth}px`) : calcWideWidth(containerWidth);
case 'full-width':
return calcBreakoutWidth(layout, containerWidth);
default:
return isResized ? `${width}px` : fullWidthMode ? getWidthIfFullWidthMode(width, containerWidth, isInsideOfInlineExtension) : getWidthIfDefaultMode(width, containerWidth, isInsideOfInlineExtension);
}
}
/**
* Calculates the image width for non-resized images.
*
* If an image has not been resized using the pctWidth attribute,
* then an image in wide or full-width can not be wider than the image's
* original width.
* @param layout
* @param width
* @param containerWidth
* @param fullWidthMode
* @param isResized
* @example
*/
export function calcLegacyWidthForInline(layout, width, containerWidth = 0, fullWidthMode, isResized) {
switch (layout) {
case 'align-start':
case 'align-end':
case 'wrap-right':
case 'wrap-left':
return width > containerWidth / 2 ? 'calc(50% - 12px)' : `${width}px`;
case 'wide':
return calcWideWidth(containerWidth, Infinity, `${containerWidth}px`);
case 'full-width':
return calcBreakoutWidth(layout, containerWidth);
default:
return isResized ? `${width}px` : fullWidthMode ? getWidthIfFullWidthMode(width, containerWidth) : getWidthIfDefaultMode(width, containerWidth);
}
}
/**
* Calculates the image width for previously resized images.
*
* Wide and full-width images are always that size (960px and 100%); there is
* no distinction between max-width and width.
* @param layout
* @param width
* @param containerWidth
* @example
*/
export function calcResizedWidth(layout, width, containerWidth = 0
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) {
switch (layout) {
case 'wide':
return calcWideWidth(containerWidth);
case 'full-width':
return calcBreakoutWidth(layout, containerWidth);
default:
return `${width}px`;
}
}
function calcMargin(layout) {
switch (layout) {
case 'wrap-right':
return '12px auto 12px 12px';
case 'wrap-left':
return '12px 12px 12px auto';
default:
return '24px auto';
}
}
function calcMaxCssForPercentageTypeMedia(layout) {
switch (layout) {
case 'wide':
return `min(var(--ak-editor--breakout-wide-layout-width), var(--ak-editor-max-container-width))`;
case 'full-width':
return `min(var(--ak-editor--full-width-layout-width), var(--ak-editor-max-container-width))`;
default:
return 'var(--ak-editor-max-container-width)';
}
}
function isImageAligned(layout) {
switch (layout) {
case 'align-end':
return 'margin-right: 0';
case 'align-start':
return 'margin-left: 0';
default:
return '';
}
}
/**
* Reduces the given CSS width value to the next lowest even pixel value if the value is in px.
* This is to mitigate subpixel rendering issues of embedded smart links.
*
* @param widthValue CSS width value to be rounded
* @returns Reduced CSS width value where px value given, or otherwise the original value
* @example
*/
// widthValue could be a string in px, rem or percentage, e.g. "800px", "100%", etc.
export function roundToClosestEvenPxValue(widthValue) {
try {
if (widthValue.endsWith('px')) {
const pxWidth = parseInt(widthValue.slice(0, -2));
return `${pxWidth - pxWidth % 2}px`;
}
return widthValue;
} catch {
return widthValue;
}
}
/**
* Can't use `.attrs` to handle highly dynamic styles because we are still
* supporting `styled-components` v1.
* @param root0
* @param root0.containerWidth
* @param root0.fullWidthMode
* @param root0.isResized
* @param root0.layout
* @param root0.mediaSingleWidth
* @param root0.width
* @param root0.isExtendedResizeExperienceOn
* @param root0.isNestedNode
* @param root0.isInsideOfInlineExtension
* @param root0.isInRenderer
* @example
*/
export const MediaSingleDimensionHelper = ({
containerWidth = 0,
fullWidthMode,
isResized,
layout,
mediaSingleWidth,
width,
// original media width
isExtendedResizeExperienceOn,
isNestedNode = false,
isInsideOfInlineExtension = false,
isInRenderer = false
}) => {
const calculatedWidth = roundToClosestEvenPxValue(isExtendedResizeExperienceOn ? `${mediaSingleWidth || width}px` : mediaSingleWidth ? calcResizedWidth(layout, width || 0, containerWidth) : calcLegacyWidth(layout, width || 0, containerWidth, fullWidthMode, isResized, isInsideOfInlineExtension));
const cssMaxWidth = isExtendedResizeExperienceOn ? 'var(--ak-editor-max-container-width)' : calcMaxCssForPercentageTypeMedia(layout);
// jest warning: JSDOM version (22) doesn't support the new @container CSS rule
// eslint-disable-next-line @atlaskit/design-system/no-css-tagged-template-expression -- Needs manual remediation
return css`
/* For nested rich media items, set max-width to 100% */
tr &,
[data-layout-column] &,
[data-node-type='expand'] &,
[data-panel-type] &,
li &,
[data-prosemirror-node-name='bodiedSyncBlock'] &,
[data-prosemirror-node-name='syncBlock'] &,
[data-sync-block-renderer] &,
[data-bodied-sync-block] & {
max-width: 100%;
}
width: ${calculatedWidth};
${layout === 'full-width' &&
/* This causes issues for new experience where we don't strip layout attributes
when copying top-level node and pasting into a table/layout,
because full-width layout will remain, causing node to be edge-to-edge */
!isExtendedResizeExperienceOn && css({
minWidth: '100%'
})}
${isInRenderer ? css({
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-container-queries
'@container ak-renderer-wrapper (min-width: 1px)': {
maxWidth: '100cqw'
}
}) : `max-width: ${cssMaxWidth};`}
${isExtendedResizeExperienceOn && `&[class*='is-resizing'] {
.new-file-experience-wrapper {
box-shadow: none !important;
}
${!isNestedNode && nonWrappedLayouts.includes(layout) && `margin-left: 50%;
transform: translateX(-50%);`}
}`}
&:not(.is-resizing) {
transition: width 100ms ease-in;
}
float: ${float(layout)};
margin: ${calcMargin(layout)};
&[class*='not-resizing'] {
${isNestedNode ? /* Make nested node appear responsive when resizing table cell */
`max-width: 100%;` : nonWrappedLayouts.includes(layout) && `margin-left: 50%;
transform: translateX(-50%);`}
}
${isImageAligned(layout)};
`;
};
const RenderFallbackContainer = ({
hasFallbackContainer,
paddingBottom,
height
}) =>
// eslint-disable-next-line @atlaskit/design-system/no-css-tagged-template-expression -- Needs manual remediation
css`
${hasFallbackContainer ? `
&::after {
content: '';
display: block;
${height ? `height: ${height}px;` : paddingBottom ? `padding-bottom: ${paddingBottom};` : ''}
/* Fixes extra padding problem in Firefox */
font-size: 0;
line-height: 0;
}
` : ''}
`;
// eslint-disable-next-line @atlaskit/design-system/no-css-tagged-template-expression -- Needs manual remediation
export const mediaWrapperStyle = props => css`
position: relative;
${RenderFallbackContainer(props)}
/* Editor */
& > figure {
position: ${props.hasFallbackContainer ? 'absolute' : 'relative'};
height: 100%;
width: 100%;
}
/* Comments on media project adds comment badge as child of the media wrapper,
thus we need to exclude it so that style is applied to intended div */
& > div:not([data-media-badges='true']) {
position: ${props.hasFallbackContainer ? 'absolute' : 'relative'};
height: 100%;
width: 100%;
}
& * [data-mark-annotation-type='inlineComment'] {
width: 100%;
height: 100%;
}
&[data-node-type='embedCard'] > div {
width: 100%;
}
/* Renderer */
[data-node-type='media'] {
position: static !important;
height: auto !important;
> div {
position: absolute;
height: 100%;
}
}
`;
export const MediaWrapper = ({
children,
...rest
}) =>
// eslint-disable-next-line @atlaskit/design-system/consistent-css-prop-usage -- Ignored via go/DSP-18766
jsx("div", {
css: mediaWrapperStyle(rest)
}, children);
MediaWrapper.displayName = 'WrapperMediaSingle';
/*
There was an issue with a small, intermittent white gap appearing between the images due to a small pixel difference in browser rendering.
The solution implemented below was adapted from: https://stackoverflow.com/a/68885576
It suggests adding an absolute div on top which matches the width and height and setting the border on that div.
*/
export const MediaBorderGapFiller = ({
borderColor
}) => {
// [FEATURE FLAG: platform_editor_media_border_radius_fix]
// Fixes border radius to properly match image with 8px radius
// To clean up: remove conditional, keep only flag-on behavior ('8px')
const borderRadius = fg('platform_editor_media_border_radius_fix') ? "var(--ds-radius-large, 8px)" : '1px'; // OLD BEHAVIOR (to be removed when flag is cleaned up)
return jsx("div", {
style: {
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
position: 'absolute',
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
inset: '0px',
border: `0.5px solid ${borderColor}`,
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
borderRadius
}
});
};