@atlaskit/renderer
Version:
Renderer component
371 lines (366 loc) • 14.7 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _extends from "@babel/runtime/helpers/extends";
/**
* @jsxRuntime classic
* @jsx jsx
* @jsxFrag
*/
import React, { PureComponent, Fragment, useEffect, useState, useMemo } from 'react';
/* eslint-disable @typescript-eslint/consistent-type-imports, @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766; jsx required at runtime for @jsxRuntime classic */
import { jsx, css } from '@emotion/react';
import { AnalyticsContext } from '@atlaskit/analytics-next';
import { MEDIA_CONTEXT } from '@atlaskit/analytics-namespaced-context';
import { WithProviders } from '@atlaskit/editor-common/provider-factory';
import { MediaBorderGapFiller } from '@atlaskit/editor-common/ui';
import { fg } from '@atlaskit/platform-feature-flags';
import { MediaCard } from '../../../ui/MediaCard';
import { AnnotationMarkStates } from '@atlaskit/adf-schema';
import { hexToEditorBorderPaletteColor } from '@atlaskit/editor-palette';
import { getEventHandler } from '../../../utils';
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, VIEW_METHOD } from '@atlaskit/editor-common/analytics';
import { MODE, PLATFORM } from '../../../analytics/events';
import AnnotationComponent from '../../marks/annotation';
import { CommentBadgeNext, ExternalImageBadge, MediaBadges } from '@atlaskit/editor-common/media-single';
import { useInlineCommentsFilter } from '../../../ui/annotations/hooks/use-inline-comments-filter';
import { useInlineCommentSubscriberContext } from '../../../ui/annotations/hooks/use-inline-comment-subscriber';
import { AnnotationUpdateEvent } from '@atlaskit/editor-common/types';
import { useAnnotationRangeState } from '../../../ui/annotations/contexts/AnnotationRangeContext';
const linkStyle = css({
position: 'absolute',
background: 'transparent',
top: 0,
right: 0,
bottom: 0,
left: 0,
cursor: 'pointer',
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-important-styles
width: '100% !important',
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-important-styles
height: '100% !important'
});
const borderStyle = css({
position: 'absolute',
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-important-styles
width: '100% !important',
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-important-styles
height: '100% !important'
});
const MediaBorder = ({
mark,
children
}) => {
var _mark$attrs$color, _mark$attrs$size;
if (!mark) {
return jsx(Fragment, null, children);
}
const borderColor = (_mark$attrs$color = mark === null || mark === void 0 ? void 0 : mark.attrs.color) !== null && _mark$attrs$color !== void 0 ? _mark$attrs$color : '';
const borderWidth = (_mark$attrs$size = mark === null || mark === void 0 ? void 0 : mark.attrs.size) !== null && _mark$attrs$size !== void 0 ? _mark$attrs$size : 0;
const paletteColorValue = hexToEditorBorderPaletteColor(borderColor) || borderColor;
// [FEATURE FLAG: platform_editor_media_border_radius_fix]
// Fixes border radius to properly match image with 8px radius
// To clean up: keep only flag-on behavior ('8px')
const borderRadius = fg('platform_editor_media_border_radius_fix') ? "var(--ds-radius-large, 8px)" : `${borderWidth}px`; // OLD BEHAVIOR (to be removed when flag is cleaned up)
return jsx("div", {
"data-mark-type": "border",
"data-color": borderColor,
"data-size": borderWidth,
css: borderStyle,
style: {
// eslint-disable-next-line @atlaskit/ui-styling-standard/enforce-style-prop -- Ignored via go/DSP-18766
borderRadius,
boxShadow: `0 0 0 ${borderWidth}px ${paletteColorValue}`
}
}, jsx(MediaBorderGapFiller, {
borderColor: borderColor
}), children);
};
const MediaLink = ({
mark,
children,
onClick
}) => {
if (!mark) {
return jsx(Fragment, null, children);
}
const linkHref = mark === null || mark === void 0 ? void 0 : mark.attrs.href;
return (
// eslint-disable-next-line @atlaskit/design-system/no-html-anchor
jsx("a", {
href: linkHref,
rel: "noreferrer noopener",
onClick: onClick,
"data-block-link": linkHref,
css: linkStyle
}, children)
);
};
const MediaAnnotation = ({
mark,
children
}) => {
if (!mark) {
return jsx(Fragment, null, children);
}
return jsx(AnnotationComponent, {
id: mark.attrs.id,
annotationType: mark.attrs.annotationType
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
dataAttributes: {
'data-renderer-mark': true,
'data-block-mark': true
}
// This should be fine being empty [] since the serializer serializeFragmentChild getMarkProps call always passes
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
annotationParentIds: [],
allowAnnotations: true,
useBlockLevel: true
}, children);
};
const MediaAnnotations = ({
marks = [],
children
}) => {
// Early Exit
if (marks.length === 0) {
return jsx(Fragment, null, children);
}
// Recursive marks
const currentMark = marks[0];
const otherMarks = marks.slice(1);
return jsx(Fragment, null, jsx(MediaAnnotation, {
key: currentMark.attrs.id,
mark: currentMark
}, otherMarks.length ? jsx(MediaAnnotations, {
marks: otherMarks
}, children) : jsx(Fragment, null, children)));
};
const CommentBadgeWrapper = ({
marks,
mediaSingleElement,
isDrafting = false,
...rest
}) => {
var _marks$map;
const [status, setStatus] = useState('default');
const [entered, setEntered] = useState(false);
const updateSubscriber = useInlineCommentSubscriberContext();
const activeParentIds = useInlineCommentsFilter({
annotationIds: (_marks$map = marks === null || marks === void 0 ? void 0 : marks.map(mark => mark.attrs.id)) !== null && _marks$map !== void 0 ? _marks$map : [''],
filter: {
state: AnnotationMarkStates.ACTIVE
}
});
useEffect(() => {
const observer = new MutationObserver(mutationList => {
mutationList.forEach(mutation => {
const parentNode = mutation.target.parentNode;
if (mutation.attributeName === 'data-has-focus') {
const isMediaCaption = parentNode === null || parentNode === void 0 ? void 0 : parentNode.closest('[data-media-caption="true"]');
const elementHasFocus = (parentNode === null || parentNode === void 0 ? void 0 : parentNode.querySelector('[data-has-focus="true"]')) && !isMediaCaption;
elementHasFocus ? setStatus('active') : setStatus('default');
}
});
});
if (mediaSingleElement) {
observer.observe(mediaSingleElement, {
attributes: true,
subtree: true,
attributeFilter: ['data-has-focus']
});
}
return () => {
observer.disconnect();
};
}, [mediaSingleElement, setStatus]);
if (!isDrafting && !activeParentIds.length) {
return null;
}
const onClick = e => {
e.preventDefault();
if (updateSubscriber) {
updateSubscriber.emit(AnnotationUpdateEvent.ON_ANNOTATION_CLICK, {
annotationIds: activeParentIds,
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
eventTarget: e.target,
// use mediaSingle here to align with annotation viewed event dispatched in editor
eventTargetType: 'mediaSingle',
viewMethod: VIEW_METHOD.BADGE
});
}
};
return jsx(CommentBadgeNext
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
, _extends({
onMouseEnter: () => setEntered(true)
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
onMouseLeave: () => setEntered(false),
status: entered ? 'entered' : status,
onClick: onClick,
mediaSingleElement: mediaSingleElement
// Ignored via go/ees005
// eslint-disable-next-line react/jsx-props-no-spreading
}, rest));
};
// Ignored via go/ees005
// eslint-disable-next-line @repo/internal/react/no-class-components
class Media extends PureComponent {
constructor(props) {
super(props);
_defineProperty(this, "renderCard", (providers = {}) => {
const {
contextIdentifierProvider
} = providers;
const {
allowAltTextOnImages,
alt,
featureFlags,
shouldOpenMediaViewer: allowMediaViewer,
enableDownloadButton,
ssr,
width,
height,
mediaSingleElement,
isDrafting = false
} = this.props;
const annotationMarks = this.props.isAnnotationMark ? this.props.marks.filter(this.props.isAnnotationMark) : undefined;
const borderMark = this.props.marks.find(this.props.isBorderMark);
const linkMark = this.props.marks.find(this.props.isLinkMark);
const linkHref = linkMark === null || linkMark === void 0 ? void 0 : linkMark.attrs.href;
const eventHandlers = linkHref ? undefined : this.props.eventHandlers;
const shouldOpenMediaViewer = !linkHref && allowMediaViewer;
const isInPageInclude = mediaSingleElement === null || mediaSingleElement === void 0 ? void 0 : mediaSingleElement.closest('[data-node-type="include"]');
const isIncludeExcerpt = !!(mediaSingleElement !== null && mediaSingleElement !== void 0 && mediaSingleElement.closest('.ak-excerpt-include'));
const showCommentBadge = !!annotationMarks && !isInPageInclude && !isIncludeExcerpt;
return jsx(MediaLink, {
mark: linkMark,
onClick: this.handleMediaLinkClickFn
}, jsx(MediaAnnotations, {
marks: annotationMarks
}, jsx(MediaBorder, {
mark: borderMark
}, jsx(AnalyticsContext
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
, {
data: {
[MEDIA_CONTEXT]: {
border: !!borderMark
}
}
}, jsx(MediaBadges, {
mediaElement: mediaSingleElement,
mediaWidth: width,
mediaHeight: height,
useMinimumZIndex: true
}, ({
visible
}) => jsx(React.Fragment, null, visible && jsx(ExternalImageBadge, {
type: this.props.type,
url: this.props.type === 'external' ? this.props.url : undefined
}), showCommentBadge && jsx(CommentBadgeWrapper, {
marks: annotationMarks,
mediaSingleElement: mediaSingleElement,
isDrafting: isDrafting
}))), jsx(MediaCard, _extends({
contextIdentifierProvider: contextIdentifierProvider
// Ignored via go/ees005
// eslint-disable-next-line react/jsx-props-no-spreading
}, this.props, {
shouldOpenMediaViewer: shouldOpenMediaViewer,
eventHandlers: eventHandlers,
alt: allowAltTextOnImages ? alt : undefined,
featureFlags: featureFlags,
shouldEnableDownloadButton: enableDownloadButton,
ssr: ssr
}))))));
});
_defineProperty(this, "handleMediaLinkClick", event => {
const {
fireAnalyticsEvent,
isLinkMark,
marks
} = this.props;
if (fireAnalyticsEvent) {
fireAnalyticsEvent({
action: ACTION.VISITED,
actionSubject: ACTION_SUBJECT.MEDIA,
actionSubjectId: ACTION_SUBJECT_ID.LINK,
eventType: EVENT_TYPE.TRACK,
attributes: {
platform: PLATFORM.WEB,
mode: MODE.RENDERER
}
});
}
const linkMark = this.props.marks.find(this.props.isLinkMark);
const linkHref = linkMark === null || linkMark === void 0 ? void 0 : linkMark.attrs.href;
const handler = getEventHandler(this.props.eventHandlers, 'link');
if (handler) {
const linkMark = marks.find(isLinkMark);
handler(event, linkMark && linkHref);
}
});
this.handleMediaLinkClickFn = this.handleMediaLinkClick.bind(this);
}
render() {
const {
providers
} = this.props;
if (!providers) {
return this.renderCard();
}
return jsx(WithProviders
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
, {
providers: ['mediaProvider', 'contextIdentifierProvider'],
providerFactory: providers,
renderNode: this.renderCard
});
}
}
const MediaWithDraftAnnotation = props => {
const {
hoverDraftDocumentPosition: draftPosition
} = useAnnotationRangeState();
const {
dataAttributes
} = props;
const pos = dataAttributes && dataAttributes['data-renderer-start-pos'];
const [position, setPosition] = useState();
const [shouldApplyDraftAnnotation, setShouldApplyDraftAnnotation] = useState(false);
useEffect(() => {
var _draftPosition$from;
if (pos === undefined) {
return;
}
const posToCheck = ((_draftPosition$from = draftPosition === null || draftPosition === void 0 ? void 0 : draftPosition.from) !== null && _draftPosition$from !== void 0 ? _draftPosition$from : 0) + 1;
if (draftPosition !== null && posToCheck === pos) {
// eslint-disable-next-line @atlassian/perf-linting/no-chain-state-updates -- Ignored via go/ees017 (to be fixed)
setShouldApplyDraftAnnotation(true);
// eslint-disable-next-line @atlassian/perf-linting/no-chain-state-updates -- Ignored via go/ees017 (to be fixed)
setPosition(posToCheck);
} else if (draftPosition === null && shouldApplyDraftAnnotation) {
// eslint-disable-next-line @atlassian/perf-linting/no-chain-state-updates -- Ignored via go/ees017 (to be fixed)
setShouldApplyDraftAnnotation(false);
// eslint-disable-next-line @atlassian/perf-linting/no-chain-state-updates -- Ignored via go/ees017 (to be fixed)
setPosition(undefined);
}
}, [draftPosition, pos, shouldApplyDraftAnnotation]);
const applyDraftAnnotation = props.allowAnnotationsDraftMode && shouldApplyDraftAnnotation && position !== undefined;
const dataAttributesWithDraftAnnotation = useMemo(() => applyDraftAnnotation ? {
...dataAttributes,
'data-annotation-draft-mark': true,
'data-renderer-mark': true
} : dataAttributes, [applyDraftAnnotation, dataAttributes]);
return jsx(Media
// Ignored via go/ees005
// eslint-disable-next-line react/jsx-props-no-spreading
, _extends({}, props, {
dataAttributes: dataAttributesWithDraftAnnotation,
isDrafting: shouldApplyDraftAnnotation
}));
};
export default MediaWithDraftAnnotation;