@atlaskit/editor-plugin-media 12.4.1 → 12.5.0

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,20 @@
1
+ import { akEditorFullWidthLayoutWidth, akEditorWideLayoutWidth } from '@atlaskit/editor-shared-styles';
2
+ var MIN_MEDIA_DISPLAY_WIDTH = 24;
3
+ /**
4
+ * Computes the new mediaSingle display width that preserves the original display height
5
+ * when a media node is replaced with a file of a different aspect ratio, clamped to valid bounds.
6
+ *
7
+ * @param targetDisplayHeight - The display height to preserve (from the old image)
8
+ * @param newIntrinsicWidth - The new file's intrinsic pixel width
9
+ * @param newIntrinsicHeight - The new file's intrinsic pixel height
10
+ * @param layout - The mediaSingle layout (affects max width)
11
+ * @param lineLength - The editor content column width in pixels
12
+ */
13
+ export var computeReplacementDisplayWidth = function computeReplacementDisplayWidth(targetDisplayHeight, newIntrinsicWidth, newIntrinsicHeight, layout, lineLength) {
14
+ if (newIntrinsicHeight <= 0) {
15
+ return MIN_MEDIA_DISPLAY_WIDTH;
16
+ }
17
+ var unclamped = targetDisplayHeight * (newIntrinsicWidth / newIntrinsicHeight);
18
+ var maxWidth = layout === 'full-width' ? akEditorFullWidthLayoutWidth : layout === 'wide' ? akEditorWideLayoutWidth : lineLength;
19
+ return Math.max(MIN_MEDIA_DISPLAY_WIDTH, Math.min(unclamped, maxWidth));
20
+ };
@@ -13,6 +13,7 @@ import React from 'react';
13
13
  import { RawIntlProvider } from 'react-intl';
14
14
  // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
15
15
  import uuid from 'uuid';
16
+ import { SetAttrsStep } from '@atlaskit/adf-schema/steps';
16
17
  import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
17
18
  import { getBrowserInfo } from '@atlaskit/editor-common/browser';
18
19
  import { mediaInlineImagesEnabled } from '@atlaskit/editor-common/media-inline';
@@ -27,6 +28,7 @@ import { CellSelection } from '@atlaskit/editor-tables/cell-selection';
27
28
  import { isFileIdentifier } from '@atlaskit/media-client';
28
29
  import { getMediaFeatureFlag } from '@atlaskit/media-common';
29
30
  import { fg } from '@atlaskit/platform-feature-flags';
31
+ import { createMediaNodeUpdater } from '../nodeviews/mediaNodeUpdater';
30
32
  // Ignored via go/ees005
31
33
  // eslint-disable-next-line import/no-namespace
32
34
  import * as helpers from '../pm-plugins/commands/helpers';
