@atlaskit/editor-plugin-media 12.4.1 → 12.5.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @atlaskit/editor-plugin-media
2
2
 
3
+ ## 12.5.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies
8
+
9
+ ## 12.5.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [`76dff28130c6a`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/76dff28130c6a) -
14
+ Add replace-media button to media plugin
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies
19
+
3
20
  ## 12.4.1
4
21
 
5
22
  ### Patch Changes
@@ -5,5 +5,9 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.hasPrivateAttrsChanged = void 0;
7
7
  var hasPrivateAttrsChanged = exports.hasPrivateAttrsChanged = function hasPrivateAttrsChanged(currentAttrs, newAttrs) {
8
- return currentAttrs.__fileName !== newAttrs.__fileName || currentAttrs.__fileMimeType !== newAttrs.__fileMimeType || currentAttrs.__fileSize !== newAttrs.__fileSize || currentAttrs.__contextId !== newAttrs.__contextId;
8
+ return currentAttrs.__fileName !== newAttrs.__fileName || currentAttrs.__fileMimeType !== newAttrs.__fileMimeType || currentAttrs.__fileSize !== newAttrs.__fileSize || currentAttrs.__contextId !== newAttrs.__contextId ||
9
+ // A changed id means the media source was replaced — always re-initialise
10
+ // the updater so getRemoteDimensions fetches dimensions for the new file.
11
+ // Only check when id is explicitly present on the new attrs (it's Partial).
12
+ 'id' in newAttrs && currentAttrs.id !== newAttrs.id;
9
13
  };
@@ -12,8 +12,10 @@ var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/cl
12
12
  var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
13
13
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
14
14
  var _v = _interopRequireDefault(require("uuid/v4"));
15
+ var _steps = require("@atlaskit/adf-schema/steps");
15
16
  var _analytics = require("@atlaskit/editor-common/analytics");
16
17
  var _mediaSingle = require("@atlaskit/editor-common/media-single");
18
+ var _state = require("@atlaskit/editor-prosemirror/state");
17
19
  var _mediaClient = require("@atlaskit/media-client");
18
20
  var _mediaClientReact = require("@atlaskit/media-client-react");
19
21
  var _mediaCommon = require("@atlaskit/media-common");
@@ -22,10 +24,11 @@ var _helpers = require("../pm-plugins/commands/helpers");
22
24
  var _pluginKey = require("../pm-plugins/plugin-key");
23
25
  var _batchMediaNodeAttrs = require("../pm-plugins/utils/batchMediaNodeAttrs");
24
26
  var _mediaCommon2 = require("../pm-plugins/utils/media-common");
27
+ var _nodeviewHelpers = require("./nodeviewHelpers");
25
28
  var _excluded = ["authProvider"],
