UNPKG

@instructure/canvas-rce

Version:

A component wrapping Canvas's usage of Tinymce

573 lines (569 loc) 17.3 kB
/* * Copyright (C) 2018 - 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 { saveMediaRecording } from '@instructure/canvas-media'; import { headerFor, originFromHost } from '../../rcs/api'; import * as files from './files'; import * as images from './images'; import bridge from '../../bridge'; import { fileEmbed } from '../../common/mimeClass'; import { isPreviewable } from '../../rce/plugins/shared/Previewable'; import { isAudioOrVideo, isImage } from '../../rce/plugins/shared/fileTypeUtils'; import { fixupFileUrl } from '../../common/fileUrl'; import { ICON_MAKER_ICONS } from '../../rce/plugins/instructure_icon_maker/svg/constants'; import * as CategoryProcessor from '../../rce/plugins/shared/Upload/CategoryProcessor'; export const COMPLETE_FILE_UPLOAD = 'COMPLETE_FILE_UPLOAD'; export const FAIL_FILE_UPLOAD = 'FAIL_FILE_UPLOAD'; export const FAIL_FOLDERS_LOAD = 'FAIL_FOLDERS_LOAD'; export const FAIL_MEDIA_UPLOAD = 'FAIL_MEDIA_UPLOAD'; export const MEDIA_UPLOAD_SUCCESS = 'MEDIA_UPLOAD_SUCCESS'; export const PROCESSED_FOLDER_BATCH = 'PROCESSED_FOLDER_BATCH'; export const QUOTA_EXCEEDED_UPLOAD = 'QUOTA_EXCEEDED_UPLOAD'; export const RECEIVE_FOLDER = 'RECEIVE_FOLDER'; export const START_FILE_UPLOAD = 'START_FILE_UPLOAD'; export const START_LOADING = 'START_LOADING'; export const START_MEDIA_UPLOADING = 'START_MEDIA_UPLOADING'; export const STOP_LOADING = 'STOP_LOADING'; export const STOP_MEDIA_UPLOADING = 'STOP_MEDIA_UPLOADING'; export const TOGGLE_UPLOAD_FORM = 'TOGGLE_UPLOAD_FORM'; export function startLoading() { return { type: START_LOADING }; } export function stopLoading() { return { type: STOP_LOADING }; } export function receiveFolder({ id, name, parentId }) { return { type: RECEIVE_FOLDER, id, name, parentId }; } export function failFoldersLoad(error) { return { type: FAIL_FOLDERS_LOAD, error }; } export function failMediaUpload(error) { bridge.showError(error); return { type: FAIL_MEDIA_UPLOAD, error }; } export function mediaUploadSuccess() { return { type: MEDIA_UPLOAD_SUCCESS }; } export function startUpload(fileMetaProps) { return { type: START_FILE_UPLOAD, file: fileMetaProps }; } export function failUpload(error) { return { type: FAIL_FILE_UPLOAD, error }; } export function quotaExceeded(error) { return { type: QUOTA_EXCEEDED_UPLOAD, error }; } export function completeUpload(results) { return { type: COMPLETE_FILE_UPLOAD, results }; } export function openOrCloseUploadForm() { return { type: TOGGLE_UPLOAD_FORM }; } export function processedFolderBatch({ folders }) { return { type: PROCESSED_FOLDER_BATCH, folders }; } export function startMediaUploading(fileMetaProps) { return { type: START_MEDIA_UPLOADING, payload: fileMetaProps }; } export function stopMediaUploading() { return { type: STOP_MEDIA_UPLOADING }; } export function activateMediaUpload(fileMetaProps) { return dispatch => { dispatch(startMediaUploading(fileMetaProps)); bridge.insertImagePlaceholder(fileMetaProps); }; } export function removePlaceholdersFor(name) { return dispatch => { dispatch(stopMediaUploading()); bridge.removePlaceholders(name); }; } export function allUploadCompleteActions(results, fileMetaProps, contextType) { const actions = []; actions.push(completeUpload(results)); const fileProps = { id: results.id, name: results.display_name, url: results.preview_url, type: fileMetaProps.contentType, embed: fileEmbed(results) }; actions.push(files.createAddFile(fileProps)); actions.push(files.createInsertFile(fileMetaProps.parentFolderId, results.id)); if (/^image\//.test(results['content-type'])) { actions.push(images.createAddImage(results, contextType)); } return actions; } export function embedUploadResult(results, selectedTabType) { const embedData = fileEmbed(results); if (selectedTabType === 'images' && isImage(embedData.type) && results.displayAs !== 'link') { // embed the image after any current selection rather than link to it or replace it bridge.activeEditor()?.mceInstance()?.selection.collapse(); const file_props = { href: results.href || results.url, title: results.title, display_name: results.display_name || results.name || results.title || results.filename, alt_text: results.alt_text, isDecorativeImage: results.isDecorativeImage, content_type: results['content-type'], contextType: results.contextType, contextId: results.contextId, uuid: results.uuid }; return bridge.insertImage(file_props); } else if (selectedTabType === 'media' && isAudioOrVideo(embedData.type)) { // embed media after any current selection rather than link to it or replace it bridge.activeEditor()?.mceInstance()?.selection.collapse(); // when we record audio, notorious thinks it's a video. use the content type we got // from the recorded file, not the returned media object. return bridge.embedMedia({ id: results.id, embedded_iframe_url: results.embedded_iframe_url, href: results.href || results.url, media_id: results.media_id, title: results.title, type: embedData.type, contextType: results.contextType, contextId: results.contextId, uuid: results.uuid }); } else { return bridge.insertLink({ 'data-canvas-previewable': isPreviewable(results['content-type']), href: results.href || results.url, title: results.alt_text || results.display_name || results.name || results.title || results.filename, content_type: results['content-type'], embed: { ...embedData, disableInlinePreview: true }, target: '_blank', contextType: results.contextType, contextId: results.contextId, uuid: results.uuid }, false); } } // fetches the list of folders to select from when uploading a file export function fetchFolders(bookmark) { return (dispatch, getState) => { dispatch(startLoading()); const { source, jwt, upload, host, contextId, contextType } = getState(); if (bookmark || upload.folders && Object.keys(upload.folders).length === 0) { return source.fetchFolders({ jwt, host, contextId, contextType }, bookmark).then(({ folders, bookmark }) => { dispatch(folders.map(receiveFolder)); const { upload } = getState(); dispatch(processedFolderBatch(upload)); if (bookmark) { dispatch(fetchFolders(bookmark)); } else { dispatch(stopLoading()); } }).catch(error => { dispatch(failFoldersLoad(error)); }); } }; } // uploads handled via canvas-media export function mediaUploadComplete(error, uploadData) { const { mediaObject, uploadedFile } = uploadData || {}; return (dispatch, _getState) => { if (error) { dispatch(failMediaUpload(error)); dispatch(removePlaceholdersFor(uploadedFile?.name)); } else { const embedData = { embedded_iframe_url: mediaObject.embedded_iframe_url, media_id: mediaObject.media_object.media_id, type: uploadedFile.type, title: uploadedFile.title || uploadedFile.name, id: mediaObject.media_object.attachment_id, uuid: mediaObject.media_object.uuid, contextType: mediaObject.media_object.context_type }; dispatch(removePlaceholdersFor(uploadedFile.name)); embedUploadResult(embedData, 'media'); dispatch(mediaUploadSuccess()); } }; } export function createMediaServerSession() { return (dispatch, getState) => { const { source } = getState(); if (!bridge.mediaServerSession) { return source.mediaServerSession().then(data => { bridge.setMediaServerSession(data); }); } }; } export function uploadToIconMakerFolder(svg, uploadSettings = {}) { return (_dispatch, getState) => { const { source, jwt, host, contextId, contextType } = getState(); const { onDuplicate } = uploadSettings; const svgAsFile = new File([svg.domElement.outerHTML], svg.name, { type: 'image/svg+xml' }); const fileMetaProps = { file: { name: svg.name, type: 'image/svg+xml' }, name: svg.name }; return source.fetchIconMakerFolder({ jwt, host, contextId, contextType }).then(({ folders }) => { fileMetaProps.parentFolderId = folders[0].id; return source.preflightUpload(fileMetaProps, { host, contextId, contextType, onDuplicate, category: ICON_MAKER_ICONS }).then(results => { return source.uploadFRD(svgAsFile, results); }); }); }; } export function uploadToMediaFolderWithoutEditor(fileMetaProps) { return (_, getState) => { const { source, jwt, host, contextId, contextType } = getState(); return source.fetchMediaFolder({ jwt, host, contextId, contextType }).then(async ({ folders }) => { fileMetaProps.parentFolderId = folders[0].id; if (fileMetaProps.domObject) { delete fileMetaProps.domObject.preview; // don't need this anymore } const getCategory = async fileProps => { const categoryObject = await CategoryProcessor.process(fileProps.domObject); return categoryObject?.category; }; const category = await getCategory(fileMetaProps); return source.preflightUpload(fileMetaProps, { jwt, host, contextId, contextType, category }).then(results => { return source.uploadFRD(fileMetaProps.domObject, results); }).then(results => { return setUsageRights(source, fileMetaProps, results); }).then(results => { return getFileUrlIfMissing(source, results); }).then(results => { return fixupFileUrl(contextType, contextId, results, source.canvasOrigin); }).then(results => { return setAltText(fileMetaProps.altText, results); }).then(results => { if (fileMetaProps.isDecorativeImage) { results.isDecorativeImage = fileMetaProps.isDecorativeImage; } if (fileMetaProps.displayAs) { results.displayAs = fileMetaProps.displayAs; } return results; }); }).catch(e => { console.error('Upload to the media folder failed.', e); }); }; } export function uploadToMediaFolder(tabContext, fileMetaProps) { return (dispatch, getState) => { const editorComponent = bridge.activeEditor(); const bookmark = editorComponent?.editor?.selection.getBookmark(undefined, true); dispatch(activateMediaUpload(fileMetaProps)); const { source, jwt, host, contextId, contextType } = getState(); if (tabContext === 'media' && fileMetaProps.domObject) { return saveMediaRecording(fileMetaProps.domObject, { contextId, contextType, origin: originFromHost(host), headers: headerFor(jwt) }, (err, uploadData) => { dispatch(mediaUploadComplete(err, uploadData)); }); } return source.fetchMediaFolder({ jwt, host, contextId, contextType }).then(({ folders }) => { fileMetaProps.parentFolderId = folders[0].id; if (fileMetaProps.domObject) { delete fileMetaProps.domObject.preview; // don't need this anymore } return dispatch(uploadPreflight(tabContext, { ...fileMetaProps, bookmark })).then(results => { return results; }); }).catch(e => { // Get rid of any placeholder that might be there. dispatch(removePlaceholdersFor(fileMetaProps.name)); console.error('Fetching the media folder failed.', e); }); }; } export function setUsageRights(source, fileMetaProps, results) { const { usageRights } = fileMetaProps; if (usageRights) { source.setUsageRights(results.id, usageRights); } return results; } export function getFileUrlIfMissing(source, results) { if (results.href || results.url) { return Promise.resolve(results); } return source.getFile(results.id).then(file => { results.url = file.url; return results; }); } function readUploadedFileAsDataURL(file, reader = new FileReader()) { return new Promise((resolve, reject) => { reader.onerror = () => { reader.abort(); reject(new DOMException('Unable to parse file')); }; reader.onload = () => { resolve(reader.result); }; reader.readAsDataURL(file); }); } export function generateThumbnailUrl(results, fileDOMObject, reader) { if (/^image\//.test(results['content-type'])) { return readUploadedFileAsDataURL(fileDOMObject, reader).then(result => { results.thumbnail_url = result; return results; }); } else { return Promise.resolve(results); } } export function setAltText(altText, results) { if (altText) { results.alt_text = altText; } return results; } export function handleFailures(error, dispatch) { if (error && error.response) { return error.response.json().then(resp => { if (resp.message === 'file size exceeds quota') { dispatch(quotaExceeded(error)); } else { dispatch(failUpload(error)); } }).catch(error => dispatch(failUpload(error))); } if (error) { return Promise.resolve().then(() => dispatch(failUpload(error))); } } export function uploadPreflight(tabContext, fileMetaProps) { return (dispatch, getState) => { const { source, jwt, host, contextId, contextType } = getState(); const { fileReader } = fileMetaProps; const getCategory = async fileProps => { const categoryObject = await CategoryProcessor.process(fileProps.domObject); return categoryObject?.category; }; dispatch(startUpload(fileMetaProps)); return getCategory(fileMetaProps).then(category => { return source.preflightUpload(fileMetaProps, { jwt, host, contextId, contextType, category }).then(results => { return source.uploadFRD(fileMetaProps.domObject, results); }).then(results => { return setUsageRights(source, fileMetaProps, results); }).then(results => { return getFileUrlIfMissing(source, results); }).then(results => { return fixupFileUrl(contextType, contextId, results, source.canvasOrigin); }).then(results => { return generateThumbnailUrl(results, fileMetaProps.domObject, fileReader); }).then(results => { return setAltText(fileMetaProps.altText, results); }).then(results => { if (fileMetaProps.isDecorativeImage) { results.isDecorativeImage = fileMetaProps.isDecorativeImage; } if (fileMetaProps.displayAs) { results.displayAs = fileMetaProps.displayAs; } return results; }).then(async results => { let newBookmark; const editorComponent = bridge.activeEditor(); if (fileMetaProps.bookmark) { newBookmark = editorComponent.editor.selection.getBookmark(undefined, true); editorComponent.editor.selection.moveToBookmark(fileMetaProps.bookmark); } const uploadResult = { contextType, contextId, ...results }; const embedResult = embedUploadResult(uploadResult, tabContext); if (fileMetaProps.bookmark) { editorComponent.editor.selection.moveToBookmark(newBookmark); } if (embedResult?.loadingPromise) { // Wait until the image loads to remove the placeholder await embedResult.loadingPromise.finally(() => dispatch(removePlaceholdersFor(fileMetaProps.name))); } else { dispatch(removePlaceholdersFor(fileMetaProps.name)); } return uploadResult; }).then(results => { dispatch(allUploadCompleteActions(results, fileMetaProps, contextType)); return results; }).catch(err => { // This may or may not be necessary depending on the upload dispatch(removePlaceholdersFor(fileMetaProps.name)); handleFailures(err, dispatch); }); }); }; }