Skip to content

Commit

Permalink
Merge branch 'instructure:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
ianespana authored Jul 10, 2021
2 parents 78be0ec + 04fe255 commit d7ff213
Show file tree
Hide file tree
Showing 12 changed files with 306 additions and 83 deletions.
5 changes: 2 additions & 3 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,9 @@ module.exports = {
'^Backbone$': '<rootDir>/public/javascripts/Backbone.js',
// jest can't import the icons
'@instructure/ui-icons/es/svg': '<rootDir>/packages/canvas-rce/src/rce/__tests__/_mockIcons.js',
// redirect import from es/rce/CanvasRce to lib
'@instructure/canvas-rce/es/rce/CanvasRce':
'<rootDir>/packages/canvas-rce/lib/rce/CanvasRce.js',
// redirect imports from es/rce to lib
'@instructure/canvas-rce/es/rce/tinyRCE': '<rootDir>/packages/canvas-rce/lib/rce/tinyRCE.js',
'@instructure/canvas-rce/es/rce/RCE': '<rootDir>/packages/canvas-rce/lib/rce/RCE.js',
// mock the tinymce-react Editor react component
'@tinymce/tinymce-react': '<rootDir>/packages/canvas-rce/src/rce/__mocks__/tinymceReact.js'
},
Expand Down
8 changes: 5 additions & 3 deletions jest/jest-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ filterUselessConsoleMessages(console)
require('jest-fetch-mock').enableFetchMocks()

window.scroll = () => {}
window.ENV = {}
window.ENV = {
use_rce_enhancements: true
}

Enzyme.configure({adapter: new Adapter()})

Expand Down Expand Up @@ -75,8 +77,8 @@ if (process.env.DEPRECATION_SENTRY_DSN) {
}
}).install()

const setupRavenConsoleLoggingPlugin = require('../app/jsx/shared/helpers/setupRavenConsoleLoggingPlugin')
.default
const setupRavenConsoleLoggingPlugin =
require('../ui/boot/initializers/setupRavenConsoleLoggingPlugin').default
setupRavenConsoleLoggingPlugin(Raven, {loggerName: 'console-jest'})
}

