@instructure/canvas-rce
Version:
A component wrapping Canvas's usage of Tinymce
220 lines (218 loc) • 7.8 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 formatMessage from '../../../format-message';
import { scaleForHeight, scaleForWidth } from '../shared/DimensionUtils';
import RCEGlobals from '../../../rce/RCEGlobals';
export const MIN_HEIGHT = 10;
export const MIN_WIDTH = 10;
export const MIN_WIDTH_VIDEO = 320;
export const MIN_PERCENTAGE = 10;
export const SMALL = 'small';
export const MEDIUM = 'medium';
export const LARGE = 'large';
export const EXTRA_LARGE = 'extra-large';
export const CUSTOM = 'custom';
export const imageSizes = [SMALL, MEDIUM, LARGE, EXTRA_LARGE, CUSTOM];
export const videoSizes = [MEDIUM, LARGE, EXTRA_LARGE, CUSTOM];
export const studioPlayerSizes = [SMALL, MEDIUM, LARGE, CUSTOM];
const sizeByMaximumDimension = {
200: SMALL,
320: MEDIUM,
400: LARGE,
640: EXTRA_LARGE
};
const studioPlayerDimensions = {
[SMALL]: {
width: 320,
height: 254
},
[MEDIUM]: {
width: 480,
height: 300
},
[LARGE]: {
width: 700,
height: 441
}
};
export const MIN_WIDTH_STUDIO_PLAYER = studioPlayerDimensions[SMALL].width;
export const MIN_HEIGHT_STUDIO_PLAYER = studioPlayerDimensions[SMALL].height;
function parsedOrNull($element, attribute) {
// when the image is first inserted into the rce, it's size
// is constrained by a style attribute with max-width, max-height.
// While it doesn't have a 'width' or 'height' attribute, we can
// still get its width and height directly from the img element
const value = $element.hasAttribute(attribute) ? $element.getAttribute(attribute) : $element[attribute];
return value ? Math.round(Number.parseInt(value, 10)) : null;
}
function imageSizeFromKnownOptions(imageOptions) {
const intendedWidth = imageOptions.appliedWidth || imageOptions.naturalWidth;
const intendedHeight = imageOptions.appliedHeight || imageOptions.naturalHeight;
const largestDimension = Math.max(intendedWidth, intendedHeight);
return sizeByMaximumDimension[largestDimension] || CUSTOM;
}
function hasHeightAndWidth($element) {
return $element.hasAttribute('width') && $element.hasAttribute('height');
}
function getPercentageUnitsFromAttributes($element) {
const getAttribute = attribute => $element.hasAttribute(attribute) ? $element.getAttribute(attribute) : $element[attribute];
const widthValue = getAttribute('width');
const heightValue = getAttribute('height');
const value = [widthValue, heightValue].find(v => /\d+(?:\.\d+)?%/.test(v));
return value ? Math.round(Number.parseInt(value, 10)) : null;
}
export function fromImageEmbed($element) {
const altText = $element.getAttribute('alt');
const percentageUnits = getPercentageUnitsFromAttributes($element);
const imageOptions = {
appliedWidth: parsedOrNull($element, 'width'),
appliedHeight: parsedOrNull($element, 'height'),
naturalWidth: $element.naturalWidth,
naturalHeight: $element.naturalHeight,
appliedPercentage: percentageUnits || 100,
// by default use percentage units
usePercentageUnits: hasHeightAndWidth($element) ? !!percentageUnits : true,
altText: altText || '',
isDecorativeImage: altText !== null && altText.replace(/\s/g, '') === '',
url: $element.src
};
imageOptions.imageSize = imageSizeFromKnownOptions(imageOptions);
return imageOptions;
}
export function fromVideoEmbed($element) {
// $element will be the <span> tinymce wraps around the iframe
// that's hosting the video player
let $videoElem = null;
let $videoDoc;
let naturalWidth, naturalHeight;
const $videoIframe = $element.tagName === 'IFRAME' ? $element : $element.firstElementChild;
const $tinymceIframeShim = $videoIframe.parentElement;
if ($videoIframe.tagName === 'IFRAME') {
$videoDoc = $videoIframe.contentDocument;
if ($videoDoc) {
$videoElem = $videoDoc.querySelector('video');
if ($videoElem && ($videoElem.loadedmetadata || $videoElem.readyState >= 1)) {
naturalWidth = $videoElem.videoWidth;
naturalHeight = $videoElem.videoHeight;
}
} else {
naturalHeight = $videoIframe.clientHeight;
naturalWidth = $videoIframe.clientWidth;
}
}
// because tinymce doesn't put the title attribute on the iframe,
// but maintains it on the span it adds around it.
const title = ($videoIframe.getAttribute('title') || $tinymceIframeShim.getAttribute('data-mce-p-title') || '').replace(formatMessage('Video player for '), '');
const rect = $element.getBoundingClientRect();
const videoOptions = {
titleText: title || '',
appliedHeight: rect.height,
appliedWidth: rect.width,
naturalHeight,
naturalWidth,
source: $videoElem && $videoElem.querySelector('source')
};
try {
const trackjson = $videoDoc.querySelector('[data-tracks]')?.getAttribute('data-tracks');
if (trackjson) {
videoOptions.tracks = JSON.parse(trackjson);
}
} catch (_ignore) {
// bad json?
}
if (RCEGlobals.getFeatures()?.consolidated_media_player) {
const width = videoOptions.appliedWidth || videoOptions.naturalWidth;
videoOptions.videoSize = [SMALL, MEDIUM, LARGE].find(size => width === studioPlayerDimensions[size].width) || CUSTOM;
} else {
videoOptions.videoSize = imageSizeFromKnownOptions(videoOptions);
}
const source = $videoIframe.getAttribute('src');
const matches = source?.match(/\/media_attachments_iframe\/(\d+)/);
if (matches) {
videoOptions.attachmentId = matches[1];
}
return videoOptions;
}
export function scaleImageForHeight(naturalWidth, naturalHeight, targetHeight) {
const constraints = {
minHeight: MIN_HEIGHT,
minWidth: MIN_WIDTH
};
return scaleForHeight(naturalWidth, naturalHeight, targetHeight, constraints);
}
export function scaleImageForWidth(naturalWidth, naturalHeight, targetWidth) {
const constraints = {
minHeight: MIN_HEIGHT,
minWidth: MIN_WIDTH
};
return scaleForWidth(naturalWidth, naturalHeight, targetWidth, constraints);
}
export function scaleToSize(imageSize, naturalWidth, naturalHeight) {
if (imageSize === CUSTOM) {
return {
width: naturalWidth,
height: naturalHeight
};
}
const [dimension] = Object.entries(sizeByMaximumDimension).find(([, size]) => size === imageSize);
const scaleFactor = dimension / Math.max(naturalWidth, naturalHeight);
return {
height: Math.round(naturalHeight * scaleFactor),
width: Math.round(naturalWidth * scaleFactor)
};
}
export function scaleVideoSize(videoSize, naturalWidth, naturalHeight) {
if (videoSize === CUSTOM) {
return {
width: naturalWidth,
height: naturalHeight
};
}
const {
width,
height
} = studioPlayerDimensions[videoSize];
return {
width,
height
};
}
export function labelForImageSize(imageSize) {
switch (imageSize) {
case SMALL:
{
return formatMessage('Small');
}
case MEDIUM:
{
return formatMessage('Medium');
}
case LARGE:
{
return formatMessage('Large');
}
case EXTRA_LARGE:
{
return formatMessage('Extra Large');
}
default:
{
return formatMessage('Custom');
}
}
}