@@ -87,6 +89,15 @@ export var MediaPluginStateImplementation = /*#__PURE__*/function () {
87
89
  _defineProperty(this, "destroyed", false);
88
90
  _defineProperty(this, "removeOnCloseListener", function () {});
89
91
  _defineProperty(this, "onPopupToggleCallback", function () {});
92
+ // When non-null, holds the file ID of the media node being replaced. Used both as a
93
+ // flag (non-null = replace mode) and as a cross-check to ensure the correct node is
94
+ // updated if the selection moves between the picker opening and the file being picked.
95
+ _defineProperty(this, "replaceMediaFileId", null);
96
+ // The display height (in pixels) of the mediaSingle being replaced, computed at replace time
97
+ // from its display width and the old media node's intrinsic aspect ratio.
98
+ // Used by the nodeview to recompute display width from the new file's aspect ratio after
99
+ // dimensions are fetched, preserving visual height rather than visual width.
100
+ _defineProperty(this, "replaceMediaTargetDisplayHeight", null);
90
101
  _defineProperty(this, "identifierCount", new Map());
91
102
  // This is to enable mediaShallowCopySope to enable only shallow copying media referenced within the edtior
92
103
  // see: trackOutOfScopeIdentifier
@@ -122,7 +133,7 @@ export var MediaPluginStateImplementation = /*#__PURE__*/function () {
122
133
  * called when we insert a new file via the picker (connected via pickerfacade)
123
134
  */
124
135
  _defineProperty(this, "insertFile", function (mediaState, onMediaStateChanged, pickerType, insertMediaVia) {
125
- var _this$pluginInjection, _mediaState$collectio, _this$pluginInjection2;
136
+ var _this$pluginInjection, _mediaState$collectio, _this$pluginInjection3;
126
137
  var state = _this.view.state;
127
138
  var editorAnalyticsAPI = (_this$pluginInjection = _this.pluginInjectionApi) === null || _this$pluginInjection === void 0 || (_this$pluginInjection = _this$pluginInjection.analytics) === null || _this$pluginInjection === void 0 ? void 0 : _this$pluginInjection.actions;
128
139
  var mediaStateWithContext = _objectSpread(_objectSpread({}, mediaState), {}, {
@@ -133,6 +144,19 @@ export var MediaPluginStateImplementation = /*#__PURE__*/function () {
133
144
  return;
134
145
  }
135
146
 
147
+ // If replace mode was set but the selection has moved away from a mediaSingle
148
+ // (e.g. the user cancelled the replace picker and then inserted media elsewhere),
149
+ // clear replace state so this insertion proceeds as a normal insert.
150
+ if (_this.replaceMediaFileId !== null) {
151
+ var _state$selection$node;
152
+ var mediaSingle = state.schema.nodes.mediaSingle;
153
+ var isStillOnTargetMedia = state.selection instanceof NodeSelection && state.selection.node.type === mediaSingle && ((_state$selection$node = state.selection.node.firstChild) === null || _state$selection$node === void 0 ? void 0 : _state$selection$node.attrs.id) === _this.replaceMediaFileId;
154
+ if (!isStillOnTargetMedia) {
155
+ _this.replaceMediaFileId = null;
156
+ _this.replaceMediaTargetDisplayHeight = null;
157
+ }
158
+ }
159
+
136
160
  // We need to dispatch the change to event dispatcher only for successful files
137
161
  if (mediaState.status !== 'error') {
138
162
  _this.updateAndDispatch({
@@ -145,13 +169,124 @@ export var MediaPluginStateImplementation = /*#__PURE__*/function () {
145
169
  });
146
170
  _this.uploadInProgressSubscriptionsNotified = true;
147
171
  }
172
+
173
+ // Replace mode: if a media node is being replaced, update its attrs in-place
174
+ // rather than inserting a new node. This preserves layout, width, and caption.
175
+ if (_this.replaceMediaFileId !== null) {
176
+ var _mediaState$fileMimeT, _mediaState$fileName, _mediaState$fileSize;
177
+ // Clear replace mode immediately so subsequent insertions behave normally
178
+ _this.replaceMediaFileId = null;
179
+ var currentState = _this.view.state;
180
+ var mediaSinglePos = currentState.selection.from;
181
+ var mediaPos = mediaSinglePos + 1;
182
+ var mediaNode = currentState.doc.nodeAt(mediaPos);
183
+ if (!mediaNode || mediaNode.type.name !== 'media') {
184
+ return;
185
+ }
186
+
187
+ // Build a single transaction that:
188
+ // 1. Updates the media child node attrs (new file identity + cleared dimensions)
189
+ // 2. Re-creates the NodeSelection so the toolbar picks up the fresh node
190
+ var tr = currentState.tr;
191
+ tr.step(new SetAttrsStep(mediaPos, {
192
+ id: mediaState.id,
193
+ collection: collection,
194
+ type: 'file',
195
+ __fileMimeType: (_mediaState$fileMimeT = mediaState.fileMimeType) !== null && _mediaState$fileMimeT !== void 0 ? _mediaState$fileMimeT : null,
196
+ __fileName: (_mediaState$fileName = mediaState.fileName) !== null && _mediaState$fileName !== void 0 ? _mediaState$fileName : null,
197
+ __fileSize: (_mediaState$fileSize = mediaState.fileSize) !== null && _mediaState$fileSize !== void 0 ? _mediaState$fileSize : null,
198
+ __mediaTraceId: null,
199
+ // Clear intrinsic dimensions — they'll be fetched once the file
200
+ // is processed and applied via updateDimensions in a single tx
201
+ // with the height-preserving mediaSingle width adjustment.
202
+ width: null,
203
+ height: null
204
+ }));
205
+ // Re-create the selection so the floating toolbar picks up
206
+ // the updated node and renders the full set of controls.
207
+ tr.setSelection(NodeSelection.create(tr.doc, mediaSinglePos));
208
+ tr.setMeta('scrollIntoView', false);
209
+ _this.view.dispatch(tr);
210
+
211
+ // Still register the state-change listener so upload completion is tracked
212
+ onMediaStateChanged(_this.handleMediaState);
213
+ var _isEndState = function _isEndState(state) {
214
+ return state.status && MEDIA_RESOLVED_STATES.includes(state.status);
215
+ };
216
+
217
+ // After the file finishes uploading/processing, trigger a dimension fetch.
218
+ // getRemoteDimensions may fail if called too early (isImageRepresentationReady
219
+ // returns false while processing), so we wait for the ready state first.
220
+ var triggerDimensionFetch = function triggerDimensionFetch() {
221
+ // Find the media node in the doc by id and create a temporary
222
+ // MediaNodeUpdater to fetch and apply dimensions
223
+ var editorState = _this.view.state;
224
+ var mediaSingleType = editorState.schema.nodes.mediaSingle;
225
+ var mediaChildNode = null;
226
+ editorState.doc.descendants(function (node) {
227
+ if (mediaChildNode) {
228
+ return false;
229
+ }
230
+ if (node.type === mediaSingleType) {
231
+ var child = node.firstChild;
232
+ if (child && child.attrs.id === mediaState.id) {
233
+ mediaChildNode = child;
234
+ }
235
+ }
236
+ return true;
237
+ });
238
+ if (mediaChildNode) {
239
+ var _this$pluginInjection2;
240
+ var updater = createMediaNodeUpdater({
241
+ view: _this.view,
242
+ mediaProvider: _this.mediaProvider ? Promise.resolve(_this.mediaProvider) : undefined,
243
+ contextIdentifierProvider: _this.contextIdentifierProvider ? Promise.resolve(_this.contextIdentifierProvider) : undefined,
244
+ node: mediaChildNode,
245
+ isMediaSingle: true,
246
+ lineLength: (_this$pluginInjection2 = _this.pluginInjectionApi) === null || _this$pluginInjection2 === void 0 || (_this$pluginInjection2 = _this$pluginInjection2.width) === null || _this$pluginInjection2 === void 0 || (_this$pluginInjection2 = _this$pluginInjection2.sharedState.currentState()) === null || _this$pluginInjection2 === void 0 ? void 0 : _this$pluginInjection2.lineLength
247
+ });
248
+ updater.getRemoteDimensions().then(function (dims) {
249
+ if (dims) {
250
+ updater.updateDimensions(dims);
251
+ }
252
+ }).catch(function () {
253
+ // Silently ignore — if dimensions can't be fetched (e.g. network error),
254
+ // the image will render at its current size without the height-preserving
255
+ // width adjustment. This is an acceptable degraded experience.
256
+ });
257
+ }
258
+ };
259
+ if (!_isEndState(mediaStateWithContext)) {
260
+ var uploadingPromise = new Promise(function (resolve) {
261
+ onMediaStateChanged(function (newState) {
262
+ if (_isEndState(newState)) {
263
+ resolve(newState);
264
+ }
265
+ });
266
+ });
267
+ _this.taskManager.addPendingTask(uploadingPromise, mediaStateWithContext.id).then(function () {
268
+ _this.updateAndDispatch({
269
+ allUploadsFinished: true
270
+ });
271
+ triggerDimensionFetch();
272
+ });
273
+ } else {
274
+ // File is already in a resolved state — fetch dimensions immediately
275
+ triggerDimensionFetch();
276
+ }
277
+ var _view = _this.view;
278
+ if (!_view.hasFocus()) {
279
+ _view.focus();
280
+ }
281
+ return;
282
+ }
148
283
  switch (getMediaNodeInsertionType(state, _this.mediaOptions, mediaStateWithContext.fileMimeType)) {
149
284
  case 'inline':
150
285
  insertMediaInlineNode(editorAnalyticsAPI)(_this.view, mediaStateWithContext, collection, _this.allowInlineImages, _this.getInputMethod(pickerType), insertMediaVia);
151
286
  break;
152
287
  case 'block':
153
288
  // read width state right before inserting to get up-to-date and define values
154
- var widthPluginState = (_this$pluginInjection2 = _this.pluginInjectionApi) === null || _this$pluginInjection2 === void 0 || (_this$pluginInjection2 = _this$pluginInjection2.width) === null || _this$pluginInjection2 === void 0 ? void 0 : _this$pluginInjection2.sharedState.currentState();
289
+ var widthPluginState = (_this$pluginInjection3 = _this.pluginInjectionApi) === null || _this$pluginInjection3 === void 0 || (_this$pluginInjection3 = _this$pluginInjection3.width) === null || _this$pluginInjection3 === void 0 ? void 0 : _this$pluginInjection3.sharedState.currentState();
155
290
  insertMediaSingleNode(_this.view, mediaStateWithContext, _this.getInputMethod(pickerType), collection, _this.mediaOptions && _this.mediaOptions.alignLeftOnInsert, widthPluginState, editorAnalyticsAPI, _this.onNodeInserted, insertMediaVia, _this.mediaOptions && _this.mediaOptions.allowPixelResizing);
156
291
  break;
157
292
  case 'group':
@@ -167,7 +302,7 @@ export var MediaPluginStateImplementation = /*#__PURE__*/function () {
167
302
  return state.status && MEDIA_RESOLVED_STATES.indexOf(state.status) !== -1;
168
303
  };
169
304
  if (!isEndState(mediaStateWithContext)) {
170
- var uploadingPromise = new Promise(function (resolve) {
305
+ var _uploadingPromise = new Promise(function (resolve) {
171
306
  onMediaStateChanged(function (newState) {
172
307
  // When media item reaches its final state, remove listener and resolve
173
308
  if (isEndState(newState)) {
@@ -176,7 +311,7 @@ export var MediaPluginStateImplementation = /*#__PURE__*/function () {
176
311
  });
177
312
  });
178
313
  if (fg('platform_editor_media_disable_save_during_upload')) {
179
- _this.taskManager.addPendingTask(uploadingPromise, mediaStateWithContext.id).then(function () {
314
+ _this.taskManager.addPendingTask(_uploadingPromise, mediaStateWithContext.id).then(function () {
180
315
  _this.updateAndDispatch({
181
316
  allUploadsFinished: true
182
317
  });
@@ -190,7 +325,7 @@ export var MediaPluginStateImplementation = /*#__PURE__*/function () {
190
325
  });
191
326
  });
192
327
  } else {
193
- _this.taskManager.addPendingTask(uploadingPromise, mediaStateWithContext.id).then(function () {
328
+ _this.taskManager.addPendingTask(_uploadingPromise, mediaStateWithContext.id).then(function () {
194
329
  _this.updateAndDispatch({
195
330
  allUploadsFinished: true
196
331
  });
@@ -232,6 +367,64 @@ export var MediaPluginStateImplementation = /*#__PURE__*/function () {
232
367
  }
233
368
  _this.onPopupToggleCallback(true);
234
369
  });
370
+ /**
371
+ * Opens the media picker in "replace" mode. The next file selected/uploaded
372
+ * will replace the currently selected mediaSingle node's media child in-place,
373
+ * preserving layout, width, and caption.
374
+ *
375
+ * The display height is computed and stored so that after the new file's intrinsic
376
+ * dimensions are fetched, the mediaSingle display width can be adjusted to maintain
377
+ * visual height stability rather than width stability.
378
+ */
379
+ _defineProperty(this, "showMediaPickerForReplace", function () {
380
+ var _this$pluginInjection4, _this$pluginInjection5;
381
+ var state = _this.view.state;
382
+ var mediaSingle = state.schema.nodes.mediaSingle;
383
+ var selection = state.selection;
384
+
385
+ // Only activate replace mode when a mediaSingle is selected
386
+ if (!(selection instanceof NodeSelection) || selection.node.type !== mediaSingle) {
387
+ return;
388
+ }
389
+ var mediaSingleNode = selection.node;
390
+ var mediaNode = mediaSingleNode.firstChild;
391
+ if (!mediaNode) {
392
+ return;
393
+ }
394
+
395
+ // Store the current media node's id so insertFile can identify and replace it
396
+ _this.replaceMediaFileId = mediaNode.attrs.id;
397
+
398
+ // Compute and store the current display height so we can preserve it after
399
+ // the new file's intrinsic dimensions are known.
400
+ // displayHeight = displayWidth * (intrinsicHeight / intrinsicWidth)
401
+ var widthAttr = mediaSingleNode.attrs.width;
402
+ var widthType = mediaSingleNode.attrs.widthType;
403
+ var intrinsicWidth = mediaNode.attrs.width;
404
+ var intrinsicHeight = mediaNode.attrs.height;
405
+
406
+ // Resolve actual pixel display width from mediaSingle attrs.
407
+ var lineLength = (_this$pluginInjection4 = (_this$pluginInjection5 = _this.pluginInjectionApi) === null || _this$pluginInjection5 === void 0 || (_this$pluginInjection5 = _this$pluginInjection5.width) === null || _this$pluginInjection5 === void 0 || (_this$pluginInjection5 = _this$pluginInjection5.sharedState.currentState()) === null || _this$pluginInjection5 === void 0 ? void 0 : _this$pluginInjection5.lineLength) !== null && _this$pluginInjection4 !== void 0 ? _this$pluginInjection4 : 760;
408
+ var displayWidth = null;
409
+ if (widthAttr && widthType === 'pixel') {
410
+ displayWidth = widthAttr;
411
+ } else if (widthAttr) {
412
+ // Default widthType is 'percentage' — convert to pixels
413
+ displayWidth = widthAttr / 100 * lineLength;
414
+ } else if (intrinsicWidth) {
415
+ // No width set at all (never resized) — fall back to intrinsic width
416
+ displayWidth = intrinsicWidth;
417
+ }
418
+ if (displayWidth && intrinsicWidth && intrinsicHeight && intrinsicWidth > 0) {
419
+ _this.replaceMediaTargetDisplayHeight = displayWidth * (intrinsicHeight / intrinsicWidth);
420
+ } else {
421
+ // Can't compute display height — fall back to preserving width
422
+ _this.replaceMediaTargetDisplayHeight = null;
423
+ }
424
+
425
+ // Finally, show the media picker
426
+ _this.showMediaPicker();
427
+ });
235
428
  _defineProperty(this, "setBrowseFn", function (browseFn) {
236
429
  _this.openMediaPickerBrowser = browseFn;
237
430
  });
@@ -687,8 +880,8 @@ export var MediaPluginStateImplementation = /*#__PURE__*/function () {
687
880
  }, {
688
881
  key: "contextIdentifierProvider",
689
882
  get: function get() {
690
- var _this$pluginInjection3;
691
- return (_this$pluginInjection3 = this.pluginInjectionApi) === null || _this$pluginInjection3 === void 0 || (_this$pluginInjection3 = _this$pluginInjection3.contextIdentifier) === null || _this$pluginInjection3 === void 0 || (_this$pluginInjection3 = _this$pluginInjection3.sharedState.currentState()) === null || _this$pluginInjection3 === void 0 ? void 0 : _this$pluginInjection3.contextIdentifierProvider;
883
+ var _this$pluginInjection6;
884
+ return (_this$pluginInjection6 = this.pluginInjectionApi) === null || _this$pluginInjection6 === void 0 || (_this$pluginInjection6 = _this$pluginInjection6.contextIdentifier) === null || _this$pluginInjection6 === void 0 || (_this$pluginInjection6 = _this$pluginInjection6.sharedState.currentState()) === null || _this$pluginInjection6 === void 0 ? void 0 : _this$pluginInjection6.contextIdentifierProvider;
692
885
  }
693
886
  }, {
694
887
  key: "selectLastAddedMediaNode",
@@ -923,9 +1116,9 @@ export var createPlugin = function createPlugin(_schema, options, getIntl, plugi
923
1116
  return;
924
1117
  },
925
1118
  key: stateKey,
926
- view: function view(_view) {
927
- var pluginState = getMediaPluginState(_view.state);
928
- pluginState.setView(_view);
1119
+ view: function view(_view2) {
1120
+ var pluginState = getMediaPluginState(_view2.state);
1121
+ pluginState.setView(_view2);
929
1122
  pluginState.updateElement();
930
1123
  return {
931
1124
  update: function update() {
@@ -23,6 +23,7 @@ import ImageFullscreenIcon from '@atlaskit/icon/core/image-fullscreen';
23
23
  import ImageInlineIcon from '@atlaskit/icon/core/image-inline';
24
24
  import MaximizeIcon from '@atlaskit/icon/core/maximize';
25
25
  import SmartLinkCardIcon from '@atlaskit/icon/core/smart-link-card';
26
+ import UploadIcon from '@atlaskit/icon/core/upload';
26
27
  import { mediaFilmstripItemDOMSelector } from '@atlaskit/media-filmstrip';
27
28
  import { messages } from '@atlaskit/media-ui';
28
29
  import { fg } from '@atlaskit/platform-feature-flags';
@@ -883,6 +884,7 @@ export var floatingToolbar = function floatingToolbar(state, intl) {
883
884
  if (!mediaPluginState.isResizing && areToolbarFlagsEnabled(Boolean(pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : pluginInjectionApi.toolbar))) {
884
885
  var _pluginInjectionApi$a0, _pluginInjectionApi$a1, _pluginInjectionApi$a10;
885
886
  updateToFullHeightSeparator(items);
887
+ var showReplaceOption = !isViewOnly && mediaPluginState.allowsUploads && expValEquals('platform_editor_inline_media_replacement', 'isEnabled', true) && selectedNodeType === mediaSingle;
886
888
  var customOptions = [].concat(_toConsumableArray(getLinkingDropdownOptions(state, intl, mediaLinkingState, allowMediaInline && selectedNodeType && selectedNodeType === mediaInline, allowLinking, isViewOnly)), _toConsumableArray(getAltTextDropdownOption(state, intl.formatMessage, allowAltTextOnImages, selectedNodeType, pluginInjectionApi === null || pluginInjectionApi === void 0 || (_pluginInjectionApi$a0 = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a0 === void 0 ? void 0 : _pluginInjectionApi$a0.actions)), _toConsumableArray(getResizeDropdownOption(options, state, intl.formatMessage, selectedNodeType)));
887
889
  if (customOptions.length) {
888
890
  customOptions.push({
@@ -901,7 +903,17 @@ export var floatingToolbar = function floatingToolbar(state, intl) {
901
903
  type: 'overflow-dropdown',
902
904
  id: 'media',
903
905
  testId: overflowDropdwonBtnTriggerTestId,
904
- options: [].concat(_toConsumableArray(customOptions), [_objectSpread({
906
+ options: [].concat(_toConsumableArray(customOptions), _toConsumableArray(showReplaceOption ? [{
907
+ title: intl.formatMessage(mediaAndEmbedToolbarMessages.replaceMedia),
908
+ onClick: function onClick() {
909
+ mediaPluginState.showMediaPickerForReplace();
910
+ return true;
911
+ },
912
+ icon: /*#__PURE__*/React.createElement(UploadIcon, {
913
+ label: ""
914
+ }),
915
+ testId: 'media-replace-toolbar-button'
916
+ }] : []), [_objectSpread({
905
917
  title: intl === null || intl === void 0 ? void 0 : intl.formatMessage(commonMessages.copyToClipboard),
906
918
  onClick: function onClick() {
907
919
  var _pluginInjectionApi$c4, _pluginInjectionApi$f3;
@@ -3,7 +3,7 @@ import type { ContextIdentifierProvider, MediaProvider } from '@atlaskit/editor-
3
3
  import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
4
4
  import type { EditorView } from '@atlaskit/editor-prosemirror/view';
5
5
  import { type MediaTraceContext } from '@atlaskit/media-common';
6
- import type { MediaOptions, MediaPluginState, getPosHandler as ProsemirrorGetPosHandler, SupportedMediaAttributes } from '../types';
6
+ import type { getPosHandler as ProsemirrorGetPosHandler, MediaOptions, MediaPluginState, SupportedMediaAttributes } from '../types';
7
7
  type RemoteDimensions = {
8
8
  height: number;
9
9
  id: string;
@@ -13,6 +13,8 @@ export interface MediaNodeUpdaterProps {
13
13
  contextIdentifierProvider?: Promise<ContextIdentifierProvider>;
14
14
  dispatchAnalyticsEvent?: DispatchAnalyticsEvent;
15
15
  isMediaSingle: boolean;
16
+ /** Content column width in pixels, used to clamp display width on media replacement. */
17
+ lineLength?: number;
16
18
  mediaOptions?: MediaOptions;
17
19
  mediaProvider?: Promise<MediaProvider>;
18
20
  node: PMNode;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Computes the new mediaSingle display width that preserves the original display height
3
+ * when a media node is replaced with a file of a different aspect ratio, clamped to valid bounds.
4
+ *
5
+ * @param targetDisplayHeight - The display height to preserve (from the old image)
6
+ * @param newIntrinsicWidth - The new file's intrinsic pixel width
7
+ * @param newIntrinsicHeight - The new file's intrinsic pixel height
8
+ * @param layout - The mediaSingle layout (affects max width)
9
+ * @param lineLength - The editor content column width in pixels
10
+ */
11
+ export declare const computeReplacementDisplayWidth: (targetDisplayHeight: number, newIntrinsicWidth: number, newIntrinsicHeight: number, layout: string, lineLength: number) => number;
@@ -52,6 +52,8 @@ export declare class MediaPluginStateImplementation implements MediaPluginState
52
52
  private removeOnCloseListener;
53
53
  private openMediaPickerBrowser?;
54
54
  private onPopupToggleCallback;
55
+ replaceMediaFileId: string | null;
56
+ replaceMediaTargetDisplayHeight: number | null;
55
57
  private identifierCount;
56
58
  private outOfEditorScopeIdentifierMap;
57
59
  private taskManager;
@@ -88,6 +90,16 @@ export declare class MediaPluginStateImplementation implements MediaPluginState
88
90
  splitMediaGroup: () => boolean;
89
91
  onPopupPickerClose: () => void;
90
92
  showMediaPicker: () => void;
93
+ /**
94
+ * Opens the media picker in "replace" mode. The next file selected/uploaded
95
+ * will replace the currently selected mediaSingle node's media child in-place,
96
+ * preserving layout, width, and caption.
97
+ *
98
+ * The display height is computed and stored so that after the new file's intrinsic
99
+ * dimensions are fetched, the mediaSingle display width can be adjusted to maintain
100
+ * visual height stability rather than width stability.
101
+ */
102
+ showMediaPickerForReplace: () => void;
91
103
  setBrowseFn: (browseFn: () => void) => void;
92
104
  onPopupToggle: (onPopupToggleCallback: (isOpen: boolean) => void) => void;
93
105
  /**
@@ -53,6 +53,8 @@ export interface MediaPluginState {
53
53
  pickerPromises: Array<Promise<PickerFacade>>;
54
54
  pickers: PickerFacade[];
55
55
  removeSelectedMediaContainer: () => boolean;
56
+ replaceMediaFileId: string | null;
57
+ replaceMediaTargetDisplayHeight: number | null;
56
58
  resizingWidth: number;
57
59
  selectedMediaContainerNode: () => PMNode | undefined;
58
60
  setBrowseFn: (browseFn: () => void) => void;
@@ -63,6 +65,7 @@ export interface MediaPluginState {
63
65
  showDropzone: boolean;
64
66
  showEditingDialog?: boolean;
65
67
  showMediaPicker: () => void;
68
+ showMediaPickerForReplace: () => void;
66
69
  splitMediaGroup: () => boolean;
67
70
  subscribeToUploadInProgressState: (fn: (isUploading: boolean) => void) => void;
68
71
  trackOutOfScopeIdentifier: (identifier: Identifier) => void;
@@ -3,7 +3,7 @@ import type { ContextIdentifierProvider, MediaProvider } from '@atlaskit/editor-
3
3
  import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
4
4
  import type { EditorView } from '@atlaskit/editor-prosemirror/view';
5
5
  import { type MediaTraceContext } from '@atlaskit/media-common';
6
- import type { MediaOptions, MediaPluginState, getPosHandler as ProsemirrorGetPosHandler, SupportedMediaAttributes } from '../types';
6
+ import type { getPosHandler as ProsemirrorGetPosHandler, MediaOptions, MediaPluginState, SupportedMediaAttributes } from '../types';
7
7
  type RemoteDimensions = {
8
8
  height: number;
9
9
  id: string;
@@ -13,6 +13,8 @@ export interface MediaNodeUpdaterProps {
13
13
  contextIdentifierProvider?: Promise<ContextIdentifierProvider>;
14
14
  dispatchAnalyticsEvent?: DispatchAnalyticsEvent;
15
15
  isMediaSingle: boolean;
16
+ /** Content column width in pixels, used to clamp display width on media replacement. */
17
+ lineLength?: number;
16
18
  mediaOptions?: MediaOptions;
17
19
  mediaProvider?: Promise<MediaProvider>;
18
20
  node: PMNode;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Computes the new mediaSingle display width that preserves the original display height
3
+ * when a media node is replaced with a file of a different aspect ratio, clamped to valid bounds.
4
+ *
5
+ * @param targetDisplayHeight - The display height to preserve (from the old image)
6
+ * @param newIntrinsicWidth - The new file's intrinsic pixel width
7
+ * @param newIntrinsicHeight - The new file's intrinsic pixel height
8
+ * @param layout - The mediaSingle layout (affects max width)
9
+ * @param lineLength - The editor content column width in pixels
10
+ */
11
+ export declare const computeReplacementDisplayWidth: (targetDisplayHeight: number, newIntrinsicWidth: number, newIntrinsicHeight: number, layout: string, lineLength: number) => number;
@@ -52,6 +52,8 @@ export declare class MediaPluginStateImplementation implements MediaPluginState
52
52
  private removeOnCloseListener;
53
53
  private openMediaPickerBrowser?;
54
54
  private onPopupToggleCallback;
55
+ replaceMediaFileId: string | null;
56
+ replaceMediaTargetDisplayHeight: number | null;
55
57
  private identifierCount;
56
58
  private outOfEditorScopeIdentifierMap;
57
59
  private taskManager;
@@ -88,6 +90,16 @@ export declare class MediaPluginStateImplementation implements MediaPluginState
88
90
  splitMediaGroup: () => boolean;
89
91
  onPopupPickerClose: () => void;
90
92
  showMediaPicker: () => void;
93
+ /**
94
+ * Opens the media picker in "replace" mode. The next file selected/uploaded
95
+ * will replace the currently selected mediaSingle node's media child in-place,
96
+ * preserving layout, width, and caption.
97
+ *
98
+ * The display height is computed and stored so that after the new file's intrinsic
99
+ * dimensions are fetched, the mediaSingle display width can be adjusted to maintain
100
+ * visual height stability rather than width stability.
101
+ */
102
+ showMediaPickerForReplace: () => void;
91
103
  setBrowseFn: (browseFn: () => void) => void;
92
104
  onPopupToggle: (onPopupToggleCallback: (isOpen: boolean) => void) => void;
93
105
  /**
@@ -53,6 +53,8 @@ export interface MediaPluginState {
53
53
  pickerPromises: Array<Promise<PickerFacade>>;
54
54
  pickers: PickerFacade[];
55
55
  removeSelectedMediaContainer: () => boolean;
56
+ replaceMediaFileId: string | null;
57
+ replaceMediaTargetDisplayHeight: number | null;
56
58
  resizingWidth: number;
57
59
  selectedMediaContainerNode: () => PMNode | undefined;
58
60
  setBrowseFn: (browseFn: () => void) => void;
@@ -63,6 +65,7 @@ export interface MediaPluginState {
63
65
  showDropzone: boolean;
64
66
  showEditingDialog?: boolean;
65
67
  showMediaPicker: () => void;
68
+ showMediaPickerForReplace: () => void;
66
69
  splitMediaGroup: () => boolean;
67
70
  subscribeToUploadInProgressState: (fn: (isUploading: boolean) => void) => void;
68
71
  trackOutOfScopeIdentifier: (identifier: Identifier) => void;
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@atlaskit/editor-plugin-media/nodeviewHelpers",
3
+ "main": "../dist/cjs/nodeviews/nodeviewHelpers.js",
4
+ "module": "../dist/esm/nodeviews/nodeviewHelpers.js",
5
+ "module:es2019": "../dist/es2019/nodeviews/nodeviewHelpers.js",
6
+ "sideEffects": [
7
+ "*.compiled.css"
8
+ ],
9
+ "types": "../dist/types/nodeviews/nodeviewHelpers.d.ts",
10
+ "typesVersions": {
11
+ ">=4.5 <5.9": {
12
+ "*": [
13
+ "../dist/types-ts4.5/nodeviews/nodeviewHelpers.d.ts"
14
+ ]
15
+ }
16
+ }
17
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-media",
3
- "version": "12.4.1",
3
+ "version": "12.5.0",
4
4
  "description": "Media plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -35,7 +35,7 @@
35
35
  "@atlaskit/button": "^23.11.0",
36
36
  "@atlaskit/editor-palette": "^2.1.0",
37
37
  "@atlaskit/editor-plugin-analytics": "^10.0.0",
38
- "@atlaskit/editor-plugin-annotation": "^10.1.0",
38
+ "@atlaskit/editor-plugin-annotation": "^10.2.0",
39
39
  "@atlaskit/editor-plugin-connectivity": "^10.0.0",
40
40
  "@atlaskit/editor-plugin-decorations": "^10.0.0",
41
41
  "@atlaskit/editor-plugin-editor-disabled": "^10.0.0",
@@ -61,12 +61,12 @@
61
61
  "@atlaskit/media-common": "^13.2.0",
62
62
  "@atlaskit/media-filmstrip": "^51.3.0",
63
63
  "@atlaskit/media-picker": "^71.2.0",
64
- "@atlaskit/media-ui": "^29.1.0",
64
+ "@atlaskit/media-ui": "^29.2.0",
65
65
  "@atlaskit/media-viewer": "^53.1.0",
66
66
  "@atlaskit/platform-feature-flags": "^1.1.0",
67
67
  "@atlaskit/primitives": "^19.0.0",
68
68
  "@atlaskit/textfield": "^8.3.0",
69
- "@atlaskit/tmp-editor-statsig": "^74.7.0",
69
+ "@atlaskit/tmp-editor-statsig": "^74.10.0",
70
70
  "@atlaskit/tokens": "^13.0.0",
71
71
  "@atlaskit/tooltip": "^22.0.0",
72
72
  "@babel/runtime": "^7.0.0",
@@ -78,7 +78,7 @@
78
78
  "uuid": "^3.1.0"
79
79
  },
80
80
  "peerDependencies": {
81
- "@atlaskit/editor-common": "^114.18.0",
81
+ "@atlaskit/editor-common": "^114.19.0",
82
82
  "@atlaskit/media-core": "^37.0.0",
83
83
  "react": "^18.2.0",
84
84
  "react-dom": "^18.2.0",