Expand Down
14 changes: 8 additions & 6 deletions packages/canvas-rce/demo/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import React, {useEffect, useRef, useState} from 'react'
import ReactDOM from 'react-dom'
import CanvasRce from '../src/rce/CanvasRce'
import RCE from '../src/rce/RCE'
import DemoOptions from './DemoOptions'
import {Button} from '@instructure/ui-buttons'
import {View} from '@instructure/ui-view'
Expand Down Expand Up @@ -183,18 +183,20 @@ function Demo() {
return (
<>
<main className="main" id="content">
<CanvasRce
<RCE
ref={rceRef}
language={lang}
textareaId="textarea3"
defaultContent="hello RCE"
readOnly={readonly}
height={350}
editorOptions={{
height: 350,
toolbar,
menu,
plugins
}}
highContrastCSS={[]}
rcsProps={rcsProps}
toolbar={toolbar}
menu={menu}
plugins={plugins}
onInitted={editor => {
setCurrentContent(editor.getContent())
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
*/

import React, {forwardRef, useState} from 'react'
import {arrayOf, bool, func, number, object, objectOf, oneOfType, shape, string} from 'prop-types'
import {arrayOf, bool, func, number, objectOf, oneOfType, shape, string} from 'prop-types'
import formatMessage from '../format-message'
import RCEWrapper, {toolbarPropType, menuPropType, ltiToolsPropType} from './RCEWrapper'
import RCEWrapper, {editorOptionsPropType, ltiToolsPropType} from './RCEWrapper'
import {trayPropTypes} from './plugins/shared/CanvasContentTray'
import editorLanguage from './editorLanguage'
import normalizeLocale from './normalizeLocale'
Expand All @@ -40,7 +40,7 @@ if (!process?.env?.BUILD_LOCALE) {
// forward rceRef to it refs the RCEWrapper where clients can call getCode etc. on it.
// You probably shouldn't use it until onInit has been called. Until then tinymce
// is not initialized.
const CanvasRce = forwardRef(function CanvasRce(props, rceRef) {
const RCE = forwardRef(function RCE(props, rceRef) {
const {
autosave,
defaultContent,
Expand All @@ -51,13 +51,10 @@ const CanvasRce = forwardRef(function CanvasRce(props, rceRef) {
language,
liveRegion,
mirroredAttrs, // attributes to transfer from the original textarea to the one created by tinymce
menu,
plugins,
readOnly,
textareaId,
textareaClassName,
rcsProps,
toolbar,
use_rce_pretty_html_editor,
use_rce_buttons_and_icons,
onFocus,
Expand Down Expand Up @@ -96,23 +93,15 @@ const CanvasRce = forwardRef(function CanvasRce(props, rceRef) {
instRecordDisabled,
language: normalizeLocale(language),
liveRegion,
menu,
plugins,
textareaId,
textareaClassName,
trayProps: rcsProps,
toolbar,
use_rce_pretty_html_editor,
use_rce_buttons_and_icons,
editorOptions: Object.assign(editorOptions, editorOptions, {
selector: `#${textareaId}`,
height,
language: editorLanguage(props.language),
toolbar: props.toolbar,
menu: props.menu,
menubar: props.menu ? Object.keys(props.menu).join(' ') : undefined,
plugins: props.plugins,
readonly: readOnly
language: editorLanguage(props.language)
})
}
wrapInitCb(mirroredAttrs, iProps.editorOptions)
Expand All @@ -139,18 +128,19 @@ const CanvasRce = forwardRef(function CanvasRce(props, rceRef) {
}
})

export default CanvasRce
export default RCE

CanvasRce.propTypes = {
RCE.propTypes = {
// do you want the rce to autosave content to localStorage, and
// how long should it be until it's deleted.
// If autosave is enabled, call yourRef.RCEClosed() if the user
// exits the page normally (e.g. via Cancel or Save)
autosave: shape({enabled: bool, maxAge: number}),
// the initial content
defaultContent: string,
// tinymce configuration. See defaultTinymceConfig for the basics
editorOptions: object,
// tinymce configuration. See defaultTinymceConfig for all the defaults
// and RCEWrapper.editorOptionsPropType for stuff you may want to include
editorOptions: editorOptionsPropType,
// height of the RCE. if a number, in px
height: oneOfType([number, string]),
// array of URLs to high-contrast css
Expand Down Expand Up @@ -179,10 +169,6 @@ CanvasRce.propTypes = {
// name:value pairs of attributes to add to the textarea
// tinymce creates as the backing store of the RCE
mirroredAttrs: objectOf(string),
// additional menu items that get merged into the default menubar
menu: menuPropType,
// additional plugins that get merged into the default list of plugins
plugins: arrayOf(string),
// is this RCE readonly?
readOnly: bool,
// id put on the generated textarea
Expand All @@ -192,8 +178,6 @@ CanvasRce.propTypes = {
// properties necessary for the RCE to us the RCS
// if missing, RCE features that require the RCS are omitted
rcsProps: trayPropTypes,
// additional toolbar items that get merged into the default toolbars
toolbar: toolbarPropType,
// enable the pretty html editor (temporary until the feature is forced on)
use_rce_pretty_html_editor: bool,
// enable the custom buttons feature (temporary until the feature is forced on)
Expand All @@ -202,10 +186,10 @@ CanvasRce.propTypes = {
onFocus: func, // f(RCEWrapper component)
onBlur: func, // f(event)
onInit: func, // f(tinymce_editor)
onContentChange: func // f(content), don't mistake this as an indication CanvasRce is a controlled component
onContentChange: func // f(content), don't mistake this as an indication RCE is a controlled component
}

CanvasRce.defaultProps = {
RCE.defaultProps = {
autosave: {enabled: false, maxAge: 3600000},
defaultContent: '',
editorOptions: {...defaultTinymceConfig},
Expand Down
54 changes: 42 additions & 12 deletions packages/canvas-rce/src/rce/RCEWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ const toolbarPropType = PropTypes.arrayOf(
PropTypes.shape({
// name of the toolbar the items are added to
// if this toolbar doesn't exist, it is created
// tinymce toolbar config does not
// include a key to identify the individual toolbars, just a name
// which is translated. This toolbar's name must be translated
// in order to be merged correctly.
name: PropTypes.string.isRequired,
// items added to the toolbar
// each is the name of the button some plugin has
Expand All @@ -70,10 +74,11 @@ const toolbarPropType = PropTypes.arrayOf(
)

const menuPropType = PropTypes.objectOf(
// the key is the name of the menu item some plugin has
// registered with tinymce
// the key is the name of the menu item a plugin has
// registered with tinymce. If it does not exist in the
// default menubar, it will be added.
PropTypes.shape({
// if this is a new menu in the menubar,title it's label.
// if this is a new menu in the menubar, title is it's label.
// if these are items being merged into an existing menu, title is ignored
title: PropTypes.string,
// items is a space separated list it menu_items
Expand All @@ -90,6 +95,26 @@ const ltiToolsPropType = PropTypes.arrayOf(
})
)

export const editorOptionsPropType = PropTypes.shape({
// height of the RCE.
// if a number interpreted as pixels.
// if a string as a CSS value.
height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
// entries you want merged into the toolbar. See toolBarPropType above.
toolbar: toolbarPropType,
// entries you want merged into to the menus. See menuPropType above.
// If an entry defines a new menu, tinymce's menubar config option will
// be updated for you. In fact, if you provide an editorOptions.menubar value
// it will be overwritten.
menu: menuPropType,
// additional plugins that get merged into the default list of plugins
// it is up to you to import the plugin's definition which will
// register it and any related toolbar or menu entries with tinymce.
plugins: PropTypes.arrayOf(PropTypes.string),
// is this RCE readonly?
readonly: PropTypes.bool
})

// we `require` instead of `import` because the ui-themeable babel require hook only works with `require`
// 2021-04-21: This is no longer true, but I didn't want to make a gratutious change when I found this out.
// see https://gerrit.instructure.com/c/canvas-lms/+/263299/2/packages/canvas-rce/src/rce/RCEWrapper.js#50
Expand Down Expand Up @@ -216,7 +241,7 @@ class RCEWrapper extends React.Component {
maxAge: PropTypes.number
}),
defaultContent: PropTypes.string,
editorOptions: PropTypes.object,
editorOptions: editorOptionsPropType,
handleUnmount: PropTypes.func,
editorView: PropTypes.oneOf([WYSIWYG_VIEW, PRETTY_HTML_EDITOR_VIEW, RAW_HTML_EDITOR_VIEW]),
id: PropTypes.string,
Expand Down Expand Up @@ -941,6 +966,10 @@ class RCEWrapper extends React.Component {
// first view
this.setEditorView(this.state.editorView)

// readonly should have been handled via the init property passed
// to <Editor>, but it's not.
editor.mode.set(this.props.readOnly ? 'readonly' : 'design')

this.props.onInitted?.(editor)
}

Expand Down Expand Up @@ -1303,9 +1332,15 @@ class RCEWrapper extends React.Component {
canvasPlugins.push('instructure_buttons')
}

const possibleNewMenubarItems = this.props.editorOptions.menu
? Object.keys(this.props.editorOptions.menu).join(' ')
: undefined

const wrappedOpts = {
...options,

readonly: this.props.readOnly,

theme: 'silver', // some older code specified 'modern', which doesn't exist any more

height: options.height || DEFAULT_RCE_HEIGHT,
Expand Down Expand Up @@ -1343,7 +1378,7 @@ class RCEWrapper extends React.Component {
content_css: options.content_css || [],
content_style: contentCSS,

menubar: mergeMenuItems('edit view insert format tools table', options.menubar),
menubar: mergeMenuItems('edit view insert format tools table', possibleNewMenubarItems),
// default menu options listed at https://www.tiny.cloud/docs/configure/editor-appearance/#menu
// tinymce's default edit and table menus are fine
// we include all the canvas specific items in the menu and toolbar
Expand Down Expand Up @@ -1433,14 +1468,13 @@ class RCEWrapper extends React.Component {
'instructure_media_embed',
'instructure_external_tools',
'a11y_checker',
'wordcount'
'wordcount',
...canvasPlugins
],
sanitizePlugins(options.plugins)
)
}

wrappedOpts.plugins.splice(wrappedOpts.plugins.length, 0, ...canvasPlugins)

if (this.props.trayProps) {
wrappedOpts.canvas_rce_user_context = {
type: this.props.trayProps.contextType,
Expand Down Expand Up @@ -1740,10 +1774,6 @@ function mergeMenu(standard, custom) {
// returns: the merged result by mutating the incoming standard arg.
// It will add commands to existing toolbars, or add a new toolbar
// if the custom one does not exist
// This is a little awkward in that tinymce toolbar config does not
// include a key to identify the individual toolbars, just a name
// which is translated. The custom toolbar's name must be translated
// in order to be merged correctly.
function mergeToolbar(standard, custom) {
if (!custom) return standard
// merge given toolbar data into the default toolbar
Expand Down
7 changes: 4 additions & 3 deletions packages/canvas-rce/src/rce/StatusBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,9 @@ import ReactDOM from 'react-dom'
import {arrayOf, bool, func, number, oneOf, string} from 'prop-types'
import {StyleSheet, css} from 'aphrodite'
import keycode from 'keycode'
import {Button, CondensedButton, IconButton} from '@instructure/ui-buttons'
import {CondensedButton, IconButton} from '@instructure/ui-buttons'
import {Flex} from '@instructure/ui-flex'
import {View} from '@instructure/ui-view'
import {ScreenReaderContent} from '@instructure/ui-a11y-content'

import {Text} from '@instructure/ui-text'
import {SVGIcon} from '@instructure/ui-svg-images'
Expand Down Expand Up @@ -112,9 +111,11 @@ export default function StatusBar(props) {
}
// adding a delay before including the HTML Editor description to wait the focus moves to the RCE
// and prevent JAWS from reading the aria-describedby element when switching back to RCE view
setTimeout(() => {
const timerid = setTimeout(() => {
setIncludeEdtrDesc(props.use_rce_pretty_html_editor && !isHtmlView())
}, 100)

return () => clearTimeout(timerid)
}, [props.editorView]) // eslint-disable-line react-hooks/exhaustive-deps

function preferredHtmlEditor() {
Expand Down
7 changes: 4 additions & 3 deletions packages/canvas-rce/src/rce/__mocks__/tinymceReact.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@
import React, {useEffect, useRef} from 'react'

class FakeEditor {
constructor(textareaId) {
constructor(props) {
this.props = props
this.hidden = true
this._textareaId = textareaId
this._textareaId = props.id
this.readonly = undefined
this._eventHandlers = {}
}
Expand Down Expand Up @@ -94,7 +95,7 @@ class FakeEditor {
export function Editor(props) {
const editorRef = useRef(null)
const textareaRef = useRef(null)
const tinymceEditor = useRef(new FakeEditor(props.id))
const tinymceEditor = useRef(new FakeEditor(props))

useEffect(() => {
window.tinymce.editors[0] = tinymceEditor.current
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2018 - present Instructure, Inc.
* Copyright (C) 2021 - present Instructure, Inc.
*
* This file is part of Canvas.
*
Expand All @@ -18,10 +18,10 @@

import React, {createRef} from 'react'
import {render, waitFor} from '@testing-library/react'
import CanvasRce from '../CanvasRce'
import RCE from '../RCE'
import bridge from '../../bridge'

describe('CanvasRce', () => {
describe('RCE', () => {
let target

beforeEach(() => {
Expand All @@ -38,13 +38,13 @@ describe('CanvasRce', () => {
})

it('bridges newly rendered editors', async () => {
render(<CanvasRce textareaId="textarea3" />, target)
render(<RCE textareaId="textarea3" />, target)
await waitFor(() => expect(bridge.activeEditor().constructor.displayName).toEqual('RCEWrapper'))
})

it('supports getCode() and setCode() on its ref', async () => {
const rceRef = createRef(null)
render(<CanvasRce ref={rceRef} textareaId="textarea3" defaultContent="Hello RCE!" />, target)
render(<RCE ref={rceRef} textareaId="textarea3" defaultContent="Hello RCE!" />, target)

await waitFor(() => expect(rceRef.current).not.toBeNull())

Expand Down
Loading

0 comments on commit d7ff213

Please sign in to comment.