@atlaskit/editor-plugin-media-insert 2.7.3 → 2.8.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-insert
2
2
 
3
+ ## 2.8.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#143644](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/pull-requests/143644)
8
+ [`b5352e3195293`](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/commits/b5352e3195293) -
9
+ [ux] [ED-24327] Integrate new media popup into toolbar and quick action
10
+ - Updated dependencies
11
+
12
+ ## 2.8.0
13
+
14
+ ### Minor Changes
15
+
16
+ - [#142802](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/pull-requests/142802)
17
+ [`09c0d0a18b491`](https://stash.atlassian.com/projects/CONFCLOUD/repos/confluence-frontend/commits/09c0d0a18b491) -
18
+ [ux] [ED-24935] [ED24921] Add URL validation and enforce max length for input field
19
+
3
20
  ## 2.7.3
4
21
 
5
22
  ### Patch Changes
@@ -40,16 +40,22 @@ var mediaInsertPlugin = exports.mediaInsertPlugin = function mediaInsertPlugin(_
40
40
  isOpen: isOpen
41
41
  };
42
42
  },
43
- contentComponent: function contentComponent(_ref3) {
44
- var editorView = _ref3.editorView,
45
- dispatchAnalyticsEvent = _ref3.dispatchAnalyticsEvent,
46
- popupsMountPoint = _ref3.popupsMountPoint,
47
- popupsBoundariesElement = _ref3.popupsBoundariesElement,
48
- popupsScrollableElement = _ref3.popupsScrollableElement;
49
- var insertMediaSingle = function insertMediaSingle(_ref4) {
43
+ commands: {
44
+ showMediaInsertPopup: function showMediaInsertPopup(_ref3) {
45
+ var tr = _ref3.tr;
46
+ return (0, _actions.showMediaInsertPopup)(tr);
47
+ }
48
+ },
49
+ contentComponent: function contentComponent(_ref4) {
50
+ var editorView = _ref4.editorView,
51
+ dispatchAnalyticsEvent = _ref4.dispatchAnalyticsEvent,
52
+ popupsMountPoint = _ref4.popupsMountPoint,
53
+ popupsBoundariesElement = _ref4.popupsBoundariesElement,
54
+ popupsScrollableElement = _ref4.popupsScrollableElement;
55
+ var insertMediaSingle = function insertMediaSingle(_ref5) {
50
56
  var _api$media$actions$in;
51
- var mediaState = _ref4.mediaState,
52
- inputMethod = _ref4.inputMethod;
57
+ var mediaState = _ref5.mediaState,
58
+ inputMethod = _ref5.inputMethod;
53
59
  var id = mediaState.id,
54
60
  dimensions = mediaState.dimensions,
55
61
  contextId = mediaState.contextId,
@@ -57,12 +63,12 @@ var mediaInsertPlugin = exports.mediaInsertPlugin = function mediaInsertPlugin(_
57
63
  scaleFactor = _mediaState$scaleFact === void 0 ? 1 : _mediaState$scaleFact,
58
64
  fileName = mediaState.fileName,
59
65
  collection = mediaState.collection;
60
- var _ref5 = dimensions || {
66
+ var _ref6 = dimensions || {
61
67
  height: undefined,
62
68
  width: undefined
63
69
  },
64
- width = _ref5.width,
65
- height = _ref5.height;
70
+ width = _ref6.width,
71
+ height = _ref6.height;
66
72
  var scaledWidth = width && Math.round(width / scaleFactor);
67
73
  var node = editorView.state.schema.nodes.media.create({
68
74
  id: id,
@@ -76,11 +82,11 @@ var mediaInsertPlugin = exports.mediaInsertPlugin = function mediaInsertPlugin(_
76
82
  });
77
83
  return (_api$media$actions$in = api === null || api === void 0 ? void 0 : api.media.actions.insertMediaAsMediaSingle(editorView, node, inputMethod, isNestingInQuoteSupported)) !== null && _api$media$actions$in !== void 0 ? _api$media$actions$in : false;
78
84
  };
79
- var insertExternalMediaSingle = function insertExternalMediaSingle(_ref6) {
85
+ var insertExternalMediaSingle = function insertExternalMediaSingle(_ref7) {
80
86
  var _api$media$actions$in2;
81
- var url = _ref6.url,
82
- alt = _ref6.alt,
83
- inputMethod = _ref6.inputMethod;
87
+ var url = _ref7.url,
88
+ alt = _ref7.alt,
89
+ inputMethod = _ref7.inputMethod;
84
90
  var node = editorView.state.schema.nodes.media.create({
85
91
  type: 'external',
86
92
  url: url,
@@ -89,11 +95,11 @@ var mediaInsertPlugin = exports.mediaInsertPlugin = function mediaInsertPlugin(_
89
95
  });
90
96
  return (_api$media$actions$in2 = api === null || api === void 0 ? void 0 : api.media.actions.insertMediaAsMediaSingle(editorView, node, inputMethod, isNestingInQuoteSupported)) !== null && _api$media$actions$in2 !== void 0 ? _api$media$actions$in2 : false;
91
97
  };
92
- var insertFile = function insertFile(_ref7) {
98
+ var insertFile = function insertFile(_ref8) {
93
99
  var _api$media$sharedStat, _api$media$sharedStat2;
94
- var mediaState = _ref7.mediaState,
95
- inputMethod = _ref7.inputMethod,
96
- onMediaStateChanged = _ref7.onMediaStateChanged;
100
+ var mediaState = _ref8.mediaState,
101
+ inputMethod = _ref8.inputMethod,
102
+ onMediaStateChanged = _ref8.onMediaStateChanged;
97
103
  var collection = mediaState.collection;
98
104
  return collection !== undefined ? (_api$media$sharedStat = api === null || api === void 0 || (_api$media$sharedStat2 = api.media.sharedState.currentState()) === null || _api$media$sharedStat2 === void 0 ? void 0 : _api$media$sharedStat2.insertFile(mediaState, onMediaStateChanged, inputMethod)) !== null && _api$media$sharedStat !== void 0 ? _api$media$sharedStat : false : false;
99
105
  };
@@ -113,14 +119,14 @@ var mediaInsertPlugin = exports.mediaInsertPlugin = function mediaInsertPlugin(_
113
119
  });
114
120
  },
115
121
  pluginsOptions: {
116
- quickInsert: function quickInsert(_ref8) {
117
- var formatMessage = _ref8.formatMessage;
122
+ quickInsert: function quickInsert(_ref9) {
123
+ var formatMessage = _ref9.formatMessage;
118
124
  return [{
119
125
  id: 'media-insert',
120
- title: formatMessage(_messages.toolbarInsertBlockMessages.insertMediaFromUrl),
121
- description: formatMessage(_messages.toolbarInsertBlockMessages.insertMediaFromUrlDescription),
126
+ title: formatMessage(_messages.toolbarInsertBlockMessages.mediaFiles),
127
+ description: formatMessage(_messages.toolbarInsertBlockMessages.mediaFilesDescription),
122
128
  priority: 400,
123
- keywords: ['attachment', 'gif', 'media', 'picture', 'image', 'video'],
129
+ keywords: ['attachment', 'gif', 'media', 'picture', 'image', 'video', 'file'],
124
130
  icon: function icon() {
125
131
  return /*#__PURE__*/_react.default.createElement(_quickInsert.IconImages, null);
126
132
  },
@@ -129,7 +135,9 @@ var mediaInsertPlugin = exports.mediaInsertPlugin = function mediaInsertPlugin(_
129
135
  // Insert empty string to remove the typeahead raw text
130
136
  // close the quick insert immediately
131
137
  var tr = insert('');
132
- (0, _actions.showMediaInsertPopup)(tr);
138
+ api === null || api === void 0 || api.mediaInsert.commands.showMediaInsertPopup({
139
+ tr: tr
140
+ });
133
141
  api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || (_api$analytics = _api$analytics.actions) === null || _api$analytics === void 0 || _api$analytics.attachAnalyticsEvent({
134
142
  action: _analytics.ACTION.OPENED,
135
143
  actionSubject: _analytics.ACTION_SUBJECT.PICKER,
@@ -42,6 +42,7 @@ var MediaCard = exports.MediaCard = function MediaCard(_ref) {
42
42
  dimensions: maxDimensions,
43
43
  originalDimensions: dimensions,
44
44
  identifier: identifier,
45
- alt: mediaAlt
45
+ alt: mediaAlt,
46
+ disableOverlay: true
46
47
  });
47
48
  };
@@ -5,16 +5,20 @@ Object.defineProperty(exports, "__esModule", {
5
5
  value: true
6
6
  });
7
7
  exports.MediaFromURL = MediaFromURL;
8
+ exports.isValidUrl = void 0;
8
9
  var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator"));
10
+ var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends"));
9
11
  var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));
10
12
  var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray"));
11
13
  var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
12
14
  var _react = _interopRequireDefault(require("react"));
13
15
  var _reactIntlNext = require("react-intl-next");
16
+ var _adfSchema = require("@atlaskit/adf-schema");
14
17
  var _buttonGroup = _interopRequireDefault(require("@atlaskit/button/button-group"));
15
18
  var _new = _interopRequireDefault(require("@atlaskit/button/new"));
16
19
  var _analytics = require("@atlaskit/editor-common/analytics");
17
20
  var _messages = require("@atlaskit/editor-common/messages");
21
+ var _form = require("@atlaskit/form");
18
22
  var _filePreview = _interopRequireDefault(require("@atlaskit/icon/glyph/editor/file-preview"));
19
23
  var _mediaClientReact = require("@atlaskit/media-client-react");
20
24
  var _primitives = require("@atlaskit/primitives");
@@ -46,6 +50,19 @@ var INITIAL_PREVIEW_STATE = Object.freeze({
46
50
  warning: null,
47
51
  previewInfo: null
48
52
  });
53
+ var MAX_URL_LENGTH = 2048;
54
+ var isValidUrl = exports.isValidUrl = function isValidUrl(value) {
55
+ try {
56
+ // Check for spaces and length first to avoid the expensive URL parsing
57
+ if (/\s/.test(value) || value.length > MAX_URL_LENGTH) {
58
+ return false;
59
+ }
60
+ new URL(value);
61
+ } catch (e) {
62
+ return false;
63
+ }
64
+ return (0, _adfSchema.isSafeUrl)(value);
65
+ };
49
66
  var previewStateReducer = function previewStateReducer(state, action) {
50
67
  switch (action.type) {
51
68
  case 'loading':
@@ -83,16 +100,25 @@ function MediaFromURL(_ref) {
83
100
  pasteLinkToUpload: intl.formatMessage(_messages.mediaInsertMessages.pasteLinkToUpload),
84
101
  cancel: intl.formatMessage(_messages.mediaInsertMessages.cancel),
85
102
  errorMessage: intl.formatMessage(_messages.mediaInsertMessages.fromUrlErrorMessage),
86
- warning: intl.formatMessage(_messages.mediaInsertMessages.fromUrlWarning)
103
+ warning: intl.formatMessage(_messages.mediaInsertMessages.fromUrlWarning),
104
+ invalidUrl: intl.formatMessage(_messages.mediaInsertMessages.invalidUrlErrorMessage)
87
105
  };
88
106
  var _React$useState = _react.default.useState(''),
89
107
  _React$useState2 = (0, _slicedToArray2.default)(_React$useState, 2),
90
108
  inputUrl = _React$useState2[0],
91
109
  setUrl = _React$useState2[1];
110
+ var _React$useState3 = _react.default.useState(),
111
+ _React$useState4 = (0, _slicedToArray2.default)(_React$useState3, 2),
112
+ hasUrlError = _React$useState4[0],
113
+ setHasUrlError = _React$useState4[1];
92
114
  var _React$useReducer = _react.default.useReducer(previewStateReducer, INITIAL_PREVIEW_STATE),
93
115
  _React$useReducer2 = (0, _slicedToArray2.default)(_React$useReducer, 2),
94
116
  previewState = _React$useReducer2[0],
95
117
  dispatch = _React$useReducer2[1];
118
+ var _React$useState5 = _react.default.useState(false),
119
+ _React$useState6 = (0, _slicedToArray2.default)(_React$useState5, 2),
120
+ isInputFocused = _React$useState6[0],
121
+ setInputFocused = _React$useState6[1];
96
122
  var pasteFlag = _react.default.useRef(false);
97
123
  var _useAnalyticsEvents = (0, _useAnalyticsEvents2.useAnalyticsEvents)(dispatchAnalyticsEvent),
98
124
  onUploadButtonClickedAnalytics = _useAnalyticsEvents.onUploadButtonClickedAnalytics,
@@ -178,12 +204,32 @@ function MediaFromURL(_ref) {
178
204
  return _ref2.apply(this, arguments);
179
205
  };
180
206
  }(), [onUploadButtonClickedAnalytics, mediaProvider, onUploadCommencedAnalytics, onUploadSuccessAnalytics, onUploadFailureAnalytics, inputUrl]);
207
+ var errorAttributes = {};
208
+ if (!isInputFocused) {
209
+ errorAttributes['aria-relevant'] = 'all';
210
+ errorAttributes['aria-atomic'] = 'false';
211
+ }
212
+ var onBlur = _react.default.useCallback(function () {
213
+ if (!isValidUrl(inputUrl)) {
214
+ setHasUrlError(true);
215
+ }
216
+ setInputFocused(false);
217
+ }, [inputUrl]);
218
+ var onFocus = _react.default.useCallback(function () {
219
+ setInputFocused(true);
220
+ }, []);
181
221
  var onURLChange = _react.default.useCallback(function (e) {
182
222
  var url = e.target.value;
183
223
  setUrl(url);
184
224
  dispatch({
185
225
  type: 'reset'
186
226
  });
227
+ if (!isValidUrl(url)) {
228
+ setHasUrlError(true);
229
+ return;
230
+ } else {
231
+ setHasUrlError(false);
232
+ }
187
233
  if (pasteFlag.current === true) {
188
234
  pasteFlag.current = false;
189
235
  uploadExternalMedia(url);
@@ -193,7 +239,7 @@ function MediaFromURL(_ref) {
193
239
  // Note: this is a little weird, but the paste event will always be
194
240
  // fired before the change event when pasting. We don't really want to
195
241
  // duplicate logic by handling pastes separately to changes, so we're
196
- // just noting paste occured to then be handled in the onURLChange fn
242
+ // just noting paste occurred to then be handled in the onURLChange fn
197
243
  // above. The one exception to this is where paste inputs exactly what was
198
244
  // already in the input, in which case we want to ignore it.
199
245
  if (e.clipboardData.getData('text') !== inputUrl) {
@@ -263,7 +309,7 @@ function MediaFromURL(_ref) {
263
309
  // This can be triggered from an enter key event on the input even when
264
310
  // the button is disabled, so we explicitly do nothing when in loading
265
311
  // state.
266
- if (previewState.isLoading) {
312
+ if (previewState.isLoading || hasUrlError) {
267
313
  return;
268
314
  }
269
315
  if (previewState.previewInfo) {
@@ -281,10 +327,16 @@ function MediaFromURL(_ref) {
281
327
  }, /*#__PURE__*/_react.default.createElement(_textfield.default, {
282
328
  value: inputUrl,
283
329
  placeholder: strings.pasteLinkToUpload,
330
+ isInvalid: hasUrlError,
331
+ maxLength: MAX_URL_LENGTH,
284
332
  onChange: onURLChange,
285
333
  onKeyPress: onInputKeyPress,
286
- onPaste: onPaste
287
- }), previewState.previewInfo && /*#__PURE__*/_react.default.createElement(_primitives.Inline, {
334
+ onPaste: onPaste,
335
+ onBlur: onBlur,
336
+ onFocus: onFocus
337
+ }), hasUrlError && /*#__PURE__*/_react.default.createElement(_form.ErrorMessage, null, /*#__PURE__*/_react.default.createElement(_primitives.Box, (0, _extends2.default)({
338
+ as: "span"
339
+ }, errorAttributes), strings.invalidUrl)), previewState.previewInfo && /*#__PURE__*/_react.default.createElement(_primitives.Inline, {
288
340
  alignInline: "center",
289
341
  alignBlock: "center",
290
342
  xcss: PreviewImageStyles,
@@ -303,7 +355,7 @@ function MediaFromURL(_ref) {
303
355
  }, /*#__PURE__*/_react.default.createElement(_new.default, {
304
356
  type: "submit",
305
357
  isLoading: previewState.isLoading,
306
- isDisabled: inputUrl.length === 0,
358
+ isDisabled: inputUrl.length === 0 || hasUrlError,
307
359
  iconBefore: _filePreview.default
308
360
  }, strings.loadPreview)), /*#__PURE__*/_react.default.createElement(_primitives.Box, {
309
361
  xcss: ButtonGroupStyles
@@ -33,6 +33,11 @@ export const mediaInsertPlugin = ({
33
33
  isOpen
34
34
  };
35
35
  },
36
+ commands: {
37
+ showMediaInsertPopup: ({
38
+ tr
39
+ }) => showMediaInsertPopup(tr)
40
+ },
36
41
  contentComponent: ({
37
42
  editorView,
38
43
  dispatchAnalyticsEvent,
@@ -114,17 +119,19 @@ export const mediaInsertPlugin = ({
114
119
  formatMessage
115
120
  }) => [{
116
121
  id: 'media-insert',
117
- title: formatMessage(messages.insertMediaFromUrl),
118
- description: formatMessage(messages.insertMediaFromUrlDescription),
122
+ title: formatMessage(messages.mediaFiles),
123
+ description: formatMessage(messages.mediaFilesDescription),
119
124
  priority: 400,
120
- keywords: ['attachment', 'gif', 'media', 'picture', 'image', 'video'],
125
+ keywords: ['attachment', 'gif', 'media', 'picture', 'image', 'video', 'file'],
121
126
  icon: () => /*#__PURE__*/React.createElement(IconImages, null),
122
127
  action(insert) {
123
128
  var _api$analytics, _api$analytics$action;
124
129
  // Insert empty string to remove the typeahead raw text
125
130
  // close the quick insert immediately
126
131
  const tr = insert('');
127
- showMediaInsertPopup(tr);
132
+ api === null || api === void 0 ? void 0 : api.mediaInsert.commands.showMediaInsertPopup({
133
+ tr
134
+ });
128
135
  api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : (_api$analytics$action = _api$analytics.actions) === null || _api$analytics$action === void 0 ? void 0 : _api$analytics$action.attachAnalyticsEvent({
129
136
  action: ACTION.OPENED,
130
137
  actionSubject: ACTION_SUBJECT.PICKER,
@@ -34,6 +34,7 @@ export const MediaCard = ({
34
34
  dimensions: maxDimensions,
35
35
  originalDimensions: dimensions,
36
36
  identifier: identifier,
37
- alt: mediaAlt
37
+ alt: mediaAlt,
38
+ disableOverlay: true
38
39
  });
39
40
  };
@@ -1,9 +1,12 @@
1
+ import _extends from "@babel/runtime/helpers/extends";
1
2
  import React from 'react';
2
3
  import { useIntl } from 'react-intl-next';
4
+ import { isSafeUrl } from '@atlaskit/adf-schema';
3
5
  import ButtonGroup from '@atlaskit/button/button-group';
4
6
  import Button from '@atlaskit/button/new';
5
7
  import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
6
8
  import { mediaInsertMessages } from '@atlaskit/editor-common/messages';
9
+ import { ErrorMessage } from '@atlaskit/form';
7
10
  import EditorFilePreviewIcon from '@atlaskit/icon/glyph/editor/file-preview';
8
11
  import { getMediaClient } from '@atlaskit/media-client-react';
9
12
  import { Box, Flex, Inline, Stack, xcss } from '@atlaskit/primitives';
@@ -33,6 +36,19 @@ const INITIAL_PREVIEW_STATE = Object.freeze({
33
36
  warning: null,
34
37
  previewInfo: null
35
38
  });
39
+ const MAX_URL_LENGTH = 2048;
40
+ export const isValidUrl = value => {
41
+ try {
42
+ // Check for spaces and length first to avoid the expensive URL parsing
43
+ if (/\s/.test(value) || value.length > MAX_URL_LENGTH) {
44
+ return false;
45
+ }
46
+ new URL(value);
47
+ } catch (e) {
48
+ return false;
49
+ }
50
+ return isSafeUrl(value);
51
+ };
36
52
  const previewStateReducer = (state, action) => {
37
53
  switch (action.type) {
38
54
  case 'loading':
@@ -75,10 +91,13 @@ export function MediaFromURL({
75
91
  pasteLinkToUpload: intl.formatMessage(mediaInsertMessages.pasteLinkToUpload),
76
92
  cancel: intl.formatMessage(mediaInsertMessages.cancel),
77
93
  errorMessage: intl.formatMessage(mediaInsertMessages.fromUrlErrorMessage),
78
- warning: intl.formatMessage(mediaInsertMessages.fromUrlWarning)
94
+ warning: intl.formatMessage(mediaInsertMessages.fromUrlWarning),
95
+ invalidUrl: intl.formatMessage(mediaInsertMessages.invalidUrlErrorMessage)
79
96
  };
80
97
  const [inputUrl, setUrl] = React.useState('');
98
+ const [hasUrlError, setHasUrlError] = React.useState();
81
99
  const [previewState, dispatch] = React.useReducer(previewStateReducer, INITIAL_PREVIEW_STATE);
100
+ const [isInputFocused, setInputFocused] = React.useState(false);
82
101
  const pasteFlag = React.useRef(false);
83
102
  const {
84
103
  onUploadButtonClickedAnalytics,
@@ -146,12 +165,32 @@ export function MediaFromURL({
146
165
  }
147
166
  }
148
167
  }, [onUploadButtonClickedAnalytics, mediaProvider, onUploadCommencedAnalytics, onUploadSuccessAnalytics, onUploadFailureAnalytics, inputUrl]);
168
+ const errorAttributes = {};
169
+ if (!isInputFocused) {
170
+ errorAttributes['aria-relevant'] = 'all';
171
+ errorAttributes['aria-atomic'] = 'false';
172
+ }
173
+ const onBlur = React.useCallback(() => {
174
+ if (!isValidUrl(inputUrl)) {
175
+ setHasUrlError(true);
176
+ }
177
+ setInputFocused(false);
178
+ }, [inputUrl]);
179
+ const onFocus = React.useCallback(() => {
180
+ setInputFocused(true);
181
+ }, []);
149
182
  const onURLChange = React.useCallback(e => {
150
183
  const url = e.target.value;
151
184
  setUrl(url);
152
185
  dispatch({
153
186
  type: 'reset'
154
187
  });
188
+ if (!isValidUrl(url)) {
189
+ setHasUrlError(true);
190
+ return;
191
+ } else {
192
+ setHasUrlError(false);
193
+ }
155
194
  if (pasteFlag.current === true) {
156
195
  pasteFlag.current = false;
157
196
  uploadExternalMedia(url);
@@ -161,7 +200,7 @@ export function MediaFromURL({
161
200
  // Note: this is a little weird, but the paste event will always be
162
201
  // fired before the change event when pasting. We don't really want to
163
202
  // duplicate logic by handling pastes separately to changes, so we're
164
- // just noting paste occured to then be handled in the onURLChange fn
203
+ // just noting paste occurred to then be handled in the onURLChange fn
165
204
  // above. The one exception to this is where paste inputs exactly what was
166
205
  // already in the input, in which case we want to ignore it.
167
206
  if (e.clipboardData.getData('text') !== inputUrl) {
@@ -231,7 +270,7 @@ export function MediaFromURL({
231
270
  // This can be triggered from an enter key event on the input even when
232
271
  // the button is disabled, so we explicitly do nothing when in loading
233
272
  // state.
234
- if (previewState.isLoading) {
273
+ if (previewState.isLoading || hasUrlError) {
235
274
  return;
236
275
  }
237
276
  if (previewState.previewInfo) {
@@ -249,10 +288,16 @@ export function MediaFromURL({
249
288
  }, /*#__PURE__*/React.createElement(TextField, {
250
289
  value: inputUrl,
251
290
  placeholder: strings.pasteLinkToUpload,
291
+ isInvalid: hasUrlError,
292
+ maxLength: MAX_URL_LENGTH,
252
293
  onChange: onURLChange,
253
294
  onKeyPress: onInputKeyPress,
254
- onPaste: onPaste
255
- }), previewState.previewInfo && /*#__PURE__*/React.createElement(Inline, {
295
+ onPaste: onPaste,
296
+ onBlur: onBlur,
297
+ onFocus: onFocus
298
+ }), hasUrlError && /*#__PURE__*/React.createElement(ErrorMessage, null, /*#__PURE__*/React.createElement(Box, _extends({
299
+ as: "span"
300
+ }, errorAttributes), strings.invalidUrl)), previewState.previewInfo && /*#__PURE__*/React.createElement(Inline, {
256
301
  alignInline: "center",
257
302
  alignBlock: "center",
258
303
  xcss: PreviewImageStyles,
@@ -271,7 +316,7 @@ export function MediaFromURL({
271
316
  }, /*#__PURE__*/React.createElement(Button, {
272
317
  type: "submit",
273
318
  isLoading: previewState.isLoading,
274
- isDisabled: inputUrl.length === 0,
319
+ isDisabled: inputUrl.length === 0 || hasUrlError,
275
320
  iconBefore: EditorFilePreviewIcon
276
321
  }, strings.loadPreview)), /*#__PURE__*/React.createElement(Box, {
277
322
  xcss: ButtonGroupStyles
@@ -3,7 +3,7 @@ import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } f
3
3
  import { toolbarInsertBlockMessages as messages } from '@atlaskit/editor-common/messages';
4
4
  import { IconImages } from '@atlaskit/editor-common/quick-insert';
5
5
  import { fg } from '@atlaskit/platform-feature-flags';
6
- import { closeMediaInsertPicker as _closeMediaInsertPicker, showMediaInsertPopup } from './actions';
6
+ import { closeMediaInsertPicker as _closeMediaInsertPicker, showMediaInsertPopup as _showMediaInsertPopup } from './actions';
7
7
  import { createPlugin } from './pm-plugins/main';
8
8
  import { pluginKey } from './pm-plugins/plugin-key';
9
9
  import { MediaInsertPicker } from './ui/MediaInsertPicker';
@@ -33,16 +33,22 @@ export var mediaInsertPlugin = function mediaInsertPlugin(_ref) {
33
33
  isOpen: isOpen
34
34
  };
35
35
  },
36
- contentComponent: function contentComponent(_ref3) {
37
- var editorView = _ref3.editorView,
38
- dispatchAnalyticsEvent = _ref3.dispatchAnalyticsEvent,
39
- popupsMountPoint = _ref3.popupsMountPoint,
40
- popupsBoundariesElement = _ref3.popupsBoundariesElement,
41
- popupsScrollableElement = _ref3.popupsScrollableElement;
42
- var insertMediaSingle = function insertMediaSingle(_ref4) {
36
+ commands: {
37
+ showMediaInsertPopup: function showMediaInsertPopup(_ref3) {
38
+ var tr = _ref3.tr;
39
+ return _showMediaInsertPopup(tr);
40
+ }
41
+ },
42
+ contentComponent: function contentComponent(_ref4) {
43
+ var editorView = _ref4.editorView,
44
+ dispatchAnalyticsEvent = _ref4.dispatchAnalyticsEvent,
45
+ popupsMountPoint = _ref4.popupsMountPoint,
46
+ popupsBoundariesElement = _ref4.popupsBoundariesElement,
47
+ popupsScrollableElement = _ref4.popupsScrollableElement;
48
+ var insertMediaSingle = function insertMediaSingle(_ref5) {
43
49
  var _api$media$actions$in;
44
- var mediaState = _ref4.mediaState,
45
- inputMethod = _ref4.inputMethod;
50
+ var mediaState = _ref5.mediaState,
51
+ inputMethod = _ref5.inputMethod;
46
52
  var id = mediaState.id,
47
53
  dimensions = mediaState.dimensions,
48
54
  contextId = mediaState.contextId,
@@ -50,12 +56,12 @@ export var mediaInsertPlugin = function mediaInsertPlugin(_ref) {
50
56
  scaleFactor = _mediaState$scaleFact === void 0 ? 1 : _mediaState$scaleFact,
51
57
  fileName = mediaState.fileName,
52
58
  collection = mediaState.collection;
53
- var _ref5 = dimensions || {
59
+ var _ref6 = dimensions || {
54
60
  height: undefined,
55
61
  width: undefined
56
62
  },
57
- width = _ref5.width,
58
- height = _ref5.height;
63
+ width = _ref6.width,
64
+ height = _ref6.height;
59
65
  var scaledWidth = width && Math.round(width / scaleFactor);
60
66
  var node = editorView.state.schema.nodes.media.create({
61
67
  id: id,
@@ -69,11 +75,11 @@ export var mediaInsertPlugin = function mediaInsertPlugin(_ref) {
69
75
  });
70
76
  return (_api$media$actions$in = api === null || api === void 0 ? void 0 : api.media.actions.insertMediaAsMediaSingle(editorView, node, inputMethod, isNestingInQuoteSupported)) !== null && _api$media$actions$in !== void 0 ? _api$media$actions$in : false;
71
77
  };
72
- var insertExternalMediaSingle = function insertExternalMediaSingle(_ref6) {
78
+ var insertExternalMediaSingle = function insertExternalMediaSingle(_ref7) {
73
79
  var _api$media$actions$in2;
74
- var url = _ref6.url,
75
- alt = _ref6.alt,
76
- inputMethod = _ref6.inputMethod;
80
+ var url = _ref7.url,
81
+ alt = _ref7.alt,
82
+ inputMethod = _ref7.inputMethod;
77
83
  var node = editorView.state.schema.nodes.media.create({
78
84
  type: 'external',
79
85
  url: url,
@@ -82,11 +88,11 @@ export var mediaInsertPlugin = function mediaInsertPlugin(_ref) {
82
88
  });
83
89
  return (_api$media$actions$in2 = api === null || api === void 0 ? void 0 : api.media.actions.insertMediaAsMediaSingle(editorView, node, inputMethod, isNestingInQuoteSupported)) !== null && _api$media$actions$in2 !== void 0 ? _api$media$actions$in2 : false;
84
90
  };
85
- var insertFile = function insertFile(_ref7) {
91
+ var insertFile = function insertFile(_ref8) {
86
92
  var _api$media$sharedStat, _api$media$sharedStat2;
87
- var mediaState = _ref7.mediaState,
88
- inputMethod = _ref7.inputMethod,
89
- onMediaStateChanged = _ref7.onMediaStateChanged;
93
+ var mediaState = _ref8.mediaState,
94
+ inputMethod = _ref8.inputMethod,
95
+ onMediaStateChanged = _ref8.onMediaStateChanged;
90
96
  var collection = mediaState.collection;
91
97
  return collection !== undefined ? (_api$media$sharedStat = api === null || api === void 0 || (_api$media$sharedStat2 = api.media.sharedState.currentState()) === null || _api$media$sharedStat2 === void 0 ? void 0 : _api$media$sharedStat2.insertFile(mediaState, onMediaStateChanged, inputMethod)) !== null && _api$media$sharedStat !== void 0 ? _api$media$sharedStat : false : false;
92
98
  };
@@ -106,14 +112,14 @@ export var mediaInsertPlugin = function mediaInsertPlugin(_ref) {
106
112
  });
107
113
  },
108
114
  pluginsOptions: {
109
- quickInsert: function quickInsert(_ref8) {
110
- var formatMessage = _ref8.formatMessage;
115
+ quickInsert: function quickInsert(_ref9) {
116
+ var formatMessage = _ref9.formatMessage;
111
117
  return [{
112
118
  id: 'media-insert',
113
- title: formatMessage(messages.insertMediaFromUrl),
114
- description: formatMessage(messages.insertMediaFromUrlDescription),
119
+ title: formatMessage(messages.mediaFiles),
120
+ description: formatMessage(messages.mediaFilesDescription),
115
121
  priority: 400,
116
- keywords: ['attachment', 'gif', 'media', 'picture', 'image', 'video'],
122
+ keywords: ['attachment', 'gif', 'media', 'picture', 'image', 'video', 'file'],
117
123
  icon: function icon() {
118
124
  return /*#__PURE__*/React.createElement(IconImages, null);
119
125
  },
@@ -122,7 +128,9 @@ export var mediaInsertPlugin = function mediaInsertPlugin(_ref) {
122
128
  // Insert empty string to remove the typeahead raw text
123
129
  // close the quick insert immediately
124
130
  var tr = insert('');
125
- showMediaInsertPopup(tr);
131
+ api === null || api === void 0 || api.mediaInsert.commands.showMediaInsertPopup({
132
+ tr: tr
133
+ });
126
134
  api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || (_api$analytics = _api$analytics.actions) === null || _api$analytics === void 0 || _api$analytics.attachAnalyticsEvent({
127
135
  action: ACTION.OPENED,
128
136
  actionSubject: ACTION_SUBJECT.PICKER,
@@ -35,6 +35,7 @@ export var MediaCard = function MediaCard(_ref) {
35
35
  dimensions: maxDimensions,
36
36
  originalDimensions: dimensions,
37
37
  identifier: identifier,
38
- alt: mediaAlt
38
+ alt: mediaAlt,
39
+ disableOverlay: true
39
40
  });
40
41
  };
@@ -1,3 +1,4 @@
1
+ import _extends from "@babel/runtime/helpers/extends";
1
2
  import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
2
3
  import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
3
4
  import _defineProperty from "@babel/runtime/helpers/defineProperty";
@@ -6,10 +7,12 @@ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbol
6
7
  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) { _defineProperty(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; }
7
8
  import React from 'react';
8
9
  import { useIntl } from 'react-intl-next';
10
+ import { isSafeUrl } from '@atlaskit/adf-schema';
9
11
  import ButtonGroup from '@atlaskit/button/button-group';
10
12
  import Button from '@atlaskit/button/new';
11
13
  import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
12
14
  import { mediaInsertMessages } from '@atlaskit/editor-common/messages';
15
+ import { ErrorMessage } from '@atlaskit/form';
13
16
  import EditorFilePreviewIcon from '@atlaskit/icon/glyph/editor/file-preview';
14
17
  import { getMediaClient } from '@atlaskit/media-client-react';
15
18
  import { Box, Flex, Inline, Stack, xcss } from '@atlaskit/primitives';
@@ -39,6 +42,19 @@ var INITIAL_PREVIEW_STATE = Object.freeze({
39
42
  warning: null,
40
43
  previewInfo: null
41
44
  });
45
+ var MAX_URL_LENGTH = 2048;
46
+ export var isValidUrl = function isValidUrl(value) {
47
+ try {
48
+ // Check for spaces and length first to avoid the expensive URL parsing
49
+ if (/\s/.test(value) || value.length > MAX_URL_LENGTH) {
50
+ return false;
51
+ }
52
+ new URL(value);
53
+ } catch (e) {
54
+ return false;
55
+ }
56
+ return isSafeUrl(value);
57
+ };
42
58
  var previewStateReducer = function previewStateReducer(state, action) {
43
59
  switch (action.type) {
44
60
  case 'loading':
@@ -76,16 +92,25 @@ export function MediaFromURL(_ref) {
76
92
  pasteLinkToUpload: intl.formatMessage(mediaInsertMessages.pasteLinkToUpload),
77
93
  cancel: intl.formatMessage(mediaInsertMessages.cancel),
78
94
  errorMessage: intl.formatMessage(mediaInsertMessages.fromUrlErrorMessage),
79
- warning: intl.formatMessage(mediaInsertMessages.fromUrlWarning)
95
+ warning: intl.formatMessage(mediaInsertMessages.fromUrlWarning),
96
+ invalidUrl: intl.formatMessage(mediaInsertMessages.invalidUrlErrorMessage)
80
97
  };
81
98
  var _React$useState = React.useState(''),
82
99
  _React$useState2 = _slicedToArray(_React$useState, 2),
83
100
  inputUrl = _React$useState2[0],
84
101
  setUrl = _React$useState2[1];
102
+ var _React$useState3 = React.useState(),
103
+ _React$useState4 = _slicedToArray(_React$useState3, 2),
104
+ hasUrlError = _React$useState4[0],
105
+ setHasUrlError = _React$useState4[1];
85
106
  var _React$useReducer = React.useReducer(previewStateReducer, INITIAL_PREVIEW_STATE),
86
107
  _React$useReducer2 = _slicedToArray(_React$useReducer, 2),
87
108
  previewState = _React$useReducer2[0],
88
109
  dispatch = _React$useReducer2[1];
110
+ var _React$useState5 = React.useState(false),
111
+ _React$useState6 = _slicedToArray(_React$useState5, 2),
112
+ isInputFocused = _React$useState6[0],
113
+ setInputFocused = _React$useState6[1];
89
114
  var pasteFlag = React.useRef(false);
90
115
  var _useAnalyticsEvents = useAnalyticsEvents(dispatchAnalyticsEvent),
91
116
  onUploadButtonClickedAnalytics = _useAnalyticsEvents.onUploadButtonClickedAnalytics,
@@ -171,12 +196,32 @@ export function MediaFromURL(_ref) {
171
196
  return _ref2.apply(this, arguments);
172
197
  };
173
198
  }(), [onUploadButtonClickedAnalytics, mediaProvider, onUploadCommencedAnalytics, onUploadSuccessAnalytics, onUploadFailureAnalytics, inputUrl]);
199
+ var errorAttributes = {};
200
+ if (!isInputFocused) {
201
+ errorAttributes['aria-relevant'] = 'all';
202
+ errorAttributes['aria-atomic'] = 'false';
203
+ }
204
+ var onBlur = React.useCallback(function () {
205
+ if (!isValidUrl(inputUrl)) {
206
+ setHasUrlError(true);
207
+ }
208
+ setInputFocused(false);
209
+ }, [inputUrl]);
210
+ var onFocus = React.useCallback(function () {
211
+ setInputFocused(true);
212
+ }, []);
174
213
  var onURLChange = React.useCallback(function (e) {
175
214
  var url = e.target.value;
176
215
  setUrl(url);
177
216
  dispatch({
178
217
  type: 'reset'
179
218
  });
219
+ if (!isValidUrl(url)) {
220
+ setHasUrlError(true);
221
+ return;
222
+ } else {
223
+ setHasUrlError(false);
224
+ }
180
225
  if (pasteFlag.current === true) {
181
226
  pasteFlag.current = false;
182
227
  uploadExternalMedia(url);
@@ -186,7 +231,7 @@ export function MediaFromURL(_ref) {
186
231
  // Note: this is a little weird, but the paste event will always be
187
232
  // fired before the change event when pasting. We don't really want to
188
233
  // duplicate logic by handling pastes separately to changes, so we're
189
- // just noting paste occured to then be handled in the onURLChange fn
234
+ // just noting paste occurred to then be handled in the onURLChange fn
190
235
  // above. The one exception to this is where paste inputs exactly what was
191
236
  // already in the input, in which case we want to ignore it.
192
237
  if (e.clipboardData.getData('text') !== inputUrl) {
@@ -256,7 +301,7 @@ export function MediaFromURL(_ref) {
256
301
  // This can be triggered from an enter key event on the input even when
257
302
  // the button is disabled, so we explicitly do nothing when in loading
258
303
  // state.
259
- if (previewState.isLoading) {
304
+ if (previewState.isLoading || hasUrlError) {
260
305
  return;
261
306
  }
262
307
  if (previewState.previewInfo) {
@@ -274,10 +319,16 @@ export function MediaFromURL(_ref) {
274
319
  }, /*#__PURE__*/React.createElement(TextField, {
275
320
  value: inputUrl,
276
321
  placeholder: strings.pasteLinkToUpload,
322
+ isInvalid: hasUrlError,
323
+ maxLength: MAX_URL_LENGTH,
277
324
  onChange: onURLChange,
278
325
  onKeyPress: onInputKeyPress,
279
- onPaste: onPaste
280
- }), previewState.previewInfo && /*#__PURE__*/React.createElement(Inline, {
326
+ onPaste: onPaste,
327
+ onBlur: onBlur,
328
+ onFocus: onFocus
329
+ }), hasUrlError && /*#__PURE__*/React.createElement(ErrorMessage, null, /*#__PURE__*/React.createElement(Box, _extends({
330
+ as: "span"
331
+ }, errorAttributes), strings.invalidUrl)), previewState.previewInfo && /*#__PURE__*/React.createElement(Inline, {
281
332
  alignInline: "center",
282
333
  alignBlock: "center",
283
334
  xcss: PreviewImageStyles,
@@ -296,7 +347,7 @@ export function MediaFromURL(_ref) {
296
347
  }, /*#__PURE__*/React.createElement(Button, {
297
348
  type: "submit",
298
349
  isLoading: previewState.isLoading,
299
- isDisabled: inputUrl.length === 0,
350
+ isDisabled: inputUrl.length === 0 || hasUrlError,
300
351
  iconBefore: EditorFilePreviewIcon
301
352
  }, strings.loadPreview)), /*#__PURE__*/React.createElement(Box, {
302
353
  xcss: ButtonGroupStyles
@@ -1,6 +1,6 @@
1
1
  import type { InputMethodInsertMedia } from '@atlaskit/editor-common/analytics';
2
2
  import type { Providers } from '@atlaskit/editor-common/provider-factory';
3
- import type { NextEditorPlugin, OptionalPlugin, UiComponentFactoryParams } from '@atlaskit/editor-common/types';
3
+ import type { EditorCommand, NextEditorPlugin, OptionalPlugin, UiComponentFactoryParams } from '@atlaskit/editor-common/types';
4
4
  import { type ExtractInjectionAPI } from '@atlaskit/editor-common/types';
5
5
  import type { AnalyticsPlugin } from '@atlaskit/editor-plugin-analytics';
6
6
  import type { FeatureFlagsPlugin } from '@atlaskit/editor-plugin-feature-flags';
@@ -20,6 +20,9 @@ export type MediaInsertPlugin = NextEditorPlugin<'mediaInsert', {
20
20
  OptionalPlugin<FeatureFlagsPlugin>
21
21
  ];
22
22
  sharedState: MediaInsertPluginState;
23
+ commands: {
24
+ showMediaInsertPopup: EditorCommand;
25
+ };
23
26
  }>;
24
27
  export type InsertExternalMediaSingle = (props: {
25
28
  url: string;
@@ -2,6 +2,7 @@
2
2
  import { type DispatchAnalyticsEvent } from '@atlaskit/editor-common/analytics';
3
3
  import type { MediaProvider } from '@atlaskit/editor-common/provider-factory';
4
4
  import { type InsertExternalMediaSingle, type InsertMediaSingle } from '../types';
5
+ export declare const isValidUrl: (value: string) => boolean;
5
6
  type Props = {
6
7
  mediaProvider: MediaProvider;
7
8
  dispatchAnalyticsEvent?: DispatchAnalyticsEvent;
@@ -1,6 +1,6 @@
1
1
  import type { InputMethodInsertMedia } from '@atlaskit/editor-common/analytics';
2
2
  import type { Providers } from '@atlaskit/editor-common/provider-factory';
3
- import type { NextEditorPlugin, OptionalPlugin, UiComponentFactoryParams } from '@atlaskit/editor-common/types';
3
+ import type { EditorCommand, NextEditorPlugin, OptionalPlugin, UiComponentFactoryParams } from '@atlaskit/editor-common/types';
4
4
  import { type ExtractInjectionAPI } from '@atlaskit/editor-common/types';
5
5
  import type { AnalyticsPlugin } from '@atlaskit/editor-plugin-analytics';
6
6
  import type { FeatureFlagsPlugin } from '@atlaskit/editor-plugin-feature-flags';
@@ -20,6 +20,9 @@ export type MediaInsertPlugin = NextEditorPlugin<'mediaInsert', {
20
20
  OptionalPlugin<FeatureFlagsPlugin>
21
21
  ];
22
22
  sharedState: MediaInsertPluginState;
23
+ commands: {
24
+ showMediaInsertPopup: EditorCommand;
25
+ };
23
26
  }>;
24
27
  export type InsertExternalMediaSingle = (props: {
25
28
  url: string;
@@ -2,6 +2,7 @@
2
2
  import { type DispatchAnalyticsEvent } from '@atlaskit/editor-common/analytics';
3
3
  import type { MediaProvider } from '@atlaskit/editor-common/provider-factory';
4
4
  import { type InsertExternalMediaSingle, type InsertMediaSingle } from '../types';
5
+ export declare const isValidUrl: (value: string) => boolean;
5
6
  type Props = {
6
7
  mediaProvider: MediaProvider;
7
8
  dispatchAnalyticsEvent?: DispatchAnalyticsEvent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-media-insert",
3
- "version": "2.7.3",
3
+ "version": "2.8.1",
4
4
  "description": "Media Insert plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -23,19 +23,21 @@
23
23
  ".": "./src/index.ts"
24
24
  },
25
25
  "dependencies": {
26
+ "@atlaskit/adf-schema": "^40.9.0",
26
27
  "@atlaskit/button": "^20.1.0",
27
- "@atlaskit/editor-common": "^90.0.0",
28
+ "@atlaskit/editor-common": "^90.2.0",
28
29
  "@atlaskit/editor-plugin-analytics": "^1.8.0",
29
- "@atlaskit/editor-plugin-media": "^1.31.0",
30
+ "@atlaskit/editor-plugin-media": "^1.32.0",
30
31
  "@atlaskit/editor-prosemirror": "6.0.0",
31
32
  "@atlaskit/editor-shared-styles": "^2.13.0",
33
+ "@atlaskit/form": "^10.5.3",
32
34
  "@atlaskit/icon": "^22.18.0",
33
35
  "@atlaskit/media-card": "^78.5.0",
34
36
  "@atlaskit/media-client": "^28.0.0",
35
37
  "@atlaskit/media-client-react": "^2.2.0",
36
38
  "@atlaskit/media-picker": "^66.7.0",
37
39
  "@atlaskit/platform-feature-flags": "^0.3.0",
38
- "@atlaskit/primitives": "^12.1.0",
40
+ "@atlaskit/primitives": "^12.2.0",
39
41
  "@atlaskit/section-message": "^6.6.0",
40
42
  "@atlaskit/tabs": "^16.4.0",
41
43
  "@atlaskit/textfield": "^6.5.0",
package/.eslintrc.js DELETED
@@ -1,14 +0,0 @@
1
- module.exports = {
2
- rules: {
3
- '@typescript-eslint/no-duplicate-imports': 'error',
4
- '@typescript-eslint/no-explicit-any': 'error',
5
- },
6
- overrides: [
7
- {
8
- files: ['**/__tests__/**/*.{js,ts,tsx}', '**/examples/**/*.{js,ts,tsx}'],
9
- rules: {
10
- '@typescript-eslint/no-explicit-any': 'off',
11
- },
12
- },
13
- ],
14
- };