@instructure/canvas-rce
Version:
A component wrapping Canvas's usage of Tinymce
234 lines • 8.75 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 React, { useEffect, useState } from 'react';
import { checkPropTypes, bool, func, object, oneOf, shape, string } from 'prop-types';
import { Button, CloseButton } from '@instructure/ui-buttons';
import { Alert } from '@instructure/ui-alerts';
import { Heading } from '@instructure/ui-heading';
import { FormFieldGroup } from '@instructure/ui-form-field';
import { Checkbox } from '@instructure/ui-checkbox';
import { RadioInputGroup, RadioInput } from '@instructure/ui-radio-input';
import { TextInput } from '@instructure/ui-text-input';
import { Flex } from '@instructure/ui-flex';
import { Tray } from '@instructure/ui-tray';
import { View } from '@instructure/ui-view';
import validateURL from '../../validateURL';
import formatMessage from '../../../../../format-message';
import { DISPLAY_AS_LINK, DISPLAY_AS_EMBED, DISPLAY_AS_EMBED_DISABLED, DISPLAY_AS_DOWNLOAD_LINK } from '../../../shared/ContentSelection';
import { getTrayHeight } from '../../../shared/trayUtils';
import { instuiPopupMountNodeFn } from '../../../../../util/fullscreenHelpers';
export default function LinkOptionsTray(props) {
const content = props.content || {};
const textToLink = content.text || '';
const showText = content.onlyTextSelected;
const [text, setText] = useState(textToLink || '');
const [url, setUrl] = useState(content.url || '');
const [err, setErr] = useState(null);
const [isValidURL, setIsValidURL] = useState(false);
const [autoOpenPreview, setAutoOpenPreview] = useState(content.displayAs === DISPLAY_AS_EMBED);
const [disableInlinePreview, setDisableInlinePreview] = useState(content.displayAs === DISPLAY_AS_EMBED_DISABLED || content.displayAs === DISPLAY_AS_DOWNLOAD_LINK);
const [displayOptionSelection, setDisplayOptionSelection] = useState(initialPreviewSelection(content));
useEffect(() => {
try {
const v = validateURL(url);
setIsValidURL(v);
setErr(null);
} catch (ex) {
setIsValidURL(false);
setErr(ex.message);
}
}, [url]);
function handleSave(event) {
event.preventDefault();
const embedType = content.isPreviewable ? 'scribd' : null;
const linkAttrs = {
embed: embedType ? {
type: embedType,
autoOpenPreview: autoOpenPreview && !disableInlinePreview,
disableInlinePreview,
noPreview: displayOptionSelection === 'disable'
} : null,
text,
href: url,
id: content.id || null,
forceRename: true // A change to "text" should always update the link's text
};
props.onSave(linkAttrs);
}
function initialPreviewSelection(previewContent) {
if (previewContent.displayAs === DISPLAY_AS_DOWNLOAD_LINK) {
return 'disable';
} else if (previewContent.displayAs === DISPLAY_AS_EMBED || previewContent.displayAs === DISPLAY_AS_LINK) {
return 'inline';
} else {
return 'overlay';
}
}
function handleTextChange(event) {
setText(event.target.value);
}
function handleLinkChange(event) {
setUrl(event.target.value);
}
function handlePreviewChange(event) {
setAutoOpenPreview(event.target.checked);
}
function handlePreviewOptionChange(_event, value) {
setDisableInlinePreview(value === 'overlay' || value === 'disable');
setDisplayOptionSelection(value);
}
function renderDisplayOptions() {
return /*#__PURE__*/React.createElement(FormFieldGroup, {
description: formatMessage('Display Options'),
layout: "stacked",
rowSpacing: "small"
}, /*#__PURE__*/React.createElement(RadioInputGroup, {
description: " " /* the FormFieldGroup is providing the label */,
name: "preview_option",
onChange: handlePreviewOptionChange,
value: displayOptionSelection
}, /*#__PURE__*/React.createElement(RadioInput, {
key: "disable",
value: "disable",
label: formatMessage('Disable Preview')
}), /*#__PURE__*/React.createElement(RadioInput, {
key: "overlay",
value: "overlay",
label: formatMessage('Preview in overlay')
}), /*#__PURE__*/React.createElement(RadioInput, {
key: "inline",
value: "inline",
label: formatMessage('Preview inline')
})), !disableInlinePreview && /*#__PURE__*/React.createElement(View, {
as: "div",
margin: "0 0 0 small"
}, /*#__PURE__*/React.createElement(Checkbox, {
label: formatMessage('Expand preview by Default'),
name: "auto-preview",
onChange: handlePreviewChange,
checked: autoOpenPreview
})));
}
return /*#__PURE__*/React.createElement(Tray, {
"data-testid": "RCELinkOptionsTray",
"data-mce-component": true,
label: formatMessage('Link Options'),
mountNode: instuiPopupMountNodeFn,
onDismiss: props.onRequestClose,
onEntered: props.onEntered,
onExited: props.onExited,
open: props.open,
placement: "end",
shouldCloseOnDocumentClick: true,
shouldContainFocus: true,
shouldReturnFocus: true
}, /*#__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"
}, formatMessage('Link Options'))), /*#__PURE__*/React.createElement(Flex.Item, null, /*#__PURE__*/React.createElement(CloseButton, {
color: "primary",
onClick: props.onRequestClose,
screenReaderLabel: formatMessage('Close')
})))), /*#__PURE__*/React.createElement(Flex.Item, {
as: "form",
shouldGrow: true,
margin: "none",
shouldShrink: true,
onSubmit: handleSave
}, /*#__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("input", {
type: "submit",
style: {
display: 'none'
}
}), /*#__PURE__*/React.createElement(Flex, {
direction: "column"
}, showText && /*#__PURE__*/React.createElement(Flex.Item, {
padding: "small"
}, /*#__PURE__*/React.createElement(TextInput, {
renderLabel: () => formatMessage('Text'),
onChange: handleTextChange,
value: text
})), /*#__PURE__*/React.createElement(Flex.Item, {
padding: "small"
}, /*#__PURE__*/React.createElement(TextInput, {
renderLabel: () => formatMessage('Link'),
onChange: handleLinkChange,
value: url
})), err && /*#__PURE__*/React.createElement(Flex.Item, {
padding: "small",
"data-testid": "url-error"
}, /*#__PURE__*/React.createElement(Alert, {
variant: "error"
}, err)), content.isPreviewable && /*#__PURE__*/React.createElement(Flex.Item, {
margin: "small none none none",
padding: "small"
}, renderDisplayOptions()))), /*#__PURE__*/React.createElement(Flex.Item, {
background: "secondary",
borderWidth: "small none none none",
padding: "small medium",
textAlign: "end"
}, /*#__PURE__*/React.createElement(Button, {
disabled: showText && text.trim().length === 0 || !(url && isValidURL),
onClick: handleSave,
color: "primary"
}, formatMessage('Done')))))));
}
LinkOptionsTray.propTypes = {
// content is required only if the tray is open
content: (props, _propName, _componentName) => {
if (props.open) {
checkPropTypes({
content: shape({
$element: object,
// the DOM's HTMLElement
dispalyAs: oneOf([DISPLAY_AS_LINK, DISPLAY_AS_EMBED]),
isPreviewable: bool,
text: string,
url: string
}).isRequired
}, props, 'content', 'LinkOptionsTray');
}
},
onEntered: func,
onExited: func,
onRequestClose: func.isRequired,
onSave: func.isRequired,
open: bool.isRequired
};
LinkOptionsTray.defaultProps = {
onEntered: null,
onExited: null
};