UNPKG

@atlaskit/editor-plugin-media-insert

Version:

Media Insert plugin for @atlaskit/editor-core

404 lines 18.1 kB
import _extends from "@babel/runtime/helpers/extends"; import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties"; import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator"; import _slicedToArray from "@babel/runtime/helpers/slicedToArray"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; var _excluded = ["value", "onChange"]; import _regeneratorRuntime from "@babel/runtime/regenerator"; 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; } 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; } import React, { Fragment } from 'react'; import { useIntl } from 'react-intl'; import { isSafeUrl } from '@atlaskit/adf-schema'; import ButtonGroup from '@atlaskit/button/button-group'; import Button from '@atlaskit/button/new'; import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics'; import { mediaInsertMessages } from '@atlaskit/editor-common/messages'; import Form, { ErrorMessage, Field, FormFooter, HelperMessage, MessageWrapper } from '@atlaskit/form'; import ExpandIcon from '@atlaskit/icon/core/grow-diagonal'; import { getMediaClient } from '@atlaskit/media-client-react'; import { fg } from '@atlaskit/platform-feature-flags'; // eslint-disable-next-line @atlaskit/design-system/no-emotion-primitives -- to be migrated to @atlaskit/primitives/compiled – go/akcss import { Box, Flex, Inline, Stack, xcss } from '@atlaskit/primitives'; import SectionMessage from '@atlaskit/section-message'; import TextField from '@atlaskit/textfield'; import { MediaCard } from './MediaCard'; import { useAnalyticsEvents } from './useAnalyticsEvents'; var PreviewBoxStyles = xcss({ borderWidth: 'border.width', borderStyle: 'dashed', borderColor: 'color.border', borderRadius: 'radius.small', height: '200px' }); var PreviewImageStyles = xcss({ height: '200px' }); var FormStyles = xcss({ flexGrow: 1 }); var INITIAL_PREVIEW_STATE = Object.freeze({ isLoading: false, error: null, warning: null, previewInfo: null }); var isValidInput = function isValidInput(value, customizedUrlValidation) { if (customizedUrlValidation) { return customizedUrlValidation(value); } return isValidUrl(value); }; var MAX_URL_LENGTH = 2048; export var isValidUrl = function isValidUrl(value) { try { // Check for spaces and length first to avoid the expensive URL parsing // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp if (/\s/.test(value) || value.length > MAX_URL_LENGTH) { return false; } new URL(value); } catch (_unused) { return false; } return isSafeUrl(value); }; var previewStateReducer = function previewStateReducer(state, action) { switch (action.type) { case 'loading': return _objectSpread(_objectSpread({}, INITIAL_PREVIEW_STATE), {}, { isLoading: true }); case 'error': return _objectSpread(_objectSpread({}, INITIAL_PREVIEW_STATE), {}, { error: action.error }); case 'warning': return _objectSpread(_objectSpread({}, INITIAL_PREVIEW_STATE), {}, { warning: action.warning }); case 'success': return _objectSpread(_objectSpread({}, INITIAL_PREVIEW_STATE), {}, { previewInfo: action.payload }); case 'reset': return INITIAL_PREVIEW_STATE; default: return state; } }; export function MediaFromURL(_ref) { var mediaProvider = _ref.mediaProvider, dispatchAnalyticsEvent = _ref.dispatchAnalyticsEvent, closeMediaInsertPicker = _ref.closeMediaInsertPicker, insertMediaSingle = _ref.insertMediaSingle, insertExternalMediaSingle = _ref.insertExternalMediaSingle, isOnlyExternalLinks = _ref.isOnlyExternalLinks, customizedUrlValidation = _ref.customizedUrlValidation, customizedHelperMessage = _ref.customizedHelperMessage; var intl = useIntl(); var strings = { loadPreview: intl.formatMessage(mediaInsertMessages.loadPreview), insert: intl.formatMessage(mediaInsertMessages.insert), pasteLinkToUpload: intl.formatMessage(mediaInsertMessages.pasteLinkToUpload), cancel: intl.formatMessage(mediaInsertMessages.cancel), errorMessage: intl.formatMessage(mediaInsertMessages.fromUrlErrorMessage), warning: intl.formatMessage(mediaInsertMessages.fromUrlWarning), invalidUrl: intl.formatMessage(mediaInsertMessages.invalidUrlErrorMessage) }; var _React$useReducer = React.useReducer(previewStateReducer, INITIAL_PREVIEW_STATE), _React$useReducer2 = _slicedToArray(_React$useReducer, 2), previewState = _React$useReducer2[0], dispatch = _React$useReducer2[1]; var _React$useState = React.useState(''), _React$useState2 = _slicedToArray(_React$useState, 2), input = _React$useState2[0], setInput = _React$useState2[1]; var pasteFlag = React.useRef(false); var _useAnalyticsEvents = useAnalyticsEvents(dispatchAnalyticsEvent), onUploadButtonClickedAnalytics = _useAnalyticsEvents.onUploadButtonClickedAnalytics, onUploadCommencedAnalytics = _useAnalyticsEvents.onUploadCommencedAnalytics, onUploadSuccessAnalytics = _useAnalyticsEvents.onUploadSuccessAnalytics, onUploadFailureAnalytics = _useAnalyticsEvents.onUploadFailureAnalytics; var uploadExternalMedia = React.useCallback( /*#__PURE__*/function () { var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(url) { var uploadMediaClientConfig, uploadParams, mediaClient, collection, _yield$mediaClient$fi, uploadableFileUpfrontIds, dimensions, mimeType, message; return _regeneratorRuntime.wrap(function _callee$(_context) { while (1) switch (_context.prev = _context.next) { case 0: onUploadButtonClickedAnalytics(); dispatch({ type: 'loading' }); uploadMediaClientConfig = mediaProvider.uploadMediaClientConfig, uploadParams = mediaProvider.uploadParams; if (uploadMediaClientConfig) { _context.next = 5; break; } return _context.abrupt("return"); case 5: mediaClient = getMediaClient(uploadMediaClientConfig); collection = uploadParams === null || uploadParams === void 0 ? void 0 : uploadParams.collection; onUploadCommencedAnalytics('url'); _context.prev = 8; _context.next = 11; return mediaClient.file.uploadExternal(url, collection); case 11: _yield$mediaClient$fi = _context.sent; uploadableFileUpfrontIds = _yield$mediaClient$fi.uploadableFileUpfrontIds; dimensions = _yield$mediaClient$fi.dimensions; mimeType = _yield$mediaClient$fi.mimeType; onUploadSuccessAnalytics('url'); dispatch({ type: 'success', payload: { id: uploadableFileUpfrontIds.id, collection: collection, dimensions: dimensions, occurrenceKey: uploadableFileUpfrontIds.occurrenceKey, fileMimeType: mimeType } // eslint-disable-next-line no-unused-vars }); _context.next = 22; break; case 19: _context.prev = 19; _context.t0 = _context["catch"](8); // eslint-disable-line no-unused-vars if (typeof _context.t0 === 'string' && _context.t0 === 'Could not download remote file') { // TODO: ED-26962 - Make sure this gets good unit test coverage with the actual media plugin. // This hard coded error message could be changed at any // point and we need a unit test to break to stop people changing it. onUploadFailureAnalytics(_context.t0, 'url'); dispatch({ type: 'warning', warning: _context.t0, url: url }); } else if (_context.t0 instanceof Error) { message = 'Image preview fetch failed'; onUploadFailureAnalytics(message, 'url'); dispatch({ type: 'error', error: message }); } else { onUploadFailureAnalytics('Unknown error', 'url'); dispatch({ type: 'error', error: 'Unknown error' }); } case 22: case "end": return _context.stop(); } }, _callee, null, [[8, 19]]); })); return function (_x) { return _ref2.apply(this, arguments); }; }(), [onUploadButtonClickedAnalytics, mediaProvider, onUploadCommencedAnalytics, onUploadSuccessAnalytics, onUploadFailureAnalytics]); var onURLChange = React.useCallback(function (e) { var url = e.currentTarget.value; dispatch({ type: 'reset' }); setInput(url); if (!isValidInput(url, customizedUrlValidation)) { return; } if (pasteFlag.current) { pasteFlag.current = false; if (!isOnlyExternalLinks) { uploadExternalMedia(url); } } }, [uploadExternalMedia, isOnlyExternalLinks, customizedUrlValidation]); var _onPaste = React.useCallback(function (e, inputUrl) { // Note: this is a little weird, but the paste event will always be // fired before the change event when pasting. We don't really want to // duplicate logic by handling pastes separately to changes, so we're // just noting paste occurred to then be handled in the onURLChange fn // above. The one exception to this is where paste inputs exactly what was // already in the input, in which case we want to ignore it. if (e.clipboardData.getData('text') !== inputUrl) { pasteFlag.current = true; } }, []); var onInsert = React.useCallback(function () { if (previewState.previewInfo) { insertMediaSingle({ mediaState: previewState.previewInfo, inputMethod: INPUT_METHOD.MEDIA_PICKER }); } closeMediaInsertPicker(); }, [closeMediaInsertPicker, insertMediaSingle, previewState.previewInfo]); var onExternalInsert = React.useCallback(function (url) { if (previewState.warning || isOnlyExternalLinks) { insertExternalMediaSingle({ url: url, alt: '', inputMethod: INPUT_METHOD.MEDIA_PICKER }); } closeMediaInsertPicker(); }, [closeMediaInsertPicker, insertExternalMediaSingle, previewState.warning, isOnlyExternalLinks]); var onInputKeyPress = React.useCallback(function (event) { if (event && event.key === 'Esc') { if (dispatchAnalyticsEvent) { var payload = { action: ACTION.CLOSED, actionSubject: ACTION_SUBJECT.PICKER, actionSubjectId: ACTION_SUBJECT_ID.PICKER_MEDIA, eventType: EVENT_TYPE.UI, attributes: { exitMethod: INPUT_METHOD.KEYBOARD } }; dispatchAnalyticsEvent(payload); } closeMediaInsertPicker(); } }, [dispatchAnalyticsEvent, closeMediaInsertPicker]); var onCancel = React.useCallback(function () { if (dispatchAnalyticsEvent) { var payload = { action: ACTION.CANCELLED, actionSubject: ACTION_SUBJECT.PICKER, actionSubjectId: ACTION_SUBJECT_ID.PICKER_MEDIA, eventType: EVENT_TYPE.UI }; dispatchAnalyticsEvent(payload); } closeMediaInsertPicker(); }, [closeMediaInsertPicker, dispatchAnalyticsEvent]); return /*#__PURE__*/React.createElement(Form, { // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) onSubmit: function onSubmit(_ref3, form) { var inputUrl = _ref3.inputUrl; // This can be triggered from an enter key event on the input even when // the button is disabled, so we explicitly do nothing when in loading // state. if (previewState.isLoading || form.getState().invalid) { return; } if (previewState.previewInfo) { return onInsert(); } if (previewState.warning || isOnlyExternalLinks) { return onExternalInsert(inputUrl); } return uploadExternalMedia(inputUrl); } }, function (_ref4) { var formProps = _ref4.formProps; return ( /*#__PURE__*/ // Ignored via go/ees005 // eslint-disable-next-line react/jsx-props-no-spreading React.createElement(Box, { xcss: FormStyles }, /*#__PURE__*/React.createElement(Stack, { space: "space.150", grow: "fill" }, /*#__PURE__*/React.createElement(Field, { isRequired: true, name: "inputUrl" // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , validate: function validate(value) { return value && isValidInput(value, customizedUrlValidation) ? undefined : strings.invalidUrl; } }, function (_ref5) { var _ref5$fieldProps = _ref5.fieldProps, value = _ref5$fieldProps.value, _onChange = _ref5$fieldProps.onChange, rest = _objectWithoutProperties(_ref5$fieldProps, _excluded), error = _ref5.error, meta = _ref5.meta; return /*#__PURE__*/React.createElement(Stack, { space: "space.150", grow: "fill" }, /*#__PURE__*/React.createElement(Box, null, /*#__PURE__*/React.createElement(TextField // Ignored via go/ees005 // eslint-disable-next-line react/jsx-props-no-spreading , _extends({}, rest, { value: value, "aria-label": fg('platform_editor_nov_a11y_fixes') ? strings.pasteLinkToUpload : undefined, placeholder: strings.pasteLinkToUpload, maxLength: MAX_URL_LENGTH, onKeyPress: onInputKeyPress // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , onPaste: function onPaste(event) { return _onPaste(event, value); } // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , onChange: function onChange(value) { onURLChange(value); _onChange(value); } // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , onKeyDown: function onKeyDown(e) { if (e.key === 'Enter') { e.preventDefault(); formProps.onSubmit(); } } })), customizedHelperMessage && /*#__PURE__*/React.createElement(Fragment, null, /*#__PURE__*/React.createElement(HelperMessage, null, customizedHelperMessage)), /*#__PURE__*/React.createElement(MessageWrapper, null, error && /*#__PURE__*/React.createElement(ErrorMessage, null, /*#__PURE__*/React.createElement(Box, { as: "span" }, error)))), !previewState.previewInfo && !previewState.error && !previewState.warning && !isOnlyExternalLinks && /*#__PURE__*/React.createElement(Flex, { xcss: PreviewBoxStyles, alignItems: "center", justifyContent: "center" }, /*#__PURE__*/React.createElement(Button, { type: "button" // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , onClick: function onClick() { return formProps.onSubmit(); }, isLoading: previewState.isLoading, isDisabled: !!error || !meta.dirty // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , iconBefore: function iconBefore() { return /*#__PURE__*/React.createElement(ExpandIcon, { label: "" }); } }, strings.loadPreview))); }), previewState.previewInfo && /*#__PURE__*/React.createElement(Inline, { alignInline: "center", alignBlock: "center", xcss: PreviewImageStyles, space: "space.200" }, /*#__PURE__*/React.createElement(MediaCard, { attrs: previewState.previewInfo, mediaProvider: mediaProvider })), /*#__PURE__*/React.createElement(MessageWrapper, null, previewState.error && /*#__PURE__*/React.createElement(SectionMessage, { appearance: "error" }, strings.errorMessage), previewState.warning && /*#__PURE__*/React.createElement(SectionMessage, { appearance: "warning" }, strings.warning)), /*#__PURE__*/React.createElement(FormFooter, null, /*#__PURE__*/React.createElement(ButtonGroup, null, /*#__PURE__*/React.createElement(Button, { appearance: "subtle", onClick: onCancel }, strings.cancel), /*#__PURE__*/React.createElement(Button, { type: "button", appearance: "primary", isDisabled: isOnlyExternalLinks ? !input || !isValidInput(input, customizedUrlValidation) : !previewState.previewInfo && !previewState.warning // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , onClick: function onClick() { return formProps.onSubmit(); } }, strings.insert))))) ); }); }