diff --git a/app/assets/stylesheets/components/editor-toolbar.scss b/app/assets/stylesheets/components/editor-toolbar.scss index 480375d58..4c3a8f6ac 100644 --- a/app/assets/stylesheets/components/editor-toolbar.scss +++ b/app/assets/stylesheets/components/editor-toolbar.scss @@ -1,4 +1,29 @@ .editor-toolbar { display: flex; background: var(--base-0); + + &::-webkit-scrollbar { + background: transparent; + height: 0; + } + + .toolbar-btn .spinner-or-cancel { + .cancel { + display: none; + } + } + + .toolbar-btn:hover, + .toolbar-btn:focus { + .spinner-or-cancel { + .cancel { + display: block; + color: var(--accent-danger); + } + + svg:not(.cancel) { + display: none; + } + } + } } diff --git a/app/assets/stylesheets/views/article-form.scss b/app/assets/stylesheets/views/article-form.scss index 46bf98f0a..c6a2e04f0 100644 --- a/app/assets/stylesheets/views/article-form.scss +++ b/app/assets/stylesheets/views/article-form.scss @@ -168,6 +168,8 @@ top: 0; background: var(--base-0); padding: var(--su-2) var(--content-padding-x); + padding-right: var(--toolbar-padding-right, 0); + margin: calc(var(--content-padding-y) * -1) calc(var(--content-padding-x) * -1) var(--su-6) calc(var(--content-padding-x) * -1); @@ -175,6 +177,10 @@ > :first-child { margin-left: calc(var(--su-2) * -1); } + + @media (min-width: $breakpoint-m) { + --toolbar-padding-right: var(--content-padding-x); + } } &__cover { diff --git a/app/javascript/.storybook/preview-head.html b/app/javascript/.storybook/preview-head.html index 2a30f665c..b7cd07019 100644 --- a/app/javascript/.storybook/preview-head.html +++ b/app/javascript/.storybook/preview-head.html @@ -1,7 +1,11 @@ diff --git a/app/javascript/article-form/actions.js b/app/javascript/article-form/actions.js index 08ed5d3ce..287d7a6ea 100644 --- a/app/javascript/article-form/actions.js +++ b/app/javascript/article-form/actions.js @@ -83,7 +83,7 @@ function generateUploadFormdata(payload) { return formData; } -export function generateMainImage(payload, successCb, failureCb) { +export function generateMainImage({ payload, successCb, failureCb, signal }) { fetch('/image_uploads', { method: 'POST', headers: { @@ -91,6 +91,7 @@ export function generateMainImage(payload, successCb, failureCb) { }, body: generateUploadFormdata(payload), credentials: 'same-origin', + signal, }) .then((response) => response.json()) .then((json) => { @@ -120,6 +121,10 @@ export function processImageUpload( if (images.length > 0 && validateFileInputs()) { const payload = { image: images }; - generateMainImage(payload, handleImageSuccess, handleImageFailure); + generateMainImage({ + payload, + successCb: handleImageSuccess, + failureCb: handleImageFailure, + }); } } diff --git a/app/javascript/article-form/components/ArticleCoverImage.jsx b/app/javascript/article-form/components/ArticleCoverImage.jsx index 9fdc54dab..89b40eb56 100644 --- a/app/javascript/article-form/components/ArticleCoverImage.jsx +++ b/app/javascript/article-form/components/ArticleCoverImage.jsx @@ -71,7 +71,11 @@ export class ArticleCoverImage extends Component { const { files: image } = event.dataTransfer || event.target; const payload = { image }; - generateMainImage(payload, this.onImageUploadSuccess, this.onUploadError); + generateMainImage({ + payload, + successCb: this.onImageUploadSuccess, + failureCb: this.onUploadError, + }); } }; diff --git a/app/javascript/article-form/components/EditorBody.jsx b/app/javascript/article-form/components/EditorBody.jsx index fda776e10..84d8d19c1 100644 --- a/app/javascript/article-form/components/EditorBody.jsx +++ b/app/javascript/article-form/components/EditorBody.jsx @@ -73,7 +73,7 @@ export const EditorBody = ({ data-testid="article-form__body" className="crayons-article-form__body drop-area text-padding" > - + fetchSearch('usernames', { username })} diff --git a/app/javascript/article-form/components/ImageUploader.jsx b/app/javascript/article-form/components/ImageUploader.jsx index 996a1393f..b3e1741a7 100644 --- a/app/javascript/article-form/components/ImageUploader.jsx +++ b/app/javascript/article-form/components/ImageUploader.jsx @@ -1,7 +1,7 @@ /* global Runtime */ import { Fragment, h } from 'preact'; -import { useReducer } from 'preact/hooks'; +import { useReducer, useEffect, useState } from 'preact/hooks'; import { generateMainImage } from '../actions'; import { validateFileInputs } from '../../packs/validateFileInputs'; import { addSnackbarItem } from '../../Snackbar'; @@ -23,6 +23,28 @@ const ImageIcon = () => ( ); +const CancelIcon = () => ( + +); + +const SpinnerOrCancel = () => ( + + + + +); + ImageIcon.displayName = 'ImageIcon'; function imageUploaderReducer(state, action) { @@ -32,33 +54,25 @@ function imageUploaderReducer(state, action) { case 'uploading_image': return { ...state, - uploadError: false, - uploadingErrorMessage: null, + uploadErrorMessage: null, uploadingImage: true, insertionImageUrls: [], - showImageCopiedMessage: false, }; case 'upload_error': return { ...state, insertionImageUrls: [], - uploadError: true, uploadErrorMessage: payload.errorMessage, uploadingImage: false, }; - case 'show_copied_image_message': - return { - ...state, - showImageCopiedMessage: true, - }; - case 'upload_image_success': return { ...state, insertionImageUrls: payload.insertionImageUrls, uploadingImage: false, + uploadErrorMessage: null, }; default: @@ -66,15 +80,23 @@ function imageUploaderReducer(state, action) { } } -const NativeIosImageUpload = ({ uploadingImage, extraProps }) => ( +function initNativeImagePicker(e) { + e.preventDefault(); + window.ForemMobile?.injectNativeMessage('imageUpload', { + action: 'imageUpload', + }); +} + +const NativeIosV1ImageUpload = ({ uploadingImage }) => ( {!uploadingImage && ( @@ -82,60 +104,133 @@ const NativeIosImageUpload = ({ uploadingImage, extraProps }) => ( ); -const StandardImageUpload = ({ handleInsertionImageUpload, uploadingImage }) => - uploadingImage ? null : ( +/** + * The V2 editor uses a toolbar button press to trigger a visually hidden file input. + * + * @param {object} props + * @param {object} props.buttonProps Any props to be added to the trigger button + * @param {function} props.handleInsertionImageUpload Callback to handle image upload + * @param {boolean} props.uploadingImage Is an image currently being uploaded + * @param {boolean} props.useNativeUpload Should iOS native upload functionality be used + * @param {function} props.handleNativeMessage Callback for iOS native upload message handling + * @param {string} props.uploadErrorMessage Error message to be displayed + * + */ +const V2EditorImageUpload = ({ + buttonProps, + handleInsertionImageUpload, + uploadingImage, + useNativeUpload, + handleNativeMessage, + uploadErrorMessage, +}) => { + useEffect(() => { + if (uploadErrorMessage) { + addSnackbarItem({ + message: uploadErrorMessage, + addCloseButton: true, + }); + } + }, [uploadErrorMessage]); + + const [abortRequestController, setAbortRequestController] = useState(null); + + const startNewRequest = (e) => { + const controller = new AbortController(); + setAbortRequestController(controller); + handleInsertionImageUpload(e, controller.signal); + }; + + const cancelRequest = () => { + abortRequestController.abort(); + setAbortRequestController(null); + }; + + const { tooltip: actionTooltip } = buttonProps; + + return ( - + )} + {uploadingImage ? ( +