UNPKG

@instructure/canvas-rce

Version:

A component wrapping Canvas's usage of Tinymce

239 lines (236 loc) 8.06 kB
/* * Copyright (C) 2017 - 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/>. */ /** * Flash alerts, especially for ajax error messages * Typical usage: * import {showFlashError} from './FlashAlert' * ... * axios.put(url, data).then((response) => { * // do something with response * }).catch(showFlashError(your_error_message)) * * showFlashError() with no argument shows a generic message * * On error, will display an inst-ui Alert at the top of the page * with an error message and "Details" button. When the user clicks * the button, it shows error details extracted from the axios Error * * You could also import the lower level showFlashAlert function or * the FlashAlert component if you need more control * * showFlashAlert({ err, message, type }, onCloseCallback) * err: error object * message: user-facing message * type: one of ['info', 'success', 'warning', 'error'] * default is 'info' unless an error object is passed in, else is 'error' */ import React from 'react'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import formatMessage from '../format-message'; import { Alert } from '@instructure/ui-alerts'; import { Link } from '@instructure/ui-link'; import { Text } from '@instructure/ui-text'; import { PresentationContent, ScreenReaderContent } from '@instructure/ui-a11y-content'; import { Transition } from '@instructure/ui-motion'; import RCEGlobals from '../rce/RCEGlobals'; const messageHolderId = 'flashalert_message_holder'; // specs fail if I reuse jquery's elements const screenreaderMessageHolderId = 'flash_screenreader_holder'; const TIMEOUT = 10000; function getLiveRegion() { // return element where flash screenreader messages go. // create if necessary let liveRegion = document.getElementById(screenreaderMessageHolderId); if (!liveRegion) { liveRegion = document.createElement('div'); liveRegion.id = screenreaderMessageHolderId; liveRegion.setAttribute('role', 'alert'); document.body.appendChild(liveRegion); } return liveRegion; } // An Alert with a message and "Details" button which surfaces // more info about the error when pressed. // Is displayed at the top of the document, and will close itself after a while export default class FlashAlert extends React.Component { constructor(props) { super(props); this.showDetails = () => { this.setState({ showDetails: true }); clearTimeout(this.timerId); this.timerId = setTimeout(() => this.closeAlert(), this.props.timeout); }; this.closeAlert = () => { this.setState({ isOpen: false }, () => { setTimeout(() => { clearTimeout(this.timerId); this.props.onClose(); }, 500); }); }; this.state = { showDetails: false, isOpen: true }; this.timerId = 0; } findDetailMessage() { const err = this.props.error; let a = err.message; let b; if (err.response) { if (err.response.data) { try { if (Array.isArray(err.response.data.errors)) { // probably a canvas api a = err.response.data.errors[0].message; b = err.message; } else if (err.response.data.message) { // probably a canvas api too a = err.response.data.message; b = err.message; } } catch (ignore) { a = err.message; } } } return { a, b }; } renderDetailMessage() { const { a, b } = this.findDetailMessage(); return /*#__PURE__*/React.createElement(Text, { as: "p", fontStyle: "italic" }, /*#__PURE__*/React.createElement(Text, null, a), b ? /*#__PURE__*/React.createElement("br", null) : null, b ? /*#__PURE__*/React.createElement(Text, null, b) : null); } render() { let details = null; if (this.props.error) { if (this.state.showDetails) { details = this.renderDetailMessage(); } else { details = /*#__PURE__*/React.createElement("span", null, /*#__PURE__*/React.createElement(PresentationContent, null, /*#__PURE__*/React.createElement(Link, { as: "button", onClick: this.showDetails }, formatMessage('Details'))), /*#__PURE__*/React.createElement(ScreenReaderContent, null, this.renderDetailMessage())); } } return /*#__PURE__*/React.createElement(Transition, { transitionOnMount: true, in: this.state.isOpen, type: "fade" }, /*#__PURE__*/React.createElement(Alert, { variant: this.props.variant, renderCloseButtonLabel: formatMessage('Close'), onDismiss: this.closeAlert, margin: "small auto", timeout: this.props.timeout, liveRegion: getLiveRegion, transition: "fade", screenReaderOnly: this.props.screenReaderOnly }, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("p", { style: { margin: '0 -5px' } }, this.props.message), details))); } } FlashAlert.propTypes = { onClose: PropTypes.func.isRequired, message: PropTypes.string.isRequired, error: PropTypes.instanceOf(Error), variant: PropTypes.oneOf(['info', 'success', 'warning', 'error']), timeout: PropTypes.number, screenReaderOnly: PropTypes.bool }; FlashAlert.defaultProps = { error: null, variant: 'info', timeout: TIMEOUT, screenReaderOnly: false }; export function showFlashAlert({ message, err, type = err ? 'error' : 'info', srOnly = false }) { function closeAlert(atNode) { ReactDOM.unmountComponentAtNode(atNode); atNode.remove(); } function getAlertContainer() { let alertContainer = document.getElementById(messageHolderId); if (!alertContainer) { alertContainer = document.createElement('div'); alertContainer.classList.add('clickthrough-container'); alertContainer.id = messageHolderId; alertContainer.setAttribute('style', 'position: fixed; top: 0; left: 0; width: 100%; z-index: 100000;'); document.body.appendChild(alertContainer); } return alertContainer; } function renderAlert(parent) { const configuredTimeout = RCEGlobals.getConfig()?.flashAlertTimeout; ReactDOM.render(/*#__PURE__*/React.createElement(FlashAlert, { message: message, timeout: Number.isNaN(parseInt(configuredTimeout, 10)) ? TIMEOUT : configuredTimeout, error: err, variant: type, onClose: closeAlert.bind(null, parent), liveRegion: getLiveRegion, screenReaderOnly: srOnly }), parent); } const div = document.createElement('div'); // div.setAttribute('class', styles.flashMessage) div.setAttribute('style', 'max-width:50em;margin:1rem auto;'); div.setAttribute('class', 'flashalert-message'); getAlertContainer().appendChild(div); renderAlert(div); } export function destroyContainer() { const container = document.getElementById(messageHolderId); const liveRegion = document.getElementById(screenreaderMessageHolderId); if (container) container.remove(); if (liveRegion) liveRegion.remove(); } export function showFlashError(message = formatMessage('An error occurred making a network request')) { return err => showFlashAlert({ message, err, type: 'error' }); } export function showFlashSuccess(message) { return () => showFlashAlert({ message, type: 'success' }); }