26
- _excluded2 = ["authProvider"]; // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
29
+ _excluded2 = ["authProvider"];
27
30
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
28
- function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
31
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
29
32
  var isMediaTypeSupported = function isMediaTypeSupported(type) {
30
33
  if (type) {
31
34
  return ['image', 'file'].includes(type);
@@ -324,7 +327,73 @@ var MediaNodeUpdater = exports.MediaNodeUpdater = /*#__PURE__*/function () {
324
327
  return attrs.__contextId || null;
325
328
  });
326
329
  (0, _defineProperty2.default)(this, "updateDimensions", function (dimensions) {
327
- (0, _batchMediaNodeAttrs.batchMediaNodeAttrsUpdate)(_this.props.view, {
330
+ var view = _this.props.view;
331
+ var mediaPluginState = _pluginKey.stateKey.getState(view.state);
332
+ var targetDisplayHeight = mediaPluginState === null || mediaPluginState === void 0 ? void 0 : mediaPluginState.replaceMediaTargetDisplayHeight;
333
+ if (targetDisplayHeight !== null && targetDisplayHeight !== undefined && dimensions.width > 0) {
334
+ var _this$props$lineLengt, _mediaSingleNode$attr, _mediaSingleNode;
335
+ // Replace mode: combine intrinsic dimension update on the media node AND
336
+ // display width update on the mediaSingle into a single transaction so they
337
+ // are never out of sync and don't cause two separate renders/layout shifts.
338
+
339
+ // Clear the stored target height — this is a one-shot adjustment
340
+ if (mediaPluginState) {
341
+ mediaPluginState.replaceMediaTargetDisplayHeight = null;
342
+ }
343
+
344
+ // Clamp to the layout's maximum width so the image never overflows its container.
345
+ var lineLength = (_this$props$lineLengt = _this.props.lineLength) !== null && _this$props$lineLengt !== void 0 ? _this$props$lineLengt : 760;
346
+ var state = view.state;
347
+ var mediaSingle = state.schema.nodes.mediaSingle;
348
+ // Typed explicitly because TypeScript can't track mutations inside
349
+ // the descendants callback closure and would narrow these to `never`.
350
+ var mediaSinglePos = null;
351
+ var mediaSingleNode = null;
352
+ var mediaPos = null;
353
+ state.doc.descendants(function (node, pos) {
354
+ if (mediaSinglePos !== null) {
355
+ return false;
356
+ }
357
+ if (node.type === mediaSingle) {
358
+ var mediaChild = node.firstChild;
359
+ if (mediaChild && mediaChild.attrs.id === dimensions.id) {
360
+ mediaSinglePos = pos;
361
+ mediaSingleNode = node;
362
+ mediaPos = pos + 1;
363
+ }
364
+ }
365
+ return true;
366
+ });
367
+ var layout = (_mediaSingleNode$attr = (_mediaSingleNode = mediaSingleNode) === null || _mediaSingleNode === void 0 || (_mediaSingleNode = _mediaSingleNode.attrs) === null || _mediaSingleNode === void 0 ? void 0 : _mediaSingleNode.layout) !== null && _mediaSingleNode$attr !== void 0 ? _mediaSingleNode$attr : 'center';
368
+ var newDisplayWidth = (0, _nodeviewHelpers.computeReplacementDisplayWidth)(targetDisplayHeight, dimensions.width, dimensions.height, layout, lineLength);
369
+ if (mediaSinglePos !== null && mediaSingleNode !== null && mediaPos !== null) {
370
+ var tr = state.tr;
371
+
372
+ // Update intrinsic dimensions on the media child node
373
+ tr.step(new _steps.SetAttrsStep(mediaPos, {
374
+ height: dimensions.height,
375
+ width: dimensions.width
376
+ }));
377
+
378
+ // Update display width on the mediaSingle parent
379
+ tr.setNodeMarkup(mediaSinglePos, undefined, _objectSpread(_objectSpread({}, mediaSingleNode.attrs), {}, {
380
+ width: Math.round(newDisplayWidth),
381
+ widthType: 'pixel'
382
+ }));
383
+
384
+ // Re-create the NodeSelection so the floating toolbar picks up
385
+ // the updated node and renders the full set of controls.
386
+ if (state.selection instanceof _state.NodeSelection) {
387
+ tr.setSelection(_state.NodeSelection.create(tr.doc, mediaSinglePos));
388
+ }
389
+ tr.setMeta('scrollIntoView', false);
390
+ view.dispatch(tr);
391
+ return;
392
+ }
393
+ }
394
+
395
+ // Normal (non-replace) path: use the existing batched update mechanism
396
+ (0, _batchMediaNodeAttrs.batchMediaNodeAttrsUpdate)(view, {
328
397
  id: dimensions.id,
329
398
  nextAttributes: {
330
399
  height: dimensions.height,
@@ -151,7 +151,8 @@ var useMediaAsyncOperations = function useMediaAsyncOperations(_ref3) {
151
151
  var mediaNode = _ref3.mediaNode,
152
152
  mediaNodeUpdater = _ref3.mediaNodeUpdater,
153
153
  addPendingTask = _ref3.addPendingTask,
154
- getPos = _ref3.getPos;
154
+ getPos = _ref3.getPos,
155
+ mediaChildNodeId = _ref3.mediaChildNodeId;
155
156
  _react.default.useEffect(function () {
156
157
  if (!mediaNodeUpdater) {
157
158
  return;
@@ -168,7 +169,11 @@ var useMediaAsyncOperations = function useMediaAsyncOperations(_ref3) {
168
169
  mediaNode: mediaNode,
169
170
  addPendingTask: addPendingTask
170
171
  });
171
- }, [mediaNode, addPendingTask, mediaNodeUpdater, getPos]);
172
+ // mediaChildNodeId is included so this effect re-runs when the media source
173
+ // is replaced (id changes), ensuring getRemoteDimensions fires for the new file
174
+ // with up-to-date updater props after setProps has been called.
175
+ // eslint-disable-next-line react-hooks/exhaustive-deps
176
+ }, [mediaNode, addPendingTask, mediaNodeUpdater, getPos, mediaChildNodeId]);
172
177
  };
173
178
  var noop = function noop() {};
174
179
 
@@ -287,7 +292,7 @@ var useUpdateSizeCallback = function useUpdateSizeCallback(_ref5) {
287
292
  */
288
293
  var FALLBACK_MOST_COMMON_WIDTH = 760;
289
294
  var MediaSingleNodeNext = exports.MediaSingleNodeNext = function MediaSingleNodeNext(mediaSingleNodeNextProps) {
290
- var _pluginInjectionApi$m, _mediaNode$firstChild;
295
+ var _mediaNode$firstChild, _pluginInjectionApi$m, _mediaNode$firstChild2;
291
296
  var selected = mediaSingleNodeNextProps.selected,
292
297
  getPos = mediaSingleNodeNextProps.getPos,
293
298
  nextMediaNode = mediaSingleNodeNextProps.node,
@@ -334,7 +339,8 @@ var MediaSingleNodeNext = exports.MediaSingleNodeNext = function MediaSingleNode
334
339
  mediaNodeUpdater: mediaNodeUpdater,
335
340
  getPos: getPos,
336
341
  mediaNode: mediaNode,
337
- addPendingTask: addPendingTask || noop
342
+ addPendingTask: addPendingTask || noop,
343
+ mediaChildNodeId: (_mediaNode$firstChild = mediaNode.firstChild) === null || _mediaNode$firstChild === void 0 ? void 0 : _mediaNode$firstChild.attrs.id
338
344
  });
339
345
  _react.default.useLayoutEffect(function () {
340
346
  mountedRef.current = true;
@@ -429,7 +435,7 @@ var MediaSingleNodeNext = exports.MediaSingleNodeNext = function MediaSingleNode
429
435
  }, [mediaSingleWidthAttribute, widthType, width, layout, contentWidthForLegacyExperience, containerWidth]);
430
436
  var currentMaxWidth = isSelected ? pluginInjectionApi === null || pluginInjectionApi === void 0 || (_pluginInjectionApi$m = pluginInjectionApi.media) === null || _pluginInjectionApi$m === void 0 || (_pluginInjectionApi$m = _pluginInjectionApi$m.sharedState.currentState()) === null || _pluginInjectionApi$m === void 0 ? void 0 : _pluginInjectionApi$m.currentMaxWidth : undefined;
431
437
  var contentWidth = currentMaxWidth || lineLength;
432
- var isCurrentNodeDrafting = Boolean(isDrafting && targetNodeId === (mediaNode === null || mediaNode === void 0 || (_mediaNode$firstChild = mediaNode.firstChild) === null || _mediaNode$firstChild === void 0 ? void 0 : _mediaNode$firstChild.attrs.id));
438
+ var isCurrentNodeDrafting = Boolean(isDrafting && targetNodeId === (mediaNode === null || mediaNode === void 0 || (_mediaNode$firstChild2 = mediaNode.firstChild) === null || _mediaNode$firstChild2 === void 0 ? void 0 : _mediaNode$firstChild2.attrs.id));
433
439
  var mediaSingleWrapperRef = /*#__PURE__*/_react.default.createRef();
434
440
  var captionPlaceHolderRef = /*#__PURE__*/_react.default.createRef();
435
441
  var browser = (0, _browser.getBrowserInfo)();
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.computeReplacementDisplayWidth = void 0;
7
+ var _editorSharedStyles = require("@atlaskit/editor-shared-styles");
8
+ var MIN_MEDIA_DISPLAY_WIDTH = 24;
9
+ /**
10
+ * Computes the new mediaSingle display width that preserves the original display height
11
+ * when a media node is replaced with a file of a different aspect ratio, clamped to valid bounds.
12
+ *
13
+ * @param targetDisplayHeight - The display height to preserve (from the old image)
14
+ * @param newIntrinsicWidth - The new file's intrinsic pixel width
15
+ * @param newIntrinsicHeight - The new file's intrinsic pixel height
16
+ * @param layout - The mediaSingle layout (affects max width)
17
+ * @param lineLength - The editor content column width in pixels
18
+ */
19
+ var computeReplacementDisplayWidth = exports.computeReplacementDisplayWidth = function computeReplacementDisplayWidth(targetDisplayHeight, newIntrinsicWidth, newIntrinsicHeight, layout, lineLength) {
20
+ if (newIntrinsicHeight <= 0) {
21
+ return MIN_MEDIA_DISPLAY_WIDTH;
22
+ }
23
+ var unclamped = targetDisplayHeight * (newIntrinsicWidth / newIntrinsicHeight);
24
+ var maxWidth = layout === 'full-width' ? _editorSharedStyles.akEditorFullWidthLayoutWidth : layout === 'wide' ? _editorSharedStyles.akEditorWideLayoutWidth : lineLength;
25
+ return Math.max(MIN_MEDIA_DISPLAY_WIDTH, Math.min(unclamped, maxWidth));
26
+ };
@@ -15,6 +15,7 @@ var _assert = _interopRequireDefault(require("assert"));
15
15
  var _react = _interopRequireDefault(require("react"));
16
16
  var _reactIntl = require("react-intl");
17
17
  var _uuid = _interopRequireDefault(require("uuid"));
18
+ var _steps = require("@atlaskit/adf-schema/steps");
18
19
  var _analytics = require("@atlaskit/editor-common/analytics");
19
20
  var _browser = require("@atlaskit/editor-common/browser");
20
21
  var _mediaInline = require("@atlaskit/editor-common/media-inline");
@@ -24,11 +25,12 @@ var _utils = require("@atlaskit/editor-common/utils");
24
25
  var _state2 = require("@atlaskit/editor-prosemirror/state");
25
26
  var _transform = require("@atlaskit/editor-prosemirror/transform");
26
27
  var _utils2 = require("@atlaskit/editor-prosemirror/utils");
27
- var _view2 = require("@atlaskit/editor-prosemirror/view");
28
+ var _view3 = require("@atlaskit/editor-prosemirror/view");
28
29
  var _cellSelection = require("@atlaskit/editor-tables/cell-selection");
29
30
  var _mediaClient = require("@atlaskit/media-client");
30
31
  var _mediaCommon = require("@atlaskit/media-common");
31
32
  var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
33
+ var _mediaNodeUpdater = require("../nodeviews/mediaNodeUpdater");
32
34
  var _helpers = _interopRequireWildcard(require("../pm-plugins/commands/helpers"));
33
35
  var helpers = _helpers;
34
36
  var _mediaCommon2 = require("../pm-plugins/utils/media-common");
@@ -95,6 +97,15 @@ var MediaPluginStateImplementation = exports.MediaPluginStateImplementation = /*
95
97
  (0, _defineProperty2.default)(this, "destroyed", false);
96
98
  (0, _defineProperty2.default)(this, "removeOnCloseListener", function () {});
97
99
  (0, _defineProperty2.default)(this, "onPopupToggleCallback", function () {});
100
+ // When non-null, holds the file ID of the media node being replaced. Used both as a
101
+ // flag (non-null = replace mode) and as a cross-check to ensure the correct node is
102
+ // updated if the selection moves between the picker opening and the file being picked.
103
+ (0, _defineProperty2.default)(this, "replaceMediaFileId", null);
104
+ // The display height (in pixels) of the mediaSingle being replaced, computed at replace time
105
+ // from its display width and the old media node's intrinsic aspect ratio.
106
+ // Used by the nodeview to recompute display width from the new file's aspect ratio after
107
+ // dimensions are fetched, preserving visual height rather than visual width.
108
+ (0, _defineProperty2.default)(this, "replaceMediaTargetDisplayHeight", null);
98
109
  (0, _defineProperty2.default)(this, "identifierCount", new Map());
99
110
  // This is to enable mediaShallowCopySope to enable only shallow copying media referenced within the edtior
100
111
  // see: trackOutOfScopeIdentifier
@@ -130,7 +141,7 @@ var MediaPluginStateImplementation = exports.MediaPluginStateImplementation = /*
130
141
  * called when we insert a new file via the picker (connected via pickerfacade)
131
142
  */
132
143
  (0, _defineProperty2.default)(this, "insertFile", function (mediaState, onMediaStateChanged, pickerType, insertMediaVia) {
133
- var _this$pluginInjection, _mediaState$collectio, _this$pluginInjection2;
144
+ var _this$pluginInjection, _mediaState$collectio, _this$pluginInjection3;
134
145
  var state = _this.view.state;
135
146
  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;
136
147
  var mediaStateWithContext = _objectSpread(_objectSpread({}, mediaState), {}, {
@@ -141,6 +152,19 @@ var MediaPluginStateImplementation = exports.MediaPluginStateImplementation = /*
141
152
  return;
142
153
  }
143
154
 
155
+ // If replace mode was set but the selection has moved away from a mediaSingle
156
+ // (e.g. the user cancelled the replace picker and then inserted media elsewhere),
157
+ // clear replace state so this insertion proceeds as a normal insert.
158
+ if (_this.replaceMediaFileId !== null) {
159
+ var _state$selection$node;
160
+ var mediaSingle = state.schema.nodes.mediaSingle;
161
+ var isStillOnTargetMedia = state.selection instanceof _state2.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;
162
+ if (!isStillOnTargetMedia) {
163
+ _this.replaceMediaFileId = null;
164
+ _this.replaceMediaTargetDisplayHeight = null;
165
+ }
166
+ }
167
+
144
168
  // We need to dispatch the change to event dispatcher only for successful files
145
169
  if (mediaState.status !== 'error') {
146
170
  _this.updateAndDispatch({
@@ -153,13 +177,124 @@ var MediaPluginStateImplementation = exports.MediaPluginStateImplementation = /*
153
177
  });
154
178
  _this.uploadInProgressSubscriptionsNotified = true;
155
179
  }
180
+
181
+ // Replace mode: if a media node is being replaced, update its attrs in-place
182
+ // rather than inserting a new node. This preserves layout, width, and caption.
183
+ if (_this.replaceMediaFileId !== null) {
184
+ var _mediaState$fileMimeT, _mediaState$fileName, _mediaState$fileSize;
185
+ // Clear replace mode immediately so subsequent insertions behave normally
186
+ _this.replaceMediaFileId = null;
187
+ var currentState = _this.view.state;
188
+ var mediaSinglePos = currentState.selection.from;
189
+ var mediaPos = mediaSinglePos + 1;
190
+ var mediaNode = currentState.doc.nodeAt(mediaPos);
191
+ if (!mediaNode || mediaNode.type.name !== 'media') {
192
+ return;
193
+ }
194
+
195
+ // Build a single transaction that:
196
+ // 1. Updates the media child node attrs (new file identity + cleared dimensions)
197
+ // 2. Re-creates the NodeSelection so the toolbar picks up the fresh node
198
+ var tr = currentState.tr;
199
+ tr.step(new _steps.SetAttrsStep(mediaPos, {
200
+ id: mediaState.id,
201
+ collection: collection,
202
+ type: 'file',
203
+ __fileMimeType: (_mediaState$fileMimeT = mediaState.fileMimeType) !== null && _mediaState$fileMimeT !== void 0 ? _mediaState$fileMimeT : null,
204
+ __fileName: (_mediaState$fileName = mediaState.fileName) !== null && _mediaState$fileName !== void 0 ? _mediaState$fileName : null,
205
+ __fileSize: (_mediaState$fileSize = mediaState.fileSize) !== null && _mediaState$fileSize !== void 0 ? _mediaState$fileSize : null,
206
+ __mediaTraceId: null,
207
+ // Clear intrinsic dimensions — they'll be fetched once the file
208
+ // is processed and applied via updateDimensions in a single tx
209
+ // with the height-preserving mediaSingle width adjustment.
210
+ width: null,
211
+ height: null
212
+ }));
213
+ // Re-create the selection so the floating toolbar picks up
214
+ // the updated node and renders the full set of controls.
215
+ tr.setSelection(_state2.NodeSelection.create(tr.doc, mediaSinglePos));
216
+ tr.setMeta('scrollIntoView', false);
217
+ _this.view.dispatch(tr);
218
+
219
+ // Still register the state-change listener so upload completion is tracked
220
+ onMediaStateChanged(_this.handleMediaState);
221
+ var _isEndState = function _isEndState(state) {
222
+ return state.status && MEDIA_RESOLVED_STATES.includes(state.status);
223
+ };
224
+
225
+ // After the file finishes uploading/processing, trigger a dimension fetch.
226
+ // getRemoteDimensions may fail if called too early (isImageRepresentationReady
227
+ // returns false while processing), so we wait for the ready state first.
228
+ var triggerDimensionFetch = function triggerDimensionFetch() {
229
+ // Find the media node in the doc by id and create a temporary
230
+ // MediaNodeUpdater to fetch and apply dimensions
231
+ var editorState = _this.view.state;
232
+ var mediaSingleType = editorState.schema.nodes.mediaSingle;
233
+ var mediaChildNode = null;
234
+ editorState.doc.descendants(function (node) {
235
+ if (mediaChildNode) {
236
+ return false;
237
+ }
238
+ if (node.type === mediaSingleType) {
239
+ var child = node.firstChild;
240
+ if (child && child.attrs.id === mediaState.id) {
241
+ mediaChildNode = child;
242
+ }
243
+ }
244
+ return true;
245
+ });
246
+ if (mediaChildNode) {
247
+ var _this$pluginInjection2;
248
+ var updater = (0, _mediaNodeUpdater.createMediaNodeUpdater)({
249
+ view: _this.view,
250
+ mediaProvider: _this.mediaProvider ? Promise.resolve(_this.mediaProvider) : undefined,
251
+ contextIdentifierProvider: _this.contextIdentifierProvider ? Promise.resolve(_this.contextIdentifierProvider) : undefined,
252
+ node: mediaChildNode,
253
+ isMediaSingle: true,
254
+ 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
255
+ });
256
+ updater.getRemoteDimensions().then(function (dims) {
257
+ if (dims) {
258
+ updater.updateDimensions(dims);
259
+ }
260
+ }).catch(function () {
261
+ // Silently ignore — if dimensions can't be fetched (e.g. network error),
262
+ // the image will render at its current size without the height-preserving
263
+ // width adjustment. This is an acceptable degraded experience.
264
+ });
265
+ }
266
+ };
267
+ if (!_isEndState(mediaStateWithContext)) {
268
+ var uploadingPromise = new Promise(function (resolve) {
269
+ onMediaStateChanged(function (newState) {
270
+ if (_isEndState(newState)) {
271
+ resolve(newState);
272
+ }
273
+ });
274
+ });
275
+ _this.taskManager.addPendingTask(uploadingPromise, mediaStateWithContext.id).then(function () {
276
+ _this.updateAndDispatch({
277
+ allUploadsFinished: true
278
+ });
279
+ triggerDimensionFetch();
280
+ });
281
+ } else {
282
+ // File is already in a resolved state — fetch dimensions immediately
283
+ triggerDimensionFetch();
284
+ }
285
+ var _view = _this.view;
286
+ if (!_view.hasFocus()) {
287
+ _view.focus();
288
+ }
289
+ return;
290
+ }
156
291
  switch ((0, _mediaInline2.getMediaNodeInsertionType)(state, _this.mediaOptions, mediaStateWithContext.fileMimeType)) {
157
292
  case 'inline':
158
293
  (0, _mediaFiles.insertMediaInlineNode)(editorAnalyticsAPI)(_this.view, mediaStateWithContext, collection, _this.allowInlineImages, _this.getInputMethod(pickerType), insertMediaVia);
159
294
  break;
160
295
  case 'block':
161
296
  // read width state right before inserting to get up-to-date and define values
162
- 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();
297
+ 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();
163
298
  (0, _mediaSingle2.insertMediaSingleNode)(_this.view, mediaStateWithContext, _this.getInputMethod(pickerType), collection, _this.mediaOptions && _this.mediaOptions.alignLeftOnInsert, widthPluginState, editorAnalyticsAPI, _this.onNodeInserted, insertMediaVia, _this.mediaOptions && _this.mediaOptions.allowPixelResizing);
164
299
  break;
165
300
  case 'group':
@@ -175,7 +310,7 @@ var MediaPluginStateImplementation = exports.MediaPluginStateImplementation = /*
175
310
  return state.status && MEDIA_RESOLVED_STATES.indexOf(state.status) !== -1;
176
311
  };
177
312
  if (!isEndState(mediaStateWithContext)) {
178
- var uploadingPromise = new Promise(function (resolve) {
313
+ var _uploadingPromise = new Promise(function (resolve) {
179
314
  onMediaStateChanged(function (newState) {
180
315
  // When media item reaches its final state, remove listener and resolve
181
316
  if (isEndState(newState)) {
@@ -184,7 +319,7 @@ var MediaPluginStateImplementation = exports.MediaPluginStateImplementation = /*
184
319
  });
185
320
  });
186
321
  if ((0, _platformFeatureFlags.fg)('platform_editor_media_disable_save_during_upload')) {
187
- _this.taskManager.addPendingTask(uploadingPromise, mediaStateWithContext.id).then(function () {
322
+ _this.taskManager.addPendingTask(_uploadingPromise, mediaStateWithContext.id).then(function () {
188
323
  _this.updateAndDispatch({
189
324
  allUploadsFinished: true
190
325
  });
@@ -198,7 +333,7 @@ var MediaPluginStateImplementation = exports.MediaPluginStateImplementation = /*
198
333
  });
199
334
  });
200
335
  } else {
201
- _this.taskManager.addPendingTask(uploadingPromise, mediaStateWithContext.id).then(function () {
336
+ _this.taskManager.addPendingTask(_uploadingPromise, mediaStateWithContext.id).then(function () {
202
337
  _this.updateAndDispatch({
203
338
  allUploadsFinished: true
204
339
  });
@@ -240,6 +375,64 @@ var MediaPluginStateImplementation = exports.MediaPluginStateImplementation = /*
240
375
  }
241
376
  _this.onPopupToggleCallback(true);
242
377
  });
378
+ /**
379
+ * Opens the media picker in "replace" mode. The next file selected/uploaded
380
+ * will replace the currently selected mediaSingle node's media child in-place,
381
+ * preserving layout, width, and caption.
382
+ *
383
+ * The display height is computed and stored so that after the new file's intrinsic
384
+ * dimensions are fetched, the mediaSingle display width can be adjusted to maintain
385
+ * visual height stability rather than width stability.
386
+ */
387
+ (0, _defineProperty2.default)(this, "showMediaPickerForReplace", function () {
388
+ var _this$pluginInjection4, _this$pluginInjection5;
389
+ var state = _this.view.state;
390
+ var mediaSingle = state.schema.nodes.mediaSingle;
391
+ var selection = state.selection;
392
+
393
+ // Only activate replace mode when a mediaSingle is selected
394
+ if (!(selection instanceof _state2.NodeSelection) || selection.node.type !== mediaSingle) {
395
+ return;
396
+ }
397
+ var mediaSingleNode = selection.node;
398
+ var mediaNode = mediaSingleNode.firstChild;
399
+ if (!mediaNode) {
400
+ return;
401
+ }
402
+
403
+ // Store the current media node's id so insertFile can identify and replace it
404
+ _this.replaceMediaFileId = mediaNode.attrs.id;
405
+
406
+ // Compute and store the current display height so we can preserve it after
407
+ // the new file's intrinsic dimensions are known.
408
+ // displayHeight = displayWidth * (intrinsicHeight / intrinsicWidth)
409
+ var widthAttr = mediaSingleNode.attrs.width;
410
+ var widthType = mediaSingleNode.attrs.widthType;
411
+ var intrinsicWidth = mediaNode.attrs.width;
412
+ var intrinsicHeight = mediaNode.attrs.height;
413
+
414
+ // Resolve actual pixel display width from mediaSingle attrs.
415
+ 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;
416
+ var displayWidth = null;
417
+ if (widthAttr && widthType === 'pixel') {
418
+ displayWidth = widthAttr;
419
+ } else if (widthAttr) {
420
+ // Default widthType is 'percentage' — convert to pixels
421
+ displayWidth = widthAttr / 100 * lineLength;
422
+ } else if (intrinsicWidth) {
423
+ // No width set at all (never resized) — fall back to intrinsic width
424
+ displayWidth = intrinsicWidth;
425
+ }
426
+ if (displayWidth && intrinsicWidth && intrinsicHeight && intrinsicWidth > 0) {
427
+ _this.replaceMediaTargetDisplayHeight = displayWidth * (intrinsicHeight / intrinsicWidth);
428
+ } else {
429
+ // Can't compute display height — fall back to preserving width
430
+ _this.replaceMediaTargetDisplayHeight = null;
431
+ }
432
+
433
+ // Finally, show the media picker
434
+ _this.showMediaPicker();
435
+ });
243
436
  (0, _defineProperty2.default)(this, "setBrowseFn", function (browseFn) {
244
437
  _this.openMediaPickerBrowser = browseFn;
245
438
  });
@@ -695,8 +888,8 @@ var MediaPluginStateImplementation = exports.MediaPluginStateImplementation = /*
695
888
  }, {
696
889
  key: "contextIdentifierProvider",
697
890
  get: function get() {
698
- var _this$pluginInjection3;
699
- 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;
891
+ var _this$pluginInjection6;
892
+ 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;
700
893
  }
701
894
  }, {
702
895
  key: "selectLastAddedMediaNode",
@@ -931,9 +1124,9 @@ var createPlugin = exports.createPlugin = function createPlugin(_schema, options
931
1124
  return;
932
1125
  },
933
1126
  key: _pluginKey.stateKey,
934
- view: function view(_view) {
935
- var pluginState = getMediaPluginState(_view.state);
936
- pluginState.setView(_view);
1127
+ view: function view(_view2) {
1128
+ var pluginState = getMediaPluginState(_view2.state);
1129
+ pluginState.setView(_view2);
937
1130
  pluginState.updateElement();
938
1131
  return {
939
1132
  update: function update() {
@@ -953,7 +1146,7 @@ var createPlugin = exports.createPlugin = function createPlugin(_schema, options
953
1146
  if (state.selection instanceof _state2.TextSelection || state.selection instanceof _state2.AllSelection || state.selection instanceof _cellSelection.CellSelection) {
954
1147
  doc.nodesBetween(state.selection.from, state.selection.to, function (node, pos) {
955
1148
  if (node.type === schema.nodes.media) {
956
- mediaNodes.push(_view2.Decoration.node(pos, pos + node.nodeSize, {}, {
1149
+ mediaNodes.push(_view3.Decoration.node(pos, pos + node.nodeSize, {}, {
957
1150
  type: 'media',
958
1151
  selected: true
959
1152
  }));
@@ -968,7 +1161,7 @@ var createPlugin = exports.createPlugin = function createPlugin(_schema, options
968
1161
  if (node.type === schema.nodes.mediaSingle || node.type === schema.nodes.mediaGroup) {
969
1162
  doc.nodesBetween($from.pos, $from.pos + node.nodeSize, function (mediaNode, mediaPos) {
970
1163
  if (mediaNode.type === schema.nodes.media) {
971
- mediaNodes.push(_view2.Decoration.node(mediaPos, mediaPos + mediaNode.nodeSize, {}, {
1164
+ mediaNodes.push(_view3.Decoration.node(mediaPos, mediaPos + mediaNode.nodeSize, {}, {
972
1165
  type: 'media',
973
1166
  selected: true
974
1167
  }));
@@ -980,30 +1173,30 @@ var createPlugin = exports.createPlugin = function createPlugin(_schema, options
980
1173
  }
981
1174
  var pluginState = getMediaPluginState(state);
982
1175
  if (!pluginState.showDropzone) {
983
- return _view2.DecorationSet.create(state.doc, mediaNodes);
1176
+ return _view3.DecorationSet.create(state.doc, mediaNodes);
984
1177
  }
985
1178
 
986
1179
  // When a media is already selected
987
1180
  if (state.selection instanceof _state2.NodeSelection) {
988
1181
  var _node = state.selection.node;
989
1182
  if (_node.type === schema.nodes.mediaSingle) {
990
- var deco = _view2.Decoration.node(state.selection.from, state.selection.to, {
1183
+ var deco = _view3.Decoration.node(state.selection.from, state.selection.to, {
991
1184
  class: 'richMedia-selected'
992
1185
  });
993
- return _view2.DecorationSet.create(state.doc, [deco].concat(mediaNodes));
1186
+ return _view3.DecorationSet.create(state.doc, [deco].concat(mediaNodes));
994
1187
  }
995
- return _view2.DecorationSet.create(state.doc, mediaNodes);
1188
+ return _view3.DecorationSet.create(state.doc, mediaNodes);
996
1189
  }
997
1190
  var pos = $anchor.pos;
998
1191
  if ($anchor.parent.type !== schema.nodes.paragraph && $anchor.parent.type !== schema.nodes.codeBlock) {
999
1192
  pos = (0, _transform.insertPoint)(state.doc, pos, schema.nodes.mediaGroup);
1000
1193
  }
1001
1194
  if (pos === null || pos === undefined) {
1002
- return _view2.DecorationSet.create(state.doc, mediaNodes);
1195
+ return _view3.DecorationSet.create(state.doc, mediaNodes);
1003
1196
  }
1004
1197
  // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
1005
1198
  var dropPlaceholderKey = (0, _uuid.default)();
1006
- var dropPlaceholders = [_view2.Decoration.widget(pos, function () {
1199
+ var dropPlaceholders = [_view3.Decoration.widget(pos, function () {
1007
1200
  return createDropPlaceholder(intl, nodeViewPortalProviderAPI, dropPlaceholderKey, mediaOptions && mediaOptions.allowDropzoneDropLine);
1008
1201
  }, {
1009
1202
  key: 'drop-placeholder',
@@ -1013,7 +1206,7 @@ var createPlugin = exports.createPlugin = function createPlugin(_schema, options
1013
1206
  }
1014
1207
  }
1015
1208
  })].concat(mediaNodes);
1016
- return _view2.DecorationSet.create(state.doc, dropPlaceholders);
1209
+ return _view3.DecorationSet.create(state.doc, dropPlaceholders);
1017
1210
  },
1018
1211
  nodeViews: options.nodeViews,
1019
1212
  handleTextInput: function handleTextInput(view, from, to, text) {
@@ -29,6 +29,7 @@ var _imageFullscreen = _interopRequireDefault(require("@atlaskit/icon/core/image
29
29
  var _imageInline = _interopRequireDefault(require("@atlaskit/icon/core/image-inline"));
30
30
  var _maximize = _interopRequireDefault(require("@atlaskit/icon/core/maximize"));
31
31
  var _smartLinkCard = _interopRequireDefault(require("@atlaskit/icon/core/smart-link-card"));
32
+ var _upload = _interopRequireDefault(require("@atlaskit/icon/core/upload"));
32
33
  var _mediaFilmstrip = require("@atlaskit/media-filmstrip");
33
34
  var _mediaUi = require("@atlaskit/media-ui");
34
35
  var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
@@ -892,6 +893,7 @@ var floatingToolbar = exports.floatingToolbar = function floatingToolbar(state,
892
893
  if (!mediaPluginState.isResizing && (0, _toolbarFlagCheck.areToolbarFlagsEnabled)(Boolean(pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : pluginInjectionApi.toolbar))) {
893
894
  var _pluginInjectionApi$a0, _pluginInjectionApi$a1, _pluginInjectionApi$a10;
894
895
  (0, _utils2.updateToFullHeightSeparator)(items);
896
+ var showReplaceOption = !isViewOnly && mediaPluginState.allowsUploads && (0, _expValEquals.expValEquals)('platform_editor_inline_media_replacement', 'isEnabled', true) && selectedNodeType === mediaSingle;
895
897
  var customOptions = [].concat((0, _toConsumableArray2.default)((0, _linking3.getLinkingDropdownOptions)(state, intl, mediaLinkingState, allowMediaInline && selectedNodeType && selectedNodeType === mediaInline, allowLinking, isViewOnly)), (0, _toConsumableArray2.default)((0, _altText2.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)), (0, _toConsumableArray2.default)((0, _pixelResizing2.getResizeDropdownOption)(options, state, intl.formatMessage, selectedNodeType)));
896
898
  if (customOptions.length) {
897
899
  customOptions.push({
@@ -910,7 +912,17 @@ var floatingToolbar = exports.floatingToolbar = function floatingToolbar(state,
910
912
  type: 'overflow-dropdown',
911
913
  id: 'media',
912
914
  testId: overflowDropdwonBtnTriggerTestId,
913
- options: [].concat((0, _toConsumableArray2.default)(customOptions), [_objectSpread({
915
+ options: [].concat((0, _toConsumableArray2.default)(customOptions), (0, _toConsumableArray2.default)(showReplaceOption ? [{
916
+ title: intl.formatMessage(_messages.mediaAndEmbedToolbarMessages.replaceMedia),
917
+ onClick: function onClick() {
918
+ mediaPluginState.showMediaPickerForReplace();
919
+ return true;
920
+ },
921
+ icon: /*#__PURE__*/_react.default.createElement(_upload.default, {
922
+ label: ""
923
+ }),
924
+ testId: 'media-replace-toolbar-button'
925
+ }] : []), [_objectSpread({
914
926
  title: intl === null || intl === void 0 ? void 0 : intl.formatMessage(_messages.default.copyToClipboard),
915
927
  onClick: function onClick() {
916
928
  var _pluginInjectionApi$c4, _pluginInjectionApi$f3;
@@ -1,3 +1,7 @@
1
1
  export const hasPrivateAttrsChanged = (currentAttrs, newAttrs) => {
2
- return currentAttrs.__fileName !== newAttrs.__fileName || currentAttrs.__fileMimeType !== newAttrs.__fileMimeType || currentAttrs.__fileSize !== newAttrs.__fileSize || currentAttrs.__contextId !== newAttrs.__contextId;
2
+ return currentAttrs.__fileName !== newAttrs.__fileName || currentAttrs.__fileMimeType !== newAttrs.__fileMimeType || currentAttrs.__fileSize !== newAttrs.__fileSize || currentAttrs.__contextId !== newAttrs.__contextId ||
3
+ // A changed id means the media source was replaced — always re-initialise
4
+ // the updater so getRemoteDimensions fetches dimensions for the new file.
5
+ // Only check when id is explicitly present on the new attrs (it's Partial).
6
+ 'id' in newAttrs && currentAttrs.id !== newAttrs.id;
3
7
  };