UNPKG

@instructure/canvas-rce

Version:

A component wrapping Canvas's usage of Tinymce

175 lines (173 loc) 6.73 kB
/* * Copyright (C) 2022 - present Instructure, Inc. * * This file is part of Canvas. * * Canvas is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the Free * Software Foundation, version 3 of the License. * * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along * with this program. If not, see <http://www.gnu.org/licenses/>. */ import React, { useState, useCallback, useEffect } from 'react'; import formatMessage from '../../../../../../format-message'; import { actions, modes } from '../../../reducers/imageSection'; import { actions as trayActions } from '../../../reducers/svgSettings'; import { IconButton } from '@instructure/ui-buttons'; import { IconCropLine, IconTrashLine } from '@instructure/ui-icons'; import { View } from '@instructure/ui-view'; import { Flex } from '@instructure/ui-flex'; import PreviewIcon from '../../../../shared/PreviewIcon'; import { TruncateText } from '@instructure/ui-truncate-text'; import { Text } from '@instructure/ui-text'; import { ImageCropperModal } from '../../../../shared/ImageCropper'; import ModeSelect from './ModeSelect'; import PropTypes from 'prop-types'; import { ImageCropperSettingsPropTypes } from '../../../../shared/ImageCropper/propTypes'; import { MAX_IMAGE_SIZE_BYTES } from '../../../../shared/compressionUtils'; import { createCroppedImageSvg } from '../../../../shared/ImageCropper/imageCropUtils'; import { convertFileToBase64 } from '../../../../shared/fileUtils'; import { ImageSettingsPropTypes } from './propTypes'; import _ from 'lodash'; const getCompressionMessage = () => formatMessage('Your image has been compressed for Icon Maker. Images less than {size} KB will not be compressed.', { size: MAX_IMAGE_SIZE_BYTES / 1024 }); function renderImagePreview({ loading }, embedImage) { return /*#__PURE__*/React.createElement(PreviewIcon, { variant: "large", testId: "selected-image-preview", image: embedImage, loading: loading, checkered: true }); } function renderImageName({ imageName }) { return /*#__PURE__*/React.createElement(View, { maxWidth: "200px", as: "div" }, /*#__PURE__*/React.createElement(TruncateText, null, /*#__PURE__*/React.createElement(Text, null, imageName || formatMessage('None Selected')))); } function renderImageActionButtons({ mode, collectionOpen }, dispatch, trayDispatch, setFocus, ref) { const showCropButton = [modes.uploadImages.type, modes.courseImages.type].includes(mode) && !collectionOpen; return /*#__PURE__*/React.createElement(React.Fragment, null, showCropButton && /*#__PURE__*/React.createElement(IconButton, { margin: "0 small 0 0", screenReaderLabel: formatMessage('Crop image'), onClick: () => dispatch({ type: actions.SET_CROPPER_OPEN.type, payload: true }) }, /*#__PURE__*/React.createElement(IconCropLine, null)), /*#__PURE__*/React.createElement(IconButton, { ref: ref, screenReaderLabel: formatMessage('Clear image'), onClick: () => { dispatch(actions.RESET_ALL); trayDispatch({ type: trayActions.SET_EMBED_IMAGE, payload: null }); }, onFocus: () => setFocus(true), onBlur: () => setFocus(false), "data-testid": "clear-image" }, /*#__PURE__*/React.createElement(IconTrashLine, null))); } export const ImageOptions = ({ state, settings, dispatch, mountNode, trayDispatch }) => { const [isImageActionFocused, setIsImageActionFocused] = useState(false); const imageActionRef = useCallback(el => { if (el && isImageActionFocused) el.focus(); }, [isImageActionFocused]); // After submitting cropper modal a new embedded image should be generated useEffect(() => { if (state.cropperSettings && settings.imageSettings && !_.isEqual(state.cropperSettings, settings.imageSettings?.cropperSettings)) { if (state.cropperSettings.shape !== settings.shape) { trayDispatch({ shape: state.cropperSettings.shape }); } createCroppedImageSvg(state.cropperSettings, settings.imageSettings.image).then(generatedSvg => convertFileToBase64(new Blob([generatedSvg.outerHTML], { type: 'image/svg+xml' }))).then(base64Image => { trayDispatch({ type: trayActions.SET_EMBED_IMAGE, payload: base64Image }); }).catch(error => console.error(error)); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.cropperSettings]); const { image } = state; const { shape, embedImage } = settings; return /*#__PURE__*/React.createElement(Flex, { padding: "small" }, /*#__PURE__*/React.createElement(Flex.Item, { margin: "0 small 0 0" }, renderImagePreview(state, embedImage)), /*#__PURE__*/React.createElement(Flex.Item, null, renderImageName(state)), /*#__PURE__*/React.createElement(Flex.Item, { margin: "0 0 0 auto" }, image ? renderImageActionButtons(state, dispatch, trayDispatch, setIsImageActionFocused, imageActionRef) : /*#__PURE__*/React.createElement(ModeSelect, { dispatch: dispatch, mountNode: mountNode, ref: imageActionRef, onFocus: () => setIsImageActionFocused(true), onBlur: () => setIsImageActionFocused(false) }), state.cropperOpen && /*#__PURE__*/React.createElement(ImageCropperModal, { shape: shape, open: state.cropperOpen, onClose: () => dispatch({ type: actions.SET_CROPPER_OPEN.type, payload: false }), onSubmit: cropperSettings => { dispatch({ type: actions.SET_CROPPER_SETTINGS.type, payload: cropperSettings }); }, image: image, cropSettings: state.cropperSettings, message: state.compressed ? getCompressionMessage() : null, loading: !image }))); }; ImageOptions.propTypes = { state: PropTypes.shape({ image: PropTypes.string, imageName: PropTypes.string, mode: PropTypes.string, loading: PropTypes.bool.isRequired, cropperOpen: PropTypes.bool.isRequired, cropperSettings: ImageCropperSettingsPropTypes, compressed: PropTypes.bool.isRequired }).isRequired, settings: PropTypes.shape({ shape: PropTypes.string, embedImage: PropTypes.string, imageSettings: ImageSettingsPropTypes }).isRequired, dispatch: PropTypes.func.isRequired, trayDispatch: PropTypes.func.isRequired, mountNode: PropTypes.oneOfType([PropTypes.element, PropTypes.func]) };