UNPKG

@instructure/canvas-rce

Version:

A component wrapping Canvas's usage of Tinymce

336 lines 12.9 kB
/* * Copyright (C) 2019 - 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, useEffect } from 'react'; import { arrayOf, bool, func, number, shape, string } from 'prop-types'; import { Button, CloseButton, IconButton } from '@instructure/ui-buttons'; import { Heading } from '@instructure/ui-heading'; import { RadioInput, RadioInputGroup } from '@instructure/ui-radio-input'; import { SimpleSelect } from '@instructure/ui-simple-select'; import { TextArea } from '@instructure/ui-text-area'; import { Text } from '@instructure/ui-text'; import { IconQuestionLine } from '@instructure/ui-icons'; import { Flex } from '@instructure/ui-flex'; import { FormFieldGroup } from '@instructure/ui-form-field'; import { View } from '@instructure/ui-view'; import { Spinner } from '@instructure/ui-spinner'; import { Tooltip } from '@instructure/ui-tooltip'; import { Tray } from '@instructure/ui-tray'; import { StoreProvider } from '../../shared/StoreContext'; import { ClosedCaptionPanel } from '@instructure/canvas-media'; import { CUSTOM, MIN_WIDTH_VIDEO, MIN_PERCENTAGE, videoSizes, labelForImageSize, scaleToSize } from '../../instructure_image/ImageEmbedOptions'; import Bridge from '../../../../bridge'; import RceApiSource from '../../../../rcs/api'; import formatMessage from '../../../../format-message'; import DimensionsInput, { useDimensionsState } from '../../shared/DimensionsInput'; import { getTrayHeight } from '../../shared/trayUtils'; import { instuiPopupMountNodeFn } from '../../../../util/fullscreenHelpers'; import { parsedStudioOptionsPropType } from '../../shared/StudioLtiSupportUtils'; const getLiveRegion = () => document.getElementById('flash_screenreader_holder'); export default function VideoOptionsTray({ videoOptions, onRequestClose, onSave, open, trayProps, requestSubtitlesFromIframe = () => {}, onEntered = null, onExited = null, id = 'video-options-tray', studioOptions = null, forBlockEditorUse = false }) { const { naturalHeight, naturalWidth } = videoOptions; const currentHeight = videoOptions.appliedHeight || naturalHeight; const currentWidth = videoOptions.appliedWidth || naturalWidth; const [titleText, setTitleText] = useState(videoOptions.titleText); const [displayAs, setDisplayAs] = useState('embed'); const [videoSize, setVideoSize] = useState(videoOptions.videoSize); const [videoHeight, setVideoHeight] = useState(currentHeight); const [videoWidth, setVideoWidth] = useState(currentWidth); const [subtitles, setSubtitles] = useState(videoOptions.tracks || []); const [minWidth] = useState(MIN_WIDTH_VIDEO); const [minHeight] = useState(Math.round(videoHeight / videoWidth * MIN_WIDTH_VIDEO)); const [minPercentage] = useState(MIN_PERCENTAGE); const [editLocked, setEditLocked] = useState(null); const [loading, setLoading] = useState(true); const isStudio = !!studioOptions; const showDisplayOptions = (!isStudio || studioOptions.convertibleToLink) && !forBlockEditorUse; const showSizeControls = (!isStudio || studioOptions.resizable) && !forBlockEditorUse; const dimensionsState = useDimensionsState(videoOptions, { minHeight, minWidth, minPercentage }); const api = new RceApiSource(trayProps); useEffect(() => { if (videoOptions.attachmentId) { api.getFile(videoOptions.attachmentId, { include: ['blueprint_course_status'] }).then(response => { setEditLocked(response?.restricted_by_master_course && response?.is_master_course_child_content); setLoading(false); }).catch(error => { setLoading(false); }); } }, [videoOptions.attachmentId]); useEffect(() => { if (subtitles.length === 0) requestSubtitlesFromIframe(setSubtitles); }, []); function handleTitleTextChange(event) { setTitleText(event.target.value); } function handleDisplayAsChange(event) { event.target.focus(); setDisplayAs(event.target.value); } function handleVideoSizeChange(event, selectedOption) { setVideoSize(selectedOption.value); if (selectedOption.value === CUSTOM) { setVideoHeight(currentHeight); setVideoWidth(currentWidth); } else { const { height, width } = scaleToSize(selectedOption.value, naturalWidth, naturalHeight); setVideoHeight(height); setVideoWidth(width); } } function handleUpdateSubtitles(new_subtitles) { setSubtitles(new_subtitles); } function handleSave(event, updateMediaObject) { event.preventDefault(); let appliedHeight = videoHeight; let appliedWidth = videoWidth; if (videoSize === CUSTOM) { appliedHeight = dimensionsState.height; appliedWidth = dimensionsState.width; } onSave({ media_object_id: videoOptions.id, attachment_id: videoOptions.attachmentId, titleText, appliedHeight, appliedWidth, displayAs, subtitles, updateMediaObject, editLocked }); } const tooltipText = formatMessage('Used by screen readers to describe the video'); const textAreaLabel = /*#__PURE__*/React.createElement(Flex, { alignItems: "center" }, /*#__PURE__*/React.createElement(Flex.Item, null, formatMessage('Title')), /*#__PURE__*/React.createElement(Flex.Item, { margin: "0 0 0 xx-small" }, /*#__PURE__*/React.createElement(Tooltip, { on: ['hover', 'focus'], placement: "top", renderTip: /*#__PURE__*/React.createElement(View, { display: "block", id: "alt-text-label-tooltip", maxWidth: "14rem" }, tooltipText) }, /*#__PURE__*/React.createElement(IconButton, { renderIcon: IconQuestionLine, size: "small", screenReaderLabel: tooltipText, withBackground: false, withBorder: false })))); const messagesForSize = []; if (videoSize !== CUSTOM) { messagesForSize.push({ text: formatMessage('{width} x {height}px', { height: videoHeight, width: videoWidth }), type: 'hint' }); } const saveDisabled = displayAs === 'embed' && (titleText === '' || videoSize === CUSTOM && !dimensionsState.isValid); return /*#__PURE__*/React.createElement(StoreProvider, trayProps, contentProps => /*#__PURE__*/React.createElement(Tray, { key: "video-options-tray", "data-mce-component": true, label: isStudio ? formatMessage('Studio Media Options Tray') : formatMessage('Video Options Tray'), mountNode: instuiPopupMountNodeFn, onDismiss: onRequestClose, onEntered: onEntered, onExited: onExited, open: open, placement: "end", shouldCloseOnDocumentClick: true, shouldContainFocus: true, shouldReturnFocus: true, size: "regular" }, /*#__PURE__*/React.createElement(Flex, { direction: "column", height: getTrayHeight() }, /*#__PURE__*/React.createElement(Flex.Item, { as: "header", padding: "medium" }, /*#__PURE__*/React.createElement(Flex, { direction: "row" }, /*#__PURE__*/React.createElement(Flex.Item, { shouldGrow: true, shouldShrink: true }, /*#__PURE__*/React.createElement(Heading, { as: "h2" }, isStudio ? formatMessage('Studio Media Options') : formatMessage('Video Options'))), /*#__PURE__*/React.createElement(Flex.Item, null, /*#__PURE__*/React.createElement(CloseButton, { color: "primary", onClick: onRequestClose, screenReaderLabel: formatMessage('Close') })))), loading && videoOptions.attachmentId ? /*#__PURE__*/React.createElement(Flex.Item, { textAlign: "center", margin: "xx-large", padding: "xx-large" }, /*#__PURE__*/React.createElement(Spinner, { renderTitle: formatMessage('Loading') })) : /*#__PURE__*/React.createElement(Flex.Item, { as: "form", shouldGrow: true, margin: "none", shouldShrink: true }, /*#__PURE__*/React.createElement(Flex, { justifyItems: "space-between", direction: "column", height: "100%" }, /*#__PURE__*/React.createElement(Flex.Item, { shouldGrow: true, padding: "small", shouldShrink: true }, /*#__PURE__*/React.createElement(Flex, { direction: "column" }, !editLocked && /*#__PURE__*/React.createElement(Flex.Item, { padding: "small" }, isStudio ? /*#__PURE__*/React.createElement(Flex, { direction: "column" }, /*#__PURE__*/React.createElement(Flex.Item, null, /*#__PURE__*/React.createElement(Text, { weight: "bold" }, formatMessage('Media Title'))), /*#__PURE__*/React.createElement(Flex.Item, { padding: "small none none small" }, titleText)) : /*#__PURE__*/React.createElement(TextArea, { "aria-describedby": "alt-text-label-tooltip", disabled: displayAs === 'link', height: "4rem", label: textAreaLabel, onChange: handleTitleTextChange, placeholder: formatMessage('(Describe the video)'), resize: "vertical", value: titleText })), showDisplayOptions && /*#__PURE__*/React.createElement(Flex.Item, { margin: "small none none none", padding: "small" }, /*#__PURE__*/React.createElement(RadioInputGroup, { description: formatMessage('Display Options'), name: "display-video-as", onChange: handleDisplayAsChange, value: displayAs }, /*#__PURE__*/React.createElement(RadioInput, { label: formatMessage('Embed Video'), value: "embed" }), /*#__PURE__*/React.createElement(RadioInput, { label: formatMessage('Display Text Link (Opens in a new tab)'), value: "link" }))), showSizeControls && /*#__PURE__*/React.createElement(Flex.Item, { margin: "small none xx-small none" }, /*#__PURE__*/React.createElement(View, { as: "div", padding: "small small xx-small small" }, /*#__PURE__*/React.createElement(SimpleSelect, { id: `${id}-size`, mountNode: instuiPopupMountNodeFn, disabled: displayAs !== 'embed', renderLabel: formatMessage('Size'), messages: messagesForSize, assistiveText: formatMessage('Use arrow keys to navigate options.'), onChange: handleVideoSizeChange, value: videoSize }, videoSizes.map(size => /*#__PURE__*/React.createElement(SimpleSelect.Option, { id: `${id}-size-${size}`, key: size, value: size }, labelForImageSize(size))))), videoSize === CUSTOM && /*#__PURE__*/React.createElement(View, { as: "div", padding: "xx-small small" }, /*#__PURE__*/React.createElement(DimensionsInput, { dimensionsState: dimensionsState, disabled: displayAs !== 'embed', minHeight: minHeight, minWidth: minWidth, minPercentage: minPercentage, hidePercentage: true }))), !isStudio && !editLocked && /*#__PURE__*/React.createElement(Flex.Item, { padding: "small" }, /*#__PURE__*/React.createElement(FormFieldGroup, { description: formatMessage('Closed Captions/Subtitles') }, /*#__PURE__*/React.createElement(ClosedCaptionPanel, { subtitles: subtitles.map(st => ({ locale: st.locale, inherited: st.inherited, file: { name: st.language || st.locale } // this is an artifact of ClosedCaptionCreatorRow's inards })), uploadMediaTranslations: Bridge.uploadMediaTranslations, userLocale: Bridge.userLocale, updateSubtitles: handleUpdateSubtitles, liveRegion: getLiveRegion, mountNode: instuiPopupMountNodeFn }))))), /*#__PURE__*/React.createElement(Flex.Item, { background: "secondary", borderWidth: "small none none none", padding: "small medium", textAlign: "end" }, /*#__PURE__*/React.createElement(Button, { disabled: saveDisabled, onClick: event => handleSave(event, contentProps.updateMediaObject), color: "primary" }, formatMessage('Done')))))))); } VideoOptionsTray.propTypes = { videoOptions: shape({ titleText: string, appliedHeight: number, appliedWidth: number, naturalHeight: number.isRequired, naturalWidth: number.isRequired, tracks: arrayOf(shape({ locale: string.isRequired, inherited: bool })) }).isRequired, onEntered: func, onExited: func, onRequestClose: func.isRequired, onSave: func.isRequired, open: bool.isRequired, trayProps: shape({ host: string.isRequired, jwt: string.isRequired }), id: string, studioOptions: parsedStudioOptionsPropType, requestSubtitlesFromIframe: func };