@instructure/canvas-rce
Version:
A component wrapping Canvas's usage of Tinymce
212 lines (208 loc) • 7.88 kB
JavaScript
/*
* 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 { fromImageEmbed, fromVideoEmbed } from '../instructure_image/ImageEmbedOptions';
import { isOnlyTextSelected } from '../../contentInsertionUtils';
import formatMessage from '../../../format-message';
import { isStudioEmbeddedMedia } from './StudioLtiSupportUtils';
import { parseUrlPath } from '../../../util/url-util';
import { findMediaPlayerIframe } from './iframeUtils';
const FILE_DOWNLOAD_PATH_REGEX = /^\/(courses\/\d+\/)?files\/\d+\/download$/;
export const LINK_TYPE = 'link';
export const FILE_LINK_TYPE = 'file-link';
export const IMAGE_EMBED_TYPE = 'image-embed';
export const VIDEO_EMBED_TYPE = 'video-embed';
export const TEXT_TYPE = 'text';
export const NONE_TYPE = 'none';
export const DISPLAY_AS_LINK = 'link';
export const DISPLAY_AS_EMBED = 'embed';
export const DISPLAY_AS_EMBED_DISABLED = 'embed-disabled';
export const DISPLAY_AS_DOWNLOAD_LINK = 'download-link';
export function asImageEmbed($element) {
const nodeName = $element?.nodeName.toLowerCase();
if (nodeName !== 'img') {
return null;
}
return {
...fromImageEmbed($element),
$element,
type: IMAGE_EMBED_TYPE
};
}
export function asLink($element, editor) {
let $link = $element;
if ($link?.tagName !== 'A') {
// the user may have selected some text that is w/in a link
// but didn't include the <a>. Let's see if that's true
$link = editor && editor.dom.getParent($link, 'a[href]');
}
if (!$link || $link.tagName !== 'A' || !$link.href) {
return null;
}
const pathname = parseUrlPath($link.href);
const type = FILE_DOWNLOAD_PATH_REGEX.test(pathname) ? FILE_LINK_TYPE : LINK_TYPE;
let displayAs = DISPLAY_AS_LINK;
if ($link.classList.contains('no_preview')) {
displayAs = DISPLAY_AS_DOWNLOAD_LINK;
} else if ($link.classList.contains('auto_open')) {
displayAs = DISPLAY_AS_EMBED;
} else if ($link.classList.contains('inline_disabled')) {
displayAs = DISPLAY_AS_EMBED_DISABLED;
}
const contentType = $link.getAttribute('data-course-type');
const fileName = $link.getAttribute('title');
const published = $link.getAttribute('data-published') === 'true';
const isPreviewable = $link.getAttribute('data-canvas-previewable') === 'true' || $link.classList.contains('instructure_scribd_file'); // needed to cover docs linked while there was a bug didn't add the data attr.
return {
$element: $link,
displayAs,
text: $link.textContent,
onlyTextSelected: isOnlyTextSelected(editor.selection.getContent()),
type,
isPreviewable,
url: $link.href,
contentType,
fileName,
published
};
}
// the video element is a bit tricky.
// tinymce won't let me add many attributes to the iframe,
// even though I've listed them in tinymce.config.js
// extended_valid_elements.
// we have to rely on the span tinymce wraps around the iframe
// and it's attributes, even though this could change with future
// tinymce releases.
// see https://github.com/tinymce/tinymce/issues/5181
export function asVideoElement($element) {
const $videoElem = findMediaPlayerIframe($element);
if (!isVideoElement($videoElem) && !isStudioEmbeddedMedia($videoElem)) {
return null;
}
return {
...fromVideoEmbed($videoElem),
$element,
type: VIDEO_EMBED_TYPE,
id: $videoElem.parentElement?.getAttribute('data-mce-p-data-media-id') || $videoElem.getAttribute('data-mce-p-data-media-id')
};
}
export function asAudioElement($element) {
if (!$element) {
return null;
}
const $audioIframe = $element.tagName === 'IFRAME' ? $element : $element.firstElementChild;
const $tinymceIframeShim = $audioIframe.parentElement;
const title = ($audioIframe.getAttribute('title') || $tinymceIframeShim.getAttribute('data-mce-p-title') || '').replace(formatMessage('Video player for '), '');
const audioOptions = {
titleText: title,
id: $element.parentElement?.getAttribute('data-mce-p-data-media-id') || $element.getAttribute('data-mce-p-data-media-id')
};
if ($audioIframe.tagName === 'IFRAME') {
const audioDoc = $audioIframe.contentDocument;
try {
const trackSJson = audioDoc.querySelector('[data-tracks]')?.getAttribute('data-tracks');
if (trackSJson) {
audioOptions.tracks = JSON.parse(trackSJson);
}
// eslint-disable-next-line no-empty
} catch (e) {}
}
const source = $audioIframe.getAttribute('src');
const matches = source?.match(/\/media_attachments_iframe\/(\d+)/);
if (matches) {
audioOptions.attachmentId = matches[1];
}
return audioOptions;
}
function asText($element, editor) {
const text = editor && editor.selection.getContent({
format: 'text'
});
if (!text) {
return null;
}
return {
$element,
text,
type: TEXT_TYPE
};
}
function asNone($element) {
return {
$element: $element || null,
type: NONE_TYPE
};
}
export function getContentFromElement($element, editor) {
if (!($element && $element.nodeName)) {
return asNone();
}
const content = asLink($element, editor) || asImageEmbed($element) || asVideoElement($element) || asText($element, editor) || asNone($element);
return content;
}
export function getContentFromEditor(editor, expandSelection = false) {
let $element;
if (editor && editor.selection) {
// tinymce selects the element around the cursor, whether it's
// content is selected in the copy/paste sense or not.
// We want to include this content if it's _really_ selected,
// or if editing the surrounding link, but not if creating a new link
if (expandSelection || !editor.selection.isCollapsed()) {
$element = editor.selection.getNode();
}
}
if ($element == null) {
return asNone();
}
return getContentFromElement($element, editor);
}
// if the selection is somewhere w/in a <a>,
// find the <a> and return it's info
export function getLinkContentFromEditor(editor) {
const $element = editor.selection.getNode();
return $element ? asLink($element, editor) : null;
}
export function isFileLink($element, editor) {
return !!asLink($element, editor);
}
export function isImageEmbed($element) {
return !!asImageEmbed($element) && !$element.getAttribute('data-placeholder-for');
}
function isMediaElement($element, mediaType) {
// the video is hosted in an iframe, but tinymce
// wraps it in a span with swizzled attribute names
if (!$element?.getAttribute || !$element) {
return false;
}
const tinymceIframeShim = $element.tagName === 'IFRAME' ? $element.parentElement : $element;
if (tinymceIframeShim.firstElementChild?.tagName !== 'IFRAME') {
return false;
}
const media_obj_id = tinymceIframeShim.getAttribute('data-mce-p-data-media-id');
const is_media_attachment_iframe = tinymceIframeShim.getAttribute('data-mce-p-src')?.includes('media_attachments_iframe');
if (!media_obj_id && !is_media_attachment_iframe) {
return false;
}
const media_type = tinymceIframeShim.getAttribute('data-mce-p-data-media-type');
return media_type === mediaType;
}
export function isVideoElement($element) {
return isMediaElement($element, 'video');
}
export function isAudioElement($element) {
return isMediaElement($element, 'audio');
}