@atlaskit/editor-plugin-media 1.36.5 → 1.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,249 @@
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { injectIntl } from 'react-intl-next';
3
+ import { usePreviousState } from '@atlaskit/editor-common/hooks';
4
+ import { nodeViewsMessages as messages } from '@atlaskit/editor-common/media';
5
+ import { isNodeSelectedOrInRange, SelectedState, setNodeSelection } from '@atlaskit/editor-common/utils';
6
+ import EditorCloseIcon from '@atlaskit/icon/glyph/editor/close';
7
+ import { getMediaFeatureFlag } from '@atlaskit/media-common';
8
+ import { Filmstrip } from '@atlaskit/media-filmstrip';
9
+ import { stateKey as mediaStateKey } from '../pm-plugins/plugin-key';
10
+ import { createMediaNodeUpdater } from './mediaNodeUpdater';
11
+ const getIdentifier = item => {
12
+ if (item.attrs.type === 'external') {
13
+ return {
14
+ mediaItemType: 'external-image',
15
+ dataURI: item.attrs.url
16
+ };
17
+ }
18
+ return {
19
+ id: item.attrs.id,
20
+ mediaItemType: 'file',
21
+ collectionName: item.attrs.collection
22
+ };
23
+ };
24
+ const isNodeSelected = props => (mediaItemPos, mediaGroupPos) => {
25
+ const selected = isNodeSelectedOrInRange(props.anchorPos, props.headPos, mediaGroupPos, props.nodeSize);
26
+ if (selected === SelectedState.selectedInRange) {
27
+ return true;
28
+ }
29
+ if (selected === SelectedState.selectedInside && props.anchorPos === mediaItemPos) {
30
+ return true;
31
+ }
32
+ return false;
33
+ };
34
+ const prepareFilmstripItem = ({
35
+ allowLazyLoading,
36
+ enableDownloadButton,
37
+ handleMediaNodeRemoval,
38
+ getPos,
39
+ intl,
40
+ isMediaItemSelected,
41
+ setMediaGroupNodeSelection,
42
+ featureFlags
43
+ }) => (item, idx) => {
44
+ // We declared this to get a fresh position every time
45
+ const getNodePos = () => {
46
+ const pos = getPos();
47
+ if (typeof pos !== 'number') {
48
+ // That may seems weird, but the previous type wasn't match with the real ProseMirror code. And a lot of Media API was built expecting a number
49
+ // Because the original code would return NaN on runtime
50
+ // We are just make it explict now.
51
+ // We may run a deep investagation on Media code to figure out a better fix. But, for now, we want to keep the current behavior.
52
+ // TODO: ED-13910 prosemirror-bump leftovers
53
+ return NaN;
54
+ }
55
+ return pos + idx + 1;
56
+ };
57
+
58
+ // Media Inline creates a floating toolbar with the same options, excludes these options if enabled
59
+ const mediaInlineOptions = (allowMediaInline = false) => {
60
+ if (!allowMediaInline) {
61
+ return {
62
+ shouldEnableDownloadButton: enableDownloadButton,
63
+ actions: [{
64
+ handler: handleMediaNodeRemoval.bind(null, undefined, getNodePos),
65
+ icon: /*#__PURE__*/React.createElement(EditorCloseIcon, {
66
+ label: intl.formatMessage(messages.mediaGroupDeleteLabel)
67
+ })
68
+ }]
69
+ };
70
+ }
71
+ };
72
+ const mediaGroupPos = getPos();
73
+ return {
74
+ identifier: getIdentifier(item),
75
+ isLazy: allowLazyLoading,
76
+ selected: isMediaItemSelected(getNodePos(), typeof mediaGroupPos === 'number' ? mediaGroupPos : NaN),
77
+ onClick: () => {
78
+ setMediaGroupNodeSelection(getNodePos());
79
+ },
80
+ ...mediaInlineOptions(getMediaFeatureFlag('mediaInline', featureFlags))
81
+ };
82
+ };
83
+
84
+ /**
85
+ * Keep returning the same ProseMirror Node, unless the node content changed.
86
+ *
87
+ * React uses shallow comparation with `Object.is`,
88
+ * but that can cause multiple re-renders when the same node is given in a different instance.
89
+ *
90
+ * To avoid unnecessary re-renders, this hook uses the `Node.eq` from ProseMirror API to compare
91
+ * previous and new values.
92
+ */
93
+ const useLatestMediaGroupNode = nextMediaNode => {
94
+ const previousMediaNode = usePreviousState(nextMediaNode);
95
+ const [mediaNode, setMediaNode] = React.useState(nextMediaNode);
96
+ React.useEffect(() => {
97
+ if (!previousMediaNode) {
98
+ return;
99
+ }
100
+ if (!previousMediaNode.eq(nextMediaNode)) {
101
+ setMediaNode(nextMediaNode);
102
+ }
103
+ }, [previousMediaNode, nextMediaNode]);
104
+ return mediaNode;
105
+ };
106
+ const runMediaNodeUpdate = async ({
107
+ mediaNodeUpdater,
108
+ getPos,
109
+ node,
110
+ updateAttrs
111
+ }) => {
112
+ if (updateAttrs) {
113
+ await mediaNodeUpdater.updateNodeAttrs(getPos);
114
+ }
115
+ const contextId = mediaNodeUpdater.getNodeContextId();
116
+ if (!contextId) {
117
+ await mediaNodeUpdater.updateNodeContextId(getPos);
118
+ }
119
+ const hasDifferentContextId = await mediaNodeUpdater.hasDifferentContextId();
120
+ if (hasDifferentContextId) {
121
+ await mediaNodeUpdater.copyNodeFromPos(getPos, {
122
+ traceId: node.attrs.__mediaTraceId
123
+ });
124
+ }
125
+ };
126
+ const noop = () => {};
127
+ export const MediaGroupNext = injectIntl( /*#__PURE__*/React.memo(props => {
128
+ const {
129
+ mediaOptions: {
130
+ allowLazyLoading,
131
+ enableDownloadButton,
132
+ featureFlags
133
+ },
134
+ intl,
135
+ getPos,
136
+ anchorPos,
137
+ headPos,
138
+ view,
139
+ disabled,
140
+ editorViewMode,
141
+ mediaProvider,
142
+ contextIdentifierProvider,
143
+ isCopyPasteEnabled
144
+ } = props;
145
+ const mediaGroupNode = useLatestMediaGroupNode(props.node);
146
+ const mediaPluginState = useMemo(() => {
147
+ return mediaStateKey.getState(view.state);
148
+ }, [view.state]);
149
+ const mediaClientConfig = mediaPluginState === null || mediaPluginState === void 0 ? void 0 : mediaPluginState.mediaClientConfig;
150
+ const handleMediaGroupUpdate = mediaPluginState === null || mediaPluginState === void 0 ? void 0 : mediaPluginState.handleMediaGroupUpdate;
151
+ const [viewMediaClientConfig, setViewMediaClientConfig] = useState(undefined);
152
+ const nodeSize = mediaGroupNode.nodeSize;
153
+ const mediaNodesWithOffsets = useMemo(() => {
154
+ const result = [];
155
+ mediaGroupNode.forEach((item, childOffset) => {
156
+ result.push({
157
+ node: item,
158
+ offset: childOffset
159
+ });
160
+ });
161
+ return result;
162
+ }, [mediaGroupNode]);
163
+ const previousMediaNodesWithOffsets = usePreviousState(mediaNodesWithOffsets);
164
+ const handleMediaNodeRemoval = useMemo(() => {
165
+ return disabled || !mediaPluginState ? noop : mediaPluginState.handleMediaNodeRemoval;
166
+ }, [disabled, mediaPluginState]);
167
+ const setMediaGroupNodeSelection = useCallback(pos => {
168
+ setNodeSelection(view, pos);
169
+ }, [view]);
170
+ const isMediaItemSelected = useMemo(() => {
171
+ return isNodeSelected({
172
+ anchorPos,
173
+ headPos,
174
+ nodeSize
175
+ });
176
+ }, [anchorPos, headPos, nodeSize]);
177
+ const filmstripItem = useMemo(() => {
178
+ return prepareFilmstripItem({
179
+ allowLazyLoading,
180
+ enableDownloadButton,
181
+ handleMediaNodeRemoval,
182
+ getPos,
183
+ intl,
184
+ isMediaItemSelected,
185
+ setMediaGroupNodeSelection,
186
+ featureFlags
187
+ });
188
+ }, [allowLazyLoading, enableDownloadButton, handleMediaNodeRemoval, getPos, intl, isMediaItemSelected, setMediaGroupNodeSelection, featureFlags]);
189
+ const items = useMemo(() => {
190
+ return mediaNodesWithOffsets.map(({
191
+ node,
192
+ offset
193
+ }) => {
194
+ return filmstripItem(node, offset);
195
+ });
196
+ }, [mediaNodesWithOffsets, filmstripItem]);
197
+ useEffect(() => {
198
+ setViewMediaClientConfig(mediaClientConfig);
199
+ }, [mediaClientConfig]);
200
+ useEffect(() => {
201
+ mediaNodesWithOffsets.forEach(({
202
+ node,
203
+ offset
204
+ }) => {
205
+ const mediaNodeUpdater = createMediaNodeUpdater({
206
+ view,
207
+ mediaProvider,
208
+ contextIdentifierProvider,
209
+ node,
210
+ isMediaSingle: false
211
+ });
212
+ const updateAttrs = isCopyPasteEnabled || isCopyPasteEnabled === undefined;
213
+ runMediaNodeUpdate({
214
+ mediaNodeUpdater,
215
+ node,
216
+ updateAttrs,
217
+ getPos: () => {
218
+ const pos = getPos();
219
+ if (typeof pos !== 'number') {
220
+ return undefined;
221
+ }
222
+ return pos + offset + 1;
223
+ }
224
+ });
225
+ });
226
+ }, [view, contextIdentifierProvider, getPos, mediaProvider, mediaNodesWithOffsets, isCopyPasteEnabled]);
227
+ useEffect(() => {
228
+ if (!handleMediaGroupUpdate || !previousMediaNodesWithOffsets) {
229
+ return;
230
+ }
231
+ const old = previousMediaNodesWithOffsets.map(({
232
+ node
233
+ }) => node);
234
+ const next = mediaNodesWithOffsets.map(({
235
+ node
236
+ }) => node);
237
+ handleMediaGroupUpdate(old, next);
238
+ return () => {
239
+ handleMediaGroupUpdate(next, []);
240
+ };
241
+ }, [handleMediaGroupUpdate, mediaNodesWithOffsets, previousMediaNodesWithOffsets]);
242
+ return /*#__PURE__*/React.createElement(Filmstrip, {
243
+ items: items,
244
+ mediaClientConfig: viewMediaClientConfig,
245
+ featureFlags: featureFlags,
246
+ shouldOpenMediaViewer: editorViewMode
247
+ });
248
+ }));
249
+ MediaGroupNext.displayName = 'MediaGroup';
@@ -424,8 +424,7 @@ const hasPrivateAttrsChanged = (currentAttrs, newAttrs) => {
424
424
  };
425
425
  export const createMediaNodeUpdater = props => {
426
426
  const updaterProps = {
427
- ...props,
428
- isMediaSingle: true
427
+ ...props
429
428
  };
430
429
  return new MediaNodeUpdater(updaterProps);
431
430
  };
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
3
3
  import { useSharedPluginState } from '@atlaskit/editor-common/hooks';
4
4
  import { toolbarInsertBlockMessages as messages } from '@atlaskit/editor-common/messages';
@@ -28,6 +28,7 @@ import { floatingToolbar } from './toolbar';
28
28
  import { MediaPickerComponents } from './ui/MediaPicker';
29
29
  import { RenderMediaViewer } from './ui/MediaViewer/PortalWrapper';
30
30
  import ToolbarMedia from './ui/ToolbarMedia';
31
+ import { createMediaIdentifierArray, extractMediaNodes } from './utils/media-common';
31
32
  import { insertMediaAsMediaSingle } from './utils/media-single';
32
33
  const MediaPickerFunctionalComponent = ({
33
34
  api,
@@ -48,12 +49,24 @@ const MediaPickerFunctionalComponent = ({
48
49
  });
49
50
  };
50
51
  const MediaViewerFunctionalComponent = ({
51
- api
52
+ api,
53
+ editorView
52
54
  }) => {
53
55
  const {
54
56
  mediaState
55
57
  } = useSharedPluginState(api, ['media']);
56
58
 
59
+ // Only traverse document once when media viewer is visible, media viewer items will not update
60
+ // when document changes are made while media viewer is open
61
+
62
+ const mediaItems = useMemo(() => {
63
+ if (mediaState !== null && mediaState !== void 0 && mediaState.isMediaViewerVisible && fg('platform_editor_media_interaction_improvements')) {
64
+ const mediaNodes = extractMediaNodes(editorView.state.doc);
65
+ return createMediaIdentifierArray(mediaNodes);
66
+ }
67
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- only update mediaItems once when media viewer is visible
68
+ }, [mediaState === null || mediaState === void 0 ? void 0 : mediaState.isMediaViewerVisible]);
69
+
57
70
  // Viewer does not have required attributes to render the media viewer
58
71
  if (!(mediaState !== null && mediaState !== void 0 && mediaState.isMediaViewerVisible) || !(mediaState !== null && mediaState !== void 0 && mediaState.mediaViewerSelectedMedia) || !(mediaState !== null && mediaState !== void 0 && mediaState.mediaClientConfig)) {
59
72
  return null;
@@ -65,7 +78,8 @@ const MediaViewerFunctionalComponent = ({
65
78
  return /*#__PURE__*/React.createElement(RenderMediaViewer, {
66
79
  mediaClientConfig: mediaState === null || mediaState === void 0 ? void 0 : mediaState.mediaClientConfig,
67
80
  onClose: handleOnClose,
68
- selectedNodeAttrs: mediaState.mediaViewerSelectedMedia
81
+ selectedNodeAttrs: mediaState.mediaViewerSelectedMedia,
82
+ items: fg('platform_editor_media_interaction_improvements') ? mediaItems : undefined
69
83
  });
70
84
  };
71
85
  export const mediaPlugin = ({
@@ -260,7 +274,8 @@ export const mediaPlugin = ({
260
274
  appearance
261
275
  }) {
262
276
  return /*#__PURE__*/React.createElement(React.Fragment, null, fg('platform_editor_media_previewer_bugfix') && /*#__PURE__*/React.createElement(MediaViewerFunctionalComponent, {
263
- api: api
277
+ api: api,
278
+ editorView: editorView
264
279
  }), /*#__PURE__*/React.createElement(MediaPickerFunctionalComponent, {
265
280
  editorDomElement: editorView.dom,
266
281
  appearance: appearance,
@@ -673,11 +673,12 @@ export const createPlugin = (_schema, options, getIntl, pluginInjectionApi, disp
673
673
  switch (meta === null || meta === void 0 ? void 0 : meta.type) {
674
674
  case ACTIONS.SHOW_MEDIA_VIEWER:
675
675
  pluginState.mediaViewerSelectedMedia = meta.mediaViewerSelectedMedia;
676
- pluginState.isMediaViewerVisible = true;
676
+ pluginState.isMediaViewerVisible = meta.isMediaViewerVisible;
677
677
  nextPluginState = nextPluginState.clone();
678
678
  break;
679
679
  case ACTIONS.HIDE_MEDIA_VIEWER:
680
680
  pluginState.mediaViewerSelectedMedia = undefined;
681
+ pluginState.isMediaViewerVisible = meta.isMediaViewerVisible;
681
682
  nextPluginState = nextPluginState.clone();
682
683
  break;
683
684
  }
@@ -24,14 +24,15 @@ const getIdentifier = attrs => {
24
24
  export const RenderMediaViewer = ({
25
25
  mediaClientConfig,
26
26
  onClose,
27
- selectedNodeAttrs
27
+ selectedNodeAttrs,
28
+ items = []
28
29
  }) => {
29
30
  if (editorExperiment('add-media-from-url', true)) {
30
31
  const identifier = getIdentifier(selectedNodeAttrs);
31
32
  const collectionName = isExternalMedia(selectedNodeAttrs) ? '' : selectedNodeAttrs.collection;
32
33
  return /*#__PURE__*/ReactDOM.createPortal( /*#__PURE__*/React.createElement(MediaViewer, {
33
34
  collectionName: collectionName,
34
- items: [],
35
+ items: items,
35
36
  mediaClientConfig: mediaClientConfig,
36
37
  selectedItem: identifier,
37
38
  onClose: onClose
@@ -3,8 +3,10 @@ import { createNewParagraphBelow, createParagraphNear } from '@atlaskit/editor-c
3
3
  import { deleteSelection, splitBlock } from '@atlaskit/editor-prosemirror/commands';
4
4
  import { NodeSelection } from '@atlaskit/editor-prosemirror/state';
5
5
  import { findPositionOfNodeBefore } from '@atlaskit/editor-prosemirror/utils';
6
- import { isMediaBlobUrl } from '@atlaskit/media-client';
6
+ import { isExternalImageIdentifier, isMediaBlobUrl } from '@atlaskit/media-client';
7
7
  import { getMediaPluginState } from '../pm-plugins/main';
8
+ import { isExternalMedia } from '../toolbar/utils';
9
+ import { isVideo } from './media-single';
8
10
  const isTemporary = id => {
9
11
  return id.indexOf('temporary:') === 0;
10
12
  };
@@ -230,4 +232,51 @@ export const getMediaFromSupportedMediaNodesFromSelection = state => {
230
232
  default:
231
233
  return null;
232
234
  }
235
+ };
236
+ export const getIdentifier = attrs => {
237
+ if (isExternalMedia(attrs)) {
238
+ return {
239
+ mediaItemType: 'external-image',
240
+ dataURI: attrs.url
241
+ };
242
+ } else {
243
+ const {
244
+ id,
245
+ collection = ''
246
+ } = attrs;
247
+ return {
248
+ id,
249
+ mediaItemType: 'file',
250
+ collectionName: collection
251
+ };
252
+ }
253
+ };
254
+ export const extractMediaNodes = doc => {
255
+ const mediaNodes = [];
256
+ doc.descendants(node => {
257
+ if (node.type.name === 'media' || node.type.name === 'mediaInline') {
258
+ mediaNodes.push(node);
259
+ }
260
+ });
261
+ return mediaNodes;
262
+ };
263
+ export const createMediaIdentifierArray = mediaNodes => {
264
+ const mediaIdentifierMap = new Map();
265
+ mediaNodes.forEach(item => {
266
+ const attrs = item.attrs;
267
+ if (!attrs) {
268
+ return;
269
+ }
270
+ if (isVideo(attrs.__fileMimeType)) {
271
+ return;
272
+ }
273
+ const identifier = getIdentifier(attrs);
274
+
275
+ // Add only if not already processed
276
+ const idKey = isExternalImageIdentifier(identifier) ? identifier.dataURI : identifier.id;
277
+ if (!mediaIdentifierMap.has(idKey)) {
278
+ mediaIdentifierMap.set(idKey, identifier);
279
+ }
280
+ });
281
+ return [...mediaIdentifierMap.values()];
233
282
  };
@@ -21,8 +21,10 @@ import { isNodeSelectedOrInRange, SelectedState, setNodeSelection } from '@atlas
21
21
  import EditorCloseIcon from '@atlaskit/icon/glyph/editor/close';
22
22
  import { getMediaFeatureFlag } from '@atlaskit/media-common';
23
23
  import { Filmstrip } from '@atlaskit/media-filmstrip';
24
+ import { fg } from '@atlaskit/platform-feature-flags';
24
25
  import { useMediaProvider } from '../hooks/useMediaProvider';
25
26
  import { stateKey as mediaStateKey } from '../pm-plugins/plugin-key';
27
+ import { MediaGroupNext } from './mediaGroupNext';
26
28
  import { MediaNodeUpdater } from './mediaNodeUpdater';
27
29
  var isMediaGroupSelectedFromProps = function isMediaGroupSelectedFromProps(props) {
28
30
  /**
@@ -339,6 +341,23 @@ var MediaGroupNodeView = /*#__PURE__*/function (_ReactNodeView) {
339
341
  if (!mediaProvider) {
340
342
  return null;
341
343
  }
344
+ if (fg('platform_editor_react18_phase2__media_single')) {
345
+ return /*#__PURE__*/React.createElement(MediaGroupNext, {
346
+ node: _this3.node,
347
+ getPos: getPos,
348
+ view: _this3.view,
349
+ forwardRef: forwardRef,
350
+ disabled: (editorDisabledPlugin || {}).editorDisabled,
351
+ allowLazyLoading: mediaOptions.allowLazyLoading,
352
+ mediaProvider: mediaProvider,
353
+ contextIdentifierProvider: contextIdentifierProvider,
354
+ isCopyPasteEnabled: mediaOptions.isCopyPasteEnabled,
355
+ anchorPos: _this3.view.state.selection.$anchor.pos,
356
+ headPos: _this3.view.state.selection.$head.pos,
357
+ mediaOptions: mediaOptions,
358
+ editorViewMode: (editorViewModePlugin === null || editorViewModePlugin === void 0 ? void 0 : editorViewModePlugin.mode) === 'view'
359
+ });
360
+ }
342
361
  return /*#__PURE__*/React.createElement(IntlMediaGroup, {
343
362
  node: _this3.node,
344
363
  getPos: getPos,