UNPKG

@instructure/canvas-rce

Version:

A component wrapping Canvas's usage of Tinymce

320 lines (317 loc) 11.6 kB
/* * Copyright (C) 2020 - 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, { Suspense, useState } from 'react'; import { arrayOf, bool, func, number, object, oneOf, oneOfType, string } from 'prop-types'; import { Modal } from '@instructure/ui-modal'; import { Button, CloseButton } from '@instructure/ui-buttons'; import { Heading } from '@instructure/ui-heading'; import { Spinner } from '@instructure/ui-spinner'; import { Tabs } from '@instructure/ui-tabs'; import { ToggleDetails } from '@instructure/ui-toggle-details'; import formatMessage from '../../../../format-message'; import { instuiPopupMountNodeFn } from '../../../../util/fullscreenHelpers'; import RceApiSource from '../../../../rcs/api'; import ImageOptionsForm from '../ImageOptionsForm'; import UsageRightsSelectBox from './UsageRightsSelectBox'; import { View } from '@instructure/ui-view'; import { UploadCanvasPanelIds, CanvasPanelTitles } from '../canvasContentUtils'; import { validateVideoUrl } from './videoValidationUtils'; const CanvasContentPanel = /*#__PURE__*/React.lazy(() => import('./CanvasContentPanel')); const ComputerPanel = /*#__PURE__*/React.lazy(() => import('./ComputerPanel')); const UrlPanel = /*#__PURE__*/React.lazy(() => import('./UrlPanel')); const VideoUrlPanel = /*#__PURE__*/React.lazy(() => import('./VideoUrlPanel')); function shouldBeDisabled({ fileUrl, theFile, error }, selectedPanel, usageRightNotSet) { if (error || usageRightNotSet && selectedPanel === 'COMPUTER') { return true; } switch (selectedPanel) { case 'COMPUTER': return !theFile || theFile.error; case 'URL': case 'VIDEO_URL': return !fileUrl; default: if (UploadCanvasPanelIds.includes(selectedPanel)) return !fileUrl; return false; // When in doubt, don't disable (but we shouldn't get here either) } } const UploadFileModal = /*#__PURE__*/React.forwardRef(({ preselectedFile, editor, contentProps, trayProps, canvasOrigin, onSubmit, onDismiss, panels, label, accept, modalBodyWidth, modalBodyHeight, requireA11yAttributes = true, forBlockEditorUse = false, uploading = false }, ref) => { const [theFile, setFile] = useState(preselectedFile); const [error, setError] = useState(null); const [fileUrl, setFileUrl] = useState(''); const [selectedPanel, setSelectedPanel] = useState(panels[0]); const [usageRightsState, setUsageRightsState] = React.useState({ usageRight: 'choose', ccLicense: '', copyrightHolder: '' }); // Image options props const [altText, setAltText] = useState(''); const [isDecorativeImage, setIsDecorativeImage] = useState(false); const [displayAs, setDisplayAs] = useState('embed'); // even though usage rights might be required by the course, canvas has no place // on the user to store it. Only Group and Course. const requiresUsageRights = contentProps?.session?.usageRightsRequired && /(?:course|group)/.test(trayProps.contextType); function handleAltTextChange(event) { setAltText(event.target.value); } function handleIsDecorativeChange(event) { setIsDecorativeImage(event.target.checked); } function handleDisplayAsChange(event) { setDisplayAs(event.target.value); } const handleRequestTabChange = index => { setSelectedPanel(panels[index]); setFileUrl(''); }; const submitDisabled = shouldBeDisabled({ fileUrl, theFile, error }, selectedPanel, requiresUsageRights && usageRightsState.usageRight === 'choose'); // Load the necessary session values, if not already loaded const loadSession = contentProps.loadSession; React.useEffect(() => { loadSession(); }, [loadSession]); const source = trayProps.source || new RceApiSource({ jwt: trayProps.jwt, refreshToken: trayProps.refreshToken, host: trayProps.host, canvasOrigin }); if (forBlockEditorUse && !['COMPUTER', 'URL'].includes(selectedPanel)) { requireA11yAttributes = false; } return /*#__PURE__*/React.createElement(Modal, { "data-mce-component": true, as: "form", label: label, mountNode: instuiPopupMountNodeFn, size: "large", overflow: "fit", onDismiss: onDismiss, onSubmit: e => { e.preventDefault(); if (submitDisabled || uploading) { return false; } let finalFileUrl = fileUrl; if (selectedPanel === 'VIDEO_URL' && finalFileUrl) { const validation = validateVideoUrl(finalFileUrl); if (!validation.isValid) { setError('Invalid video URL'); return false; } finalFileUrl = validation.embedUrl; } onSubmit(editor, accept, selectedPanel, { fileUrl: finalFileUrl, theFile, imageOptions: { altText, isDecorativeImage, displayAs }, usageRights: usageRightsState }, contentProps, source, onDismiss); }, open: true, shouldCloseOnDocumentClick: false, liveRegion: trayProps.liveRegion }, /*#__PURE__*/React.createElement(Modal.Header, null, /*#__PURE__*/React.createElement(CloseButton, { onClick: onDismiss, offset: "small", placement: "end", screenReaderLabel: formatMessage('Close') }), /*#__PURE__*/React.createElement(Heading, null, label)), /*#__PURE__*/React.createElement(Modal.Body, { ref: ref }, /*#__PURE__*/React.createElement(Tabs, { onRequestTabChange: (_event, { index }) => handleRequestTabChange(index) }, panels.map(panel => { switch (panel) { case 'COMPUTER': return /*#__PURE__*/React.createElement(Tabs.Panel, { key: panel, renderTitle: function () { return formatMessage('Computer'); }, isSelected: selectedPanel === 'COMPUTER' }, /*#__PURE__*/React.createElement(Suspense, { fallback: /*#__PURE__*/React.createElement(Spinner, { renderTitle: formatMessage('Loading'), size: "large" }) }, /*#__PURE__*/React.createElement(ComputerPanel, { theFile: theFile, setFile: setFile, setError: setError, label: label, accept: accept, bounds: { width: modalBodyWidth, height: modalBodyHeight } }))); case 'URL': return /*#__PURE__*/React.createElement(Tabs.Panel, { key: panel, renderTitle: function () { return formatMessage('URL'); }, isSelected: selectedPanel === 'URL' }, /*#__PURE__*/React.createElement(Suspense, { fallback: /*#__PURE__*/React.createElement(Spinner, { renderTitle: formatMessage('Loading'), size: "large" }) }, /*#__PURE__*/React.createElement(UrlPanel, { fileUrl: fileUrl, setFileUrl: setFileUrl, urlHasError: !!error }))); case 'VIDEO_URL': return /*#__PURE__*/React.createElement(Tabs.Panel, { key: panel, renderTitle: function () { return formatMessage('Video URL'); }, isSelected: selectedPanel === 'VIDEO_URL' }, /*#__PURE__*/React.createElement(Suspense, { fallback: /*#__PURE__*/React.createElement(Spinner, { renderTitle: formatMessage('Loading'), size: "large" }) }, /*#__PURE__*/React.createElement(VideoUrlPanel, { fileUrl: fileUrl, setFileUrl: url => { setError(null); setFileUrl(url); }, urlHasError: !!error }))); default: if (UploadCanvasPanelIds.includes(panel)) { return /*#__PURE__*/React.createElement(Tabs.Panel, { key: panel, renderTitle: () => CanvasPanelTitles[panel], isSelected: selectedPanel === panel }, /*#__PURE__*/React.createElement(Suspense, { fallback: /*#__PURE__*/React.createElement(Spinner, { renderTitle: formatMessage('Loading'), size: "large" }) }, /*#__PURE__*/React.createElement(CanvasContentPanel, { trayProps: trayProps, canvasOrigin: canvasOrigin, plugin: panel, setFileUrl: setFileUrl }))); } return null; } })), // We shouldn't show the accordions until the session data is loaded. Object.keys(contentProps.session || {}).length > 0 && /*#__PURE__*/React.createElement(React.Fragment, null, selectedPanel === 'COMPUTER' && requiresUsageRights && /*#__PURE__*/React.createElement(View, { as: "div", role: "group", borderColor: "primary", borderWidth: "0 0 small 0", padding: "medium" }, /*#__PURE__*/React.createElement(ToggleDetails, { defaultExpanded: true, summary: /*#__PURE__*/React.createElement(Heading, { level: "h3" }, formatMessage('Usage Rights (required)')) }, /*#__PURE__*/React.createElement(UsageRightsSelectBox, { usageRightsState: usageRightsState, setUsageRightsState: setUsageRightsState, contextType: trayProps.contextType, contextId: trayProps.contextId, showMessage: false }))), /image/.test(accept) && requireA11yAttributes && /*#__PURE__*/React.createElement(View, { as: "div", role: "group", borderColor: "primary", borderWidth: "0 0 small 0", padding: "medium" }, /*#__PURE__*/React.createElement(ToggleDetails, { defaultExpanded: !requiresUsageRights, summary: /*#__PURE__*/React.createElement(Heading, { level: "h3" }, formatMessage('Attributes')) }, /*#__PURE__*/React.createElement(ImageOptionsForm, { id: "upload-file-form", altText: altText, isDecorativeImage: isDecorativeImage, displayAs: displayAs, handleAltTextChange: handleAltTextChange, handleIsDecorativeChange: handleIsDecorativeChange, handleDisplayAsChange: handleDisplayAsChange, hideDimensions: true, forBlockEditorUse: forBlockEditorUse }))))), /*#__PURE__*/React.createElement(Modal.Footer, null, /*#__PURE__*/React.createElement(Button, { onClick: onDismiss }, formatMessage('Close')), "\xA0", /*#__PURE__*/React.createElement(Button, { color: "primary", type: "submit", disabled: submitDisabled || uploading }, uploading ? formatMessage('Submitting...') : formatMessage('Submit')))); }); UploadFileModal.propTypes = { editor: object, contentProps: object, trayProps: object, canvasOrigin: string, onSubmit: func, onDismiss: func.isRequired, panels: arrayOf(oneOf(['COMPUTER', 'URL', 'VIDEO_URL', ...UploadCanvasPanelIds])), label: string.isRequired, accept: oneOfType([arrayOf(string), string]), modalBodyWidth: number, modalBodyHeight: number, requireA11yAttributes: bool, forBlockEditorUse: bool, uploading: bool, preselectedFile: object // JS File }; export default UploadFileModal;