@instructure/canvas-rce
Version:
A component wrapping Canvas's usage of Tinymce
306 lines (302 loc) • 11.3 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 { bool, element, func, oneOf, oneOfType, string } from 'prop-types';
import formatMessage from '../../../format-message';
import { Flex } from '@instructure/ui-flex';
import { View } from '@instructure/ui-view';
import { TextInput } from '@instructure/ui-text-input';
import { SimpleSelect } from '@instructure/ui-simple-select';
import { IconButton } from '@instructure/ui-buttons';
import { ScreenReaderContent } from '@instructure/ui-a11y-content';
import { ICON_MAKER_ICONS } from '../instructure_icon_maker/svg/constants';
import { IconLinkLine, IconFolderLine, IconImageLine, IconDocumentLine, IconAttachMediaLine, IconSearchLine, IconXLine } from '@instructure/ui-icons';
function fileLabelFromContext(contextType) {
switch (contextType) {
case 'user':
return formatMessage('User Files');
case 'course':
return formatMessage('Course Files');
case 'group':
return formatMessage('Group Files');
case 'files':
default:
return formatMessage('Files');
}
}
function renderTypeOptions(contentType, contentSubtype, userContextType) {
const options = [/*#__PURE__*/React.createElement(SimpleSelect.Option, {
key: "links",
id: "links",
value: "links",
renderBeforeLabel: IconLinkLine
}, formatMessage('Links'))];
if (userContextType === 'course' && contentType !== 'links' && contentSubtype !== 'all') {
options.push(/*#__PURE__*/React.createElement(SimpleSelect.Option, {
key: "course_files",
id: "course_files",
value: "course_files",
renderBeforeLabel: IconFolderLine
}, fileLabelFromContext('course')));
}
if (userContextType === 'group' && contentType !== 'links' && contentSubtype !== 'all') {
options.push(/*#__PURE__*/React.createElement(SimpleSelect.Option, {
key: "group_files",
id: "group_files",
value: "group_files",
renderBeforeLabel: IconFolderLine
}, fileLabelFromContext('group')));
}
// Icon Maker icons are only stored in course folders.
if (contentSubtype !== ICON_MAKER_ICONS) {
options.push(/*#__PURE__*/React.createElement(SimpleSelect.Option, {
key: "user_files",
id: "user_files",
value: "user_files",
renderBeforeLabel: IconFolderLine
}, fileLabelFromContext(contentType === 'links' || contentSubtype === 'all' ? 'files' : 'user')));
}
return options;
}
function renderType(contentType, contentSubtype, mountNode, onChange, userContextType, containingContextType) {
// Check containingContextType so that we always show context links
if (containingContextType === 'course' || containingContextType === 'group') {
return /*#__PURE__*/React.createElement(SimpleSelect, {
"data-testid": "filter-content-type",
mountNode: mountNode,
renderLabel: /*#__PURE__*/React.createElement(ScreenReaderContent, null, formatMessage('Content Type')),
assistiveText: formatMessage('Use arrow keys to navigate options.'),
onChange: (e, selection) => {
const changed = {
contentType: selection.value
};
if (contentType === 'links') {
// when changing away from links, go to all user files
changed.contentSubtype = 'all';
}
onChange(changed);
},
value: contentType
}, renderTypeOptions(contentType, contentSubtype, userContextType));
} else {
return /*#__PURE__*/React.createElement(View, {
as: "div",
borderWidth: "small",
padding: "x-small small",
borderRadius: "medium",
width: "100%"
}, /*#__PURE__*/React.createElement(ScreenReaderContent, null, formatMessage('Content Type')), fileLabelFromContext('user', contentSubtype));
}
}
function shouldSearch(searchString) {
return searchString.length === 0 || searchString.length >= 3;
}
export default function Filter(props) {
const {
contentType,
contentSubtype,
mountNode,
onChange,
sortValue,
searchString,
userContextType,
isContentLoading,
containingContextType
} = props;
const [pendingSearchString, setPendingSearchString] = useState(searchString);
const [searchInputTimer, setSearchInputTimer] = useState(0);
// only run on mounting to trigger change to correct contextType
useEffect(() => {
onChange({
contentType
});
}, []); // eslint-disable-line react-hooks/exhaustive-deps
function doSearch(value) {
if (shouldSearch(value)) {
if (searchInputTimer) {
window.clearTimeout(searchInputTimer);
setSearchInputTimer(0);
}
onChange({
searchString: value
});
}
}
function handleChangeSearch(value) {
setPendingSearchString(value);
if (searchInputTimer) {
window.clearTimeout(searchInputTimer);
}
const tid = window.setTimeout(() => doSearch(value), 250);
setSearchInputTimer(tid);
}
function handleClear() {
handleChangeSearch('');
}
function renderClearButton() {
if (pendingSearchString) {
return /*#__PURE__*/React.createElement(IconButton, {
screenReaderLabel: formatMessage('Clear'),
onClick: handleClear,
withBorder: false,
withBackground: false,
size: "small"
}, /*#__PURE__*/React.createElement(IconXLine, null));
}
return undefined;
}
const searchMessage = formatMessage('Enter at least 3 characters to search');
const loadingMessage = formatMessage('Loading, please wait');
const msg = isContentLoading ? loadingMessage : searchMessage;
const isEdit = contentSubtype === 'edit';
return /*#__PURE__*/React.createElement(View, {
display: "block",
direction: "column"
}, !isEdit && renderType(contentType, contentSubtype, mountNode, onChange, userContextType, containingContextType), contentType !== 'links' && /*#__PURE__*/React.createElement(Flex, {
margin: "small none none none"
}, /*#__PURE__*/React.createElement(Flex.Item, {
shouldGrow: true,
shouldShrink: true,
margin: "none xx-small none none"
}, /*#__PURE__*/React.createElement(SimpleSelect, {
"data-testid": "filter-content-subtype",
mountNode: mountNode,
renderLabel: /*#__PURE__*/React.createElement(ScreenReaderContent, null, formatMessage('Content Subtype')),
assistiveText: formatMessage('Use arrow keys to navigate options.'),
onChange: (e, selection) => {
const changed = {
contentSubtype: selection.value
};
if (changed.contentSubtype === 'all') {
// when flipped to All, the context needs to be user
// so we can get media_objects, which are all returned in the user context
changed.contentType = 'user_files';
} else if (changed.contentSubtype === ICON_MAKER_ICONS) {
// Icon Maker icons only belong to Courses.
changed.contentType = 'course_files';
}
onChange(changed);
},
value: contentSubtype
}, /*#__PURE__*/React.createElement(SimpleSelect.Option, {
id: "images",
value: "images",
renderBeforeLabel: IconImageLine
}, formatMessage('Images')), /*#__PURE__*/React.createElement(SimpleSelect.Option, {
id: "documents",
value: "documents",
renderBeforeLabel: IconDocumentLine
}, formatMessage('Documents')), /*#__PURE__*/React.createElement(SimpleSelect.Option, {
id: "media",
value: "media",
renderBeforeLabel: IconAttachMediaLine
}, formatMessage('Media')), props.use_rce_icon_maker && /*#__PURE__*/React.createElement(SimpleSelect.Option, {
id: ICON_MAKER_ICONS,
value: ICON_MAKER_ICONS,
renderBeforeLabel: IconImageLine
}, formatMessage('Icon Maker Icons')), /*#__PURE__*/React.createElement(SimpleSelect.Option, {
id: "all",
value: "all"
}, formatMessage('All')))), contentSubtype !== 'all' && /*#__PURE__*/React.createElement(Flex.Item, {
shouldGrow: true,
shouldShrink: true,
margin: "none none none xx-small"
}, /*#__PURE__*/React.createElement(SimpleSelect, {
"data-testid": "filter-sort-by",
mountNode: mountNode,
renderLabel: /*#__PURE__*/React.createElement(ScreenReaderContent, null, formatMessage('Sort By')),
assistiveText: formatMessage('Use arrow keys to navigate options.'),
onChange: (e, selection) => {
onChange({
sortValue: selection.value
});
},
value: sortValue
}, /*#__PURE__*/React.createElement(SimpleSelect.Option, {
id: "date_added",
value: "date_added"
}, formatMessage('Date Added')), /*#__PURE__*/React.createElement(SimpleSelect.Option, {
id: "alphabetical",
value: "alphabetical"
}, formatMessage('Alphabetical'))))), /*#__PURE__*/React.createElement(View, {
as: "div",
margin: "small none none none"
}, /*#__PURE__*/React.createElement(TextInput, {
renderLabel: /*#__PURE__*/React.createElement(ScreenReaderContent, null, formatMessage('Search')),
renderBeforeInput: /*#__PURE__*/React.createElement(IconSearchLine, {
inline: false
}),
renderAfterInput: renderClearButton(),
messages: [{
type: 'hint',
text: msg
}],
placeholder: formatMessage('Search'),
value: pendingSearchString,
onChange: (e, value) => handleChangeSearch(value),
onKeyDown: e => {
if (e.key === 'Enter') {
doSearch(pendingSearchString);
}
}
})));
}
Filter.propTypes = {
/**
* `contentSubtype` is the secondary filter setting, currently only used when
* `contentType` is set to "files"
*/
contentSubtype: string.isRequired,
/**
* `contentType` is the primary filter setting (e.g. links, files)
*/
contentType: oneOf(['links', 'user_files', 'course_files', 'group_files']).isRequired,
/**
* `mountNode` is where INSTUI popups should mount. This is necessary for them
* to work correctly when the RCE is in fullscreen
*/
mountNode: oneOfType([element, func]),
/**
* `onChange` is called when any of the Filter settings are changed
*/
onChange: func.isRequired,
/**
* `sortValue` defines how items in the CanvasContentTray are sorted
*/
sortValue: string.isRequired,
/**
* `searchString` is used to search for matching file names. Must be >3 chars long
*/
searchString: string.isRequired,
/**
* The user's context
*/
userContextType: oneOf(['user', 'course', 'group']),
/**
* Is my content currently loading?
*/
isContentLoading: bool,
/**
* The page context
*/
containingContextType: oneOf(['user', 'course', 'group']),
/**
* Should include Icon Maker?
*/
use_rce_icon_maker: bool
};