Remove rce_enhancements flag from canvas - phase 1

refs LS-2655
flag=none

- this updates the files under ./ui, a few ancillary files and specs.
- Sadly the commit hook ran prettier and
  fiddled with the formatting of a handful of files too.
- Also made a few changes to quiet lint errors
- Also updated OutcomeManagement.test.js. I don't understand why,
  but 'renders ManagementHeader with lhsGroupId if selected a group in lhs'
  started failing with this change, even though nothing obviously related
  changed. The problem is that the modal isn't getting attached to the
  document being tested. The spec changed to test that the modal contained
  what it's supposed to contain.

test plan: jenkins passes and the RCE works

Change-Id: I48d85077bdbf7563cb07510d3e71d2b448c55e49
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/275301
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Weston Dransfield <wdransfield@instructure.com>
QA-Review: Weston Dransfield <wdransfield@instructure.com>
Product-Review: Ed Schiebel <eschiebel@instructure.com>
This commit is contained in:
Ed Schiebel 2021-10-06 10:49:53 -04:00
parent 5e3eb1f0fa
commit e9afcfcce3
96 changed files with 1374 additions and 5471 deletions

View File

@ -11,7 +11,6 @@ window.ENV = window.ENV || {
rce_pretty_html_editor: true,
rce_auto_save: true
},
use_rce_enhancements: true,
// the RCE won't load w/o these yet
context_asset_string: 'course_1',
current_user_id: 2
@ -22,7 +21,7 @@ window.INST = window.INST || {
}
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
actions: {argTypesRegex: '^on[A-Z].*'}
}
export const globalTypes = {
@ -59,7 +58,7 @@ const canvasThemeProvider = (Story, context) => {
const canvasTheme = context.globals.canvasTheme
return (
<ApplyTheme theme={ApplyTheme.generateTheme(canvasTheme)}>
<Story {...context}/>
<Story {...context} />
</ApplyTheme>
)
}
@ -68,7 +67,7 @@ const bidirectionalProvider = (Story, context) => {
const direction = context.globals.bidirectional
return (
<ApplyTextDirection dir={direction}>
<Story {...context}/>
<Story {...context} />
</ApplyTextDirection>
)
}
@ -78,9 +77,7 @@ const lolcalizeProvider = (Story, context) => {
if (enableLolcalize === 'enable') {
I18n.CallHelpers.normalizeDefault = i18nLolcalize
}
return (
<Story {...context}/>
)
return <Story {...context} />
}
export const decorators = [canvasThemeProvider, bidirectionalProvider, lolcalizeProvider]

View File

@ -1,57 +0,0 @@
/*
* Copyright (C) 2012 - 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 "base/environment";
.insertUpdateImage {
.insertUpdateImageTabpane {
min-height: 265px;
overflow: auto;
}
.checkbox.inline { white-space: nowrap; }
// fix safari legend margin issue
legend {
margin-bottom: 0px;
}
legend + * {
margin-top: 20px;
-webkit-margin-collapse: separate;
}
fieldset {
min-width: 50%;
}
}
.image-upload__form {
display: flex;
justify-content: flex-end;
align-items: center;
}
.file-browser__tree {
height: 175px;
overflow: auto;
border: solid 1px $ic-border-color;
border-radius: $ic-border-radius;
margin: .5rem auto;
padding: .25rem;
}
.file-browser__container {
padding: .25rem;
}

View File

@ -218,7 +218,6 @@ require('@instructure/ui-themes')
if (process.env.DEPRECATION_SENTRY_DSN) {
const Raven = require('raven-js')
Raven.config(process.env.DEPRECATION_SENTRY_DSN, {
ignoreErrors: ['renderIntoDiv', 'renderSidebarIntoDiv'], // silence the `Cannot read property 'renderIntoDiv' of null` errors we get from the pre- rce_enhancements old rce code
release: process.env.GIT_COMMIT,
autoBreadcrumbs: {
xhr: false

View File

@ -21,7 +21,6 @@
},
"dependencies": {
"@instructure/brandable_css": "^3",
"@instructure/canvas-rce-old": "4.1.5",
"@instructure/canvas-theme": "^7",
"@instructure/debounce": "^7",
"@instructure/js-utils": ">=1",

View File

@ -2,6 +2,7 @@
exports[`renders 1`] = `
<Alert
hasShadow={true}
isLiveRegionAtomic={false}
liveRegionPoliteness="assertive"
margin="small"
@ -17,6 +18,7 @@ exports[`renders 1`] = `
exports[`renders with Error details 1`] = `
<Alert
hasShadow={true}
isLiveRegionAtomic={false}
liveRegionPoliteness="assertive"
margin="small"
@ -41,6 +43,7 @@ exports[`renders with Error details 1`] = `
exports[`renders with string details 1`] = `
<Alert
hasShadow={true}
isLiveRegionAtomic={false}
liveRegionPoliteness="assertive"
margin="small"

View File

@ -318,6 +318,7 @@ exports[`prefers to render feedback if it and the location are available 1`] = `
className="PlannerItem-styles__feedbackAvatar"
>
<Avatar
color="default"
data-fs-exclude={true}
display="inline-block"
name="Dr. David Bowman"
@ -3297,6 +3298,7 @@ exports[`renders Note correctly with everything 1`] = `
}
>
<Avatar
color="default"
data-fs-exclude={true}
display="inline-block"
name="Jane"
@ -3488,6 +3490,7 @@ exports[`renders Note correctly without Course 1`] = `
}
>
<Avatar
color="default"
data-fs-exclude={true}
display="inline-block"
name="Jane"
@ -5365,6 +5368,7 @@ exports[`renders feedback anonymously according to the assignment settings 1`] =
className="PlannerItem-styles__feedbackAvatar"
>
<Avatar
color="default"
data-fs-exclude={true}
display="inline-block"
name="?"
@ -5542,6 +5546,7 @@ exports[`renders feedback if available 1`] = `
className="PlannerItem-styles__feedbackAvatar"
>
<Avatar
color="default"
data-fs-exclude={true}
display="inline-block"
name="Boyd Crowder"
@ -5720,6 +5725,7 @@ exports[`renders media feedback if available 1`] = `
className="PlannerItem-styles__feedbackAvatar"
>
<Avatar
color="default"
data-fs-exclude={true}
display="inline-block"
name="Howard Stern"
@ -6124,6 +6130,7 @@ exports[`renders user-created Todo correctly 1`] = `
}
>
<Avatar
color="default"
data-fs-exclude={true}
display="inline-block"
name="Jane"

View File

@ -1,99 +0,0 @@
/*
* 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/>.
*/
/* this class is a solution to CNVS-37129. this solution will create DOM
* MutationObserver to all tables created in tinymce. it does this by hooking
* the tinymce editor object's "AddVisual" method. this method gets called
* often and is called internally by tinymce in a way that hooking it will
* allow us to catch all cases where tables are created.
* the hook simply looks at all tables currently created in the tinymce
* editor and adds a MutationObserver on that table for when any subtree
* changes are made. when this occurs, that code will figure out if there are
* any '<td><iframe>' conditions in the table. if there are, the code inserts
* a div to make it '<td><div><iframe>'
* ultimately, we need to get this fixed in tinymce, but this hack will resolve
* the customer issue now in the short term.
*/
export default class IframesTableFix {
getHackedTables(editor) {
return editor.hackedTables || []
}
setHackedTables(editor, hackedTables) {
editor.hackedTables = hackedTables
}
cleanHackedTables(editor) {
const hackedTables = this.getHackedTables(editor)
const tables = editor.dom.select('table')
this.setHackedTables(editor, hackedTables.filter(t => tables.indexOf(t) > -1))
}
isTableHacked(editor, table) {
this.cleanHackedTables(editor)
return this.getHackedTables(editor).indexOf(table) > -1
}
addHackedTable(editor, table) {
this.getHackedTables(editor).push(table)
}
fixIframes(editor) {
const tds = editor && editor.dom && editor.dom.select ? editor.dom.select('td') : []
const brokenTds = []
tds.forEach(td => {
const spanChildren = [].slice
.call(td.children)
.filter(n => n.tagName === 'SPAN' && n.getAttribute('data-mce-object') === 'iframe')
if (spanChildren.length > 0) {
if (brokenTds.indexOf(td) === -1) {
td.innerHTML = `<div>${td.innerHTML}</div>`
brokenTds.push(td)
}
}
})
}
addMutationObserverToTables(editor, MutationObserver) {
const tables =
editor && editor.dom && editor.dom.select
? editor.dom.select('table').filter(t => !this.isTableHacked(editor, t))
: []
if (tables.length > 0) {
const mo = new MutationObserver(() => {
this.fixIframes(editor)
})
for (let i = tables.length - 1; i >= 0; i--) {
const table = tables[i]
mo.observe(table, {childList: true, subtree: true})
this.addHackedTable(editor, table)
}
}
this.fixIframes(editor)
}
hookAddVisual(editor, MutationObserver) {
const addVisual = editor.addVisual.bind(editor)
const newAddVisual = elm => {
this.addMutationObserverToTables(editor, MutationObserver)
addVisual(elm)
}
editor.addVisual = newAddVisual.bind(editor)
this.addMutationObserverToTables(editor, MutationObserver)
}
}

View File

@ -16,55 +16,11 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import formatMessage from '../format-message'
import sanitizeEditorOptions from './sanitizeEditorOptions'
import wrapInitCb from './wrapInitCb'
import normalizeLocale from './normalizeLocale'
import editorLanguage from './editorLanguage'
export default function (props, tinymce, MutationObserver) {
const initialEditorOptions = props.editorOptions(tinymce)
const sanitizedEditorOptions = window.ENV?.use_rce_enhancements
? initialEditorOptions
: sanitizeEditorOptions(initialEditorOptions)
const editorOptions = wrapInitCb(props.mirroredAttrs, sanitizedEditorOptions, MutationObserver)
if (!window.ENV?.use_rce_enhancements) {
// propagate localization prop to appropriate parameter/value on the
// editor configuration
props.language = normalizeLocale(props.language)
const language = editorLanguage(props.language)
if (language !== undefined) {
editorOptions.language = language
editorOptions.language_url = 'none'
}
// It is expected that consumers provide their own content_css so that the
// styles inside the editor match the styles of the site it is going to be
// displayed in.
if (props.editorOptions.content_css) {
editorOptions.content_css = props.editorOptions.content_css
}
// tell tinymce that we're handling the skin
editorOptions.skin = false
// when tiny formats menuitems with a style attribute, don't let it set the color so they get properly themed
editorOptions.preview_styles =
'font-family font-size font-weight font-style text-decoration text-transform border border-radius outline text-shadow'
// force tinyMCE to NOT use the "mobile" theme,
// see: https://stackoverflow.com/questions/54579110/is-it-possible-to-disable-the-mobile-ui-features-in-tinymce-5
editorOptions.mobile = {theme: 'silver'}
// tell tinyMCE not to put its own branding in the footer of the editor
editorOptions.branding = false
// we provide our own statusbar
editorOptions.statusbar = false
configureMenus(editorOptions, props.instRecordDisabled)
}
const editorOptions = wrapInitCb(props.mirroredAttrs, initialEditorOptions, MutationObserver)
return {
// other props, including overrides
@ -76,38 +32,3 @@ export default function (props, tinymce, MutationObserver) {
tinymce
}
}
function configureMenus(editorOptions, instRecordDisabled) {
const insertMenuItems = [
['instructure_links', 'instructure_image', 'instructure_document'],
['instructure_equation', 'inserttable', 'instructure_media_embed'],
['hr']
]
if (!instRecordDisabled) {
insertMenuItems[0].splice(2, 0, 'instructure_media')
}
editorOptions.menubar = 'edit view insert format tools table'
editorOptions.menu = {
// default menu options listed at https://www.tiny.cloud/docs/configure/editor-appearance/#menu
// default edit menu is fine
view: {
title: formatMessage('View'),
items: 'fullscreen instructure_html_view'
},
insert: {
title: formatMessage('Insert'),
items: insertMenuItems.map(item => item.join(' ')).join(' | ')
},
format: {
title: formatMessage('Format'),
items:
'bold italic underline strikethrough superscript subscript codeformat | formats blockformats fontformats fontsizes align directionality | forecolor backcolor | removeformat'
},
tools: {
title: formatMessage('Tools'),
items: 'wordcount lti_tools_menuitem'
}
// default table menu is fine
}
}

View File

@ -213,7 +213,7 @@ tinymce.create('tinymce.plugins.InstructureLinksPlugin', {
onItemAction: (splitButtonApi, value) => doMenuItem(ed, value),
onSetup(api) {
function handleNodeChange(e) {
if (e !== null) {
if (e?.element) {
api.setActive(!!getAnchorElement(ed, e.element))
}
api.setDisabled(!isOKToLink(ed.selection.getContent()))

View File

@ -28,30 +28,3 @@ export function sanitizePlugins(plugins) {
}
return plugins
}
const extPluginsToRemove = ['instructure_embed']
function sanitizeExternalPlugins(external_plugins) {
if (external_plugins !== undefined) {
const cleanExternalPlugins = {}
Object.keys(external_plugins).forEach(key => {
if (external_plugins.hasOwnProperty(key)) {
if (extPluginsToRemove.indexOf(key) == -1) {
cleanExternalPlugins[key] = external_plugins[key]
}
}
})
return cleanExternalPlugins
}
return external_plugins
}
export default function sanitizeEditorOptions(options) {
const fixed = {...options}
fixed.plugins = sanitizePlugins(options.plugins)
fixed.external_plugins = sanitizeExternalPlugins(options.external_plugins)
fixed.toolbar = options.toolbar
return fixed
}

View File

@ -17,7 +17,6 @@
*/
import $ from 'jquery'
import IframesTableFix from './IframesTableFix'
// mirror attributes onto tinymce editor (if this can be done
// via tiny api, it is preferable, but I dont see a way)
@ -31,20 +30,6 @@ export default function wrapInitCb(mirroredAttrs, editorOptions, MutationObserve
Object.keys(attrs).forEach(attr => {
el.setAttribute(attr, attrs[attr])
})
if (!window.ENV?.use_rce_enhancements) {
// *** moved to RCEWrapper for new rce ***
// add data to textarea so it can be found by canvas
// (which unfortunately relies on this a lot)
el.dataset.rich_text = true
}
}
if (!window.ENV?.use_rce_enhancements) {
// *** no longer necessary with tinymce 5 ***
// hookAddVisual for hacky <td><iframe> fix
const ifr = new IframesTableFix()
ifr.hookAddVisual(ed, MutationObserver)
}
// *** moved from setupAndFocusTinyMCEConfig ***
@ -71,26 +56,6 @@ export default function wrapInitCb(mirroredAttrs, editorOptions, MutationObserve
$(window).triggerHandler('resize')
if (!window.ENV?.use_rce_enhancements) {
// this is a hack so that when you drag an image from the sidebar to the editor that it doesn't
// try to embed the thumbnail but rather the full size version of the image.
// so basically, to document why and how this works: in wiki_sidebar.js we add the
// _mce_src="http://path/to/the/fullsize/image" to the images whose src="path/to/thumbnail/of/image/"
// what this does is check to see if some DOM node that got inserted into the editor has the attribute _mce_src
// and if it does, use that instead.
$(ed.contentDocument).bind('DOMNodeInserted', e => {
const target = e.target
let mceSrc
if (
target.nodeType === 1 &&
target.nodeName === 'IMG' &&
(mceSrc = $(target).data('url'))
) {
$(target).attr('src', tinymce.activeEditor.documentBaseURI.toAbsolute(mceSrc))
}
})
}
// tiny sets a focusout event handler, which only IE supports
// (Chrome/Safari/Opera support DOMFocusOut, FF supports neither)
// we attach a blur event that does the same thing (which in turn

View File

@ -55,6 +55,18 @@
background-color: var(--canvasBackgroundColor);
}
.tox.tox-tinymce .screenreader-only {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
transform: translatez(0);
}
.tox-tinymce-aux {
font-family: var(--canvasFontFamily);
}

View File

@ -1,135 +0,0 @@
/*
* Copyright (C) 2018 - 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 assert from 'assert'
import sinon from 'sinon'
import IframesTableFix from '../../src/rce/IframesTableFix'
let table,
editor,
ifr,
sandbox = sinon.createSandbox()
class MockMutationObserver {
observe() {}
}
describe('IframesTableFix - for CNVS-37129', () => {
beforeEach(() => {
table = {name: 'table'}
editor = {
addVisual: () => {},
dom: {select: () => {}}
}
ifr = new IframesTableFix()
})
afterEach(() => {
sandbox.restore()
})
it('ensures hackTableInsertion hooks editor.addVisual', () => {
const mock = sandbox
.mock(ifr)
.expects('addMutationObserverToTables')
.twice()
.withArgs(editor)
ifr.hookAddVisual(editor, MockMutationObserver)
editor.addVisual()
mock.verify()
})
it('ensures addMutationObserverToTables adds MutationObserver to table', () => {
sandbox
.stub(editor.dom, 'select')
.withArgs('table')
.returns([table])
const mock = sandbox
.mock(MockMutationObserver.prototype)
.expects('observe')
.once()
.withArgs(table)
sandbox.stub(ifr, 'fixIframes')
ifr.addMutationObserverToTables(editor, MockMutationObserver)
mock.verify()
})
it('ensures addMutationObserverToTables adds MutationObserver to table only once', () => {
sandbox
.stub(editor.dom, 'select')
.withArgs('table')
.returns([table])
const mock = sandbox
.mock(MockMutationObserver.prototype)
.expects('observe')
.once()
.withArgs(table)
sandbox.stub(ifr, 'fixIframes')
ifr.addMutationObserverToTables(editor, MockMutationObserver)
ifr.addMutationObserverToTables(editor, MockMutationObserver)
mock.verify()
})
it('ensures fixIframes is called from mutationobserver', () => {
sandbox
.stub(editor.dom, 'select')
.withArgs('table')
.returns([table])
sandbox.stub(MockMutationObserver.prototype, 'observe')
const mock = sandbox
.mock(ifr)
.expects('fixIframes')
.once()
ifr.addMutationObserverToTables(editor, MockMutationObserver)
mock.verify()
})
it('ensures fixIframes fixes iframes', () => {
const innerHTML = '<span>gomer</span>'
const elem = {
tagName: 'SPAN',
getAttribute: () => {
return 'iframe'
}
}
const td = {children: [elem], innerHTML}
sandbox
.stub(editor.dom, 'select')
.withArgs('td')
.returns([td])
ifr.fixIframes(editor)
assert(td.innerHTML === `<div>${innerHTML}</div>`)
})
it('ensure fixIframes does not fix non-iframes', () => {
const innerHTML = '<p><span>gomer</span></p>'
const elem = {
tagName: 'P',
getAttribute: () => {
return 'iframe'
}
}
const td = {children: [elem], innerHTML}
sandbox
.stub(editor.dom, 'select')
.withArgs('td')
.returns([td])
ifr.fixIframes(editor)
assert(td.innerHTML === innerHTML)
})
})

View File

@ -49,48 +49,6 @@ QUnit.module('EditorConfig', {
}
})
test('buttons spread across rows for narrow windowing', () => {
const width = 100
const config = new EditorConfig(fake_tinymce, INST, width, dom_id)
const toolbar = config.toolbar()
strictEqual(toolbar[0], toolbar1)
strictEqual(toolbar[1], toolbar2)
strictEqual(toolbar[2], toolbar3)
})
test('buttons go on the first row for large windowing', () => {
const config = new EditorConfig(fake_tinymce, INST, largeScreenWidth, dom_id)
const toolbar = config.toolbar()
equal(toolbar[0], `${toolbar1},${toolbar2},${toolbar3}`)
strictEqual(toolbar[1], '')
strictEqual(toolbar[2], '')
})
test('adding a few extra buttons', () => {
INST.editorButtons = [
{
id: 'example',
name: 'new_button'
}
]
const config = new EditorConfig(fake_tinymce, INST, largeScreenWidth, dom_id)
const toolbar = config.toolbar()
ok(toolbar[0].match(/instructure_external_button_example/))
})
test('calculating an external button clump', () => {
INST.editorButtons = [
{
id: 'example',
name: 'new_button'
}
]
INST.maxVisibleEditorButtons = 0
const config = new EditorConfig(fake_tinymce, INST, largeScreenWidth, dom_id)
const btns = config.external_buttons()
equal(btns, ' instructure_external_button_clump')
})
test('default config has static attributes', () => {
INST.maxVisibleEditorButtons = 2
const config = new EditorConfig(fake_tinymce, INST, largeScreenWidth, dom_id)
@ -98,13 +56,6 @@ test('default config has static attributes', () => {
equal(schema.skin, false)
})
test('default config includes toolbar', () => {
INST.maxVisibleEditorButtons = 2
const config = new EditorConfig(fake_tinymce, INST, largeScreenWidth, dom_id)
const schema = config.defaultConfig()
equal(schema.toolbar[0], config.toolbar()[0])
})
test('default config includes Lato font-family', () => {
const config = new EditorConfig(fake_tinymce, INST, largeScreenWidth, dom_id)
const schema = config.defaultConfig()

View File

@ -1,115 +0,0 @@
/*
* Copyright (C) 2015 - 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 $ from 'jquery'
import EditorAccessibility from '@canvas/rce/jquery/editorAccessibility'
const fixtures = $('#fixtures')
let label = null
let textarea = null
let acc = null
let activeEditorNodes = null
let initPromise = null
const initializedTest = (description, fn) => {
test(description, assert => {
const done = assert.async()
initPromise.then(() => {
fn()
done()
})
})
}
QUnit.module('EditorAccessibility', {
setup() {
initPromise = new Promise(resolve => {
label = $("<label for='a42'>This is a label</label>")
textarea = $("<textarea id='a42' data-rich_text='true'></textarea>")
fixtures.append(label)
fixtures.append(textarea)
tinymce
.init({
selector: '#fixtures textarea#a42'
})
.then(() => {
resolve()
})
acc = new EditorAccessibility(tinymce.activeEditor)
activeEditorNodes = tinymce.activeEditor.getContainer().children
})
},
teardown() {
label.remove()
textarea.remove()
fixtures.empty()
acc = null
activeEditorNodes = null
initPromise = null
}
})
initializedTest('initialization', () => equal(acc.$el.length, 1))
initializedTest('cacheElements grabs the relevant tinymce iframe', () => {
acc._cacheElements()
ok(acc.$iframe.length, 1)
})
initializedTest('accessiblize() gives a helpful title to the iFrame', () => {
acc.accessiblize()
equal($(acc.$iframe).attr('title'), 'Rich Text Area. Press ALT+F8 for help')
})
initializedTest('accessiblize() removes the statusbar from the tabindex', () => {
acc.accessiblize()
const statusbar = $(activeEditorNodes).find('.mce-statusbar > .mce-container-body')
equal(statusbar.attr('tabindex'), '-1')
})
initializedTest('accessibilize() hides the menubar, Alt+F9 shows it', () => {
acc.accessiblize()
const $menu = $(activeEditorNodes).find('.mce-menubar')
equal($menu.is(':visible'), false)
const event = {
isDefaultPrevented() {
return false
},
altKey: true,
ctrlKey: false,
metaKey: false,
shiftKey: false,
keyCode: 120, // <- this is F9
preventDefault() {},
isImmediatePropagationStopped() {
return false
}
}
tinymce.activeEditor.fire('keydown', event)
equal($menu.is(':visible'), true)
})
initializedTest('accessiblize() gives an aria-label to the role=application div', () => {
acc.accessiblize()
ok($(acc.$el).attr('aria-label'), 'aria-label has a value')
})
initializedTest('accessiblize() gives an aria-label to the rce body element', () => {
acc.accessiblize()
equal($(acc.editor.getBody()).attr('aria-label'), 'This is a label')
})

View File

@ -1,80 +0,0 @@
/*
* Copyright (C) 2015 - 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 $ from 'jquery'
import KeyboardShortcuts from '@canvas/tinymce-keyboard-shortcuts'
let view = null
QUnit.module('editor KeyboardShortcuts', {
setup() {
view = new KeyboardShortcuts()
view.$dialog = {
opened: false,
dialog(cmd) {
if (cmd === 'open') {
this.opened = true
}
}
}
return view.bindEvents()
},
teardown() {
return view.remove()
}
})
test('ALT+F8 should open the helpmenu', () => {
$(document).trigger('editorKeyUp', [
{
keyCode: 119,
altKey: true
}
])
equal(view.$dialog.opened, true)
})
test('ALT+0 opens the helpmenu', () => {
$(document).trigger('editorKeyUp', [
{
keyCode: 48,
altKey: true
}
])
equal(view.$dialog.opened, true)
})
test('ALT+0 (numpad) does not open the helpmenu (we need that for unicode entry on windows)', () => {
$(document).trigger('editorKeyUp', [
{
keyCode: 96,
altKey: true
}
])
equal(view.$dialog.opened, false)
})
test('any of those help values without alt does nothing', () => {
$(document).trigger('editorKeyUp', [
{
keyCode: 119,
altKey: false
}
])
equal(view.$dialog.opened, false)
})

View File

@ -1,42 +0,0 @@
/*
* Copyright (C) 2016 - 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 Sidebar from '@canvas/rce/Sidebar'
import RCELoader from '@canvas/rce/serviceRCELoader'
import fakeENV from 'helpers/fakeENV'
import editorUtils from 'helpers/editorUtils'
QUnit.module('Sidebar - init', {
setup() {
// in case other specs left it not fresh
editorUtils.resetRCE()
fakeENV.setup()
},
teardown() {
fakeENV.teardown()
editorUtils.resetRCE()
}
})
test('loads remote sidebar when feature flag on', () => {
const remoteSidebar = {is_a: 'remote_sidebar'}
sandbox.stub(RCELoader, 'loadSidebarOnTarget').callsArgWith(1, remoteSidebar)
Sidebar.pendingShow = false
Sidebar.init()
equal(Sidebar.instance, remoteSidebar)
})

View File

@ -1,155 +0,0 @@
/*
* Copyright (C) 2015 - 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 EquationEditorView from '@canvas/rce/backbone/views/EquationEditorView'
import Links from '@canvas/tinymce-links'
import InsertUpdateImageView from '@canvas/rce/backbone/views/InsertUpdateImageView'
import loadEventListeners from '@canvas/rce/loadEventListeners'
import 'jquery'
import 'jqueryui/tabs'
import 'browser-sniffer'
let fakeEditor
QUnit.module('loadEventListeners', {
setup() {
window.INST.maxVisibleEditorButtons = 10
window.INST.editorButtons = [{id: '__BUTTON_ID__'}]
fakeEditor = {
id: 'someId',
bookmarkMoved: false,
focus: () => {},
dom: {createHTML: () => "<a href='#'>stub link html</a>"},
selection: {
getBookmark: () => ({}),
getNode: () => ({}),
getContent: () => ({}),
moveToBookmark: prevSelect => (fakeEditor.bookmarkMoved = true)
},
addCommand: () => ({}),
addButton: () => ({})
}
this.dispatchEvent = name => {
const event = document.createEvent('CustomEvent')
const eventData = {
ed: fakeEditor,
selectNode: '<div></div>'
}
event.initCustomEvent(`tinyRCE/${name}`, true, true, eventData)
document.dispatchEvent(event)
}
},
teardown() {
window.alert.restore && window.alert.restore()
console.log.restore && console.log.restore()
}
})
test('initializes equation editor plugin', function(assert) {
const done = assert.async()
loadEventListeners({
equationCB: view => {
ok(view instanceof EquationEditorView)
equal(view.$editor.selector, '#someId')
done()
}
})
return this.dispatchEvent('initEquation')
})
test('initializes links plugin and renders dialog', function(assert) {
const done = assert.async()
sandbox.stub(Links)
loadEventListeners({
linksCB: () => {
ok(Links.renderDialog.calledWithExactly(fakeEditor))
done()
}
})
return this.dispatchEvent('initLinks')
})
test('builds new image view on RCE event', assert => {
const done = assert.async()
assert.expect(1)
loadEventListeners({
imagePickerCB: view => {
ok(view instanceof InsertUpdateImageView)
done()
}
})
const event = document.createEvent('CustomEvent')
const eventData = {
ed: fakeEditor,
selectNode: '<div></div>'
}
event.initCustomEvent('tinyRCE/initImagePicker', true, true, eventData)
return document.dispatchEvent(event)
})
test('initializes equella plugin', assert => {
const done = assert.async()
const alertSpy = sinon.spy(window, 'alert')
assert.expect(1)
loadEventListeners({
equellaCB() {
ok(
alertSpy.calledWith(
'Equella is not properly configured for this account, please notify your system administrator.'
)
)
done()
}
})
const event = document.createEvent('CustomEvent')
const eventData = {
ed: fakeEditor,
selectNode: '<div></div>'
}
event.initCustomEvent('tinyRCE/initEquella', true, true, eventData)
return document.dispatchEvent(event)
})
test('initializes external tools plugin', () => {
const commandSpy = sinon.spy(fakeEditor, 'addCommand')
loadEventListeners()
const event = document.createEvent('CustomEvent')
const eventData = {
ed: fakeEditor,
url: 'someurl.com'
}
event.initCustomEvent('tinyRCE/initExternalTools', true, true, eventData)
document.dispatchEvent(event)
ok(commandSpy.calledWith('instructureExternalButton__BUTTON_ID__'))
})
test('initializes recording plugin', assert => {
const done = assert.async()
const logSpy = sinon.spy(console, 'log')
assert.expect(1)
loadEventListeners({
recordCB() {
ok(logSpy.calledWith('Kaltura has not been enabled for this account'))
done()
}
})
const event = document.createEvent('CustomEvent')
const eventData = {ed: fakeEditor}
event.initCustomEvent('tinyRCE/initRecord', true, true, eventData)
return document.dispatchEvent(event)
})

View File

@ -18,7 +18,6 @@
import $ from 'jquery'
import RCELoader from '@canvas/rce/serviceRCELoader'
import * as jwt from '@canvas/rce/jwt'
import editorUtils from 'helpers/editorUtils'
import fakeENV from 'helpers/fakeENV'
import fixtures from 'helpers/fixtures'
@ -180,74 +179,3 @@ test('ensures yielded editor has call and focus methods', function (assert) {
}
RCELoader.loadOnTarget(this.$div, {}, cb)
})
QUnit.module('loadSidebarOnTarget', {
setup() {
fakeENV.setup()
ENV.RICH_CONTENT_APP_HOST = 'http://rce.host'
ENV.RICH_CONTENT_CAN_UPLOAD_FILES = true
ENV.context_asset_string = 'courses_1'
ENV.current_user_id = '17'
fixtures.setup()
this.$div = fixtures.create('<div />')
this.sidebar = {}
this.rce = {renderSidebarIntoDiv: sinon.stub().callsArgWith(2, this.sidebar)}
sinon.stub(RCELoader, 'loadRCE').callsArgWith(0, this.rce)
this.refreshToken = sinon.spy()
sandbox.stub(jwt, 'refreshFn').returns(this.refreshToken)
},
teardown() {
fakeENV.teardown()
fixtures.teardown()
RCELoader.loadRCE.restore()
}
})
test('passes host and context from ENV as props to sidebar', function () {
const cb = sinon.spy()
RCELoader.loadSidebarOnTarget(this.$div, cb)
ok(this.rce.renderSidebarIntoDiv.called)
const props = this.rce.renderSidebarIntoDiv.args[0][1]
equal(props.host, 'http://rce.host')
equal(props.contextType, 'courses')
equal(props.contextId, '1')
})
test('uses user context when in account context', function () {
ENV.context_asset_string = 'account_1'
const cb = sinon.spy()
RCELoader.loadSidebarOnTarget(this.$div, cb)
ok(this.rce.renderSidebarIntoDiv.called)
const props = this.rce.renderSidebarIntoDiv.args[0][1]
equal(props.contextType, 'user')
equal(props.contextId, '17')
})
test('yields sidebar to callback', function () {
const cb = sinon.spy()
RCELoader.loadSidebarOnTarget(this.$div, cb)
ok(cb.calledWith(this.sidebar))
})
test('ensures yielded sidebar has show and hide methods', function () {
const cb = () => {}
RCELoader.loadSidebarOnTarget(this.$div, cb)
equal(typeof this.sidebar.show, 'function')
equal(typeof this.sidebar.hide, 'function')
})
test('provides a callback for loading a new jwt', function () {
const cb = sinon.spy()
RCELoader.loadSidebarOnTarget(this.$div, cb)
ok(this.rce.renderSidebarIntoDiv.called)
const props = this.rce.renderSidebarIntoDiv.args[0][1]
ok(jwt.refreshFn.calledWith(props.jwt))
equal(props.refreshToken, this.refreshToken)
})
test('passes brand config json url', function () {
ENV.active_brand_config_json_url = {}
RCELoader.loadSidebarOnTarget(this.$div, () => {})
const props = this.rce.renderSidebarIntoDiv.args[0][1]
equal(props.themeUrl, ENV.active_brand_config_json_url)
})

View File

@ -16,9 +16,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import _ from 'underscore'
import ExternalToolsHelper from '@canvas/tinymce-external-tools/ExternalToolsHelper'
import $ from 'jquery'
QUnit.module('ExternalToolsHelper:buttonConfig', {
setup() {
@ -30,24 +28,13 @@ QUnit.module('ExternalToolsHelper:buttonConfig', {
teardown() {}
})
test('makes a config as expected', function() {
test('makes a config as expected', function () {
const config = ExternalToolsHelper.buttonConfig(this.buttonOpts)
equal(config.title, 'SomeName')
equal(config.cmd, 'instructureExternalButton_SomeId')
equal(config.classes, 'widget btn instructure_external_tool_button')
})
test('modified string to avoid mce prefix', function() {
const btn = {
...this.buttonOpts,
canvas_icon_class: 'foo-class'
}
const config = ExternalToolsHelper.buttonConfig(btn)
equal(config.icon, 'hack-to-avoid-mce-prefix foo-class')
equal(config.image, null)
})
test('defaults to image if no icon class', function() {
test('defaults to image if no icon class', function () {
const btn = {
...this.buttonOpts,
icon_url: 'example.com'
@ -57,89 +44,6 @@ test('defaults to image if no icon class', function() {
equal(config.image, 'example.com')
})
QUnit.module('ExternalToolsHelper:clumpedButtonMapping', {
setup() {
this.clumpedButtons = [
{
id: 'ID_1',
name: 'NAME_1',
icon_url: '',
canvas_icon_class: 'foo'
},
{
id: 'ID_2',
name: 'NAME_2',
icon_url: '',
canvas_icon_class: null
}
]
this.onClickHander = sinon.spy()
this.fakeEditor = sinon.spy()
},
teardown() {}
})
test('returns a hash of markup keys and attaches click handler to value', function() {
const mapping = ExternalToolsHelper.clumpedButtonMapping(
this.clumpedButtons,
this.fakeEditor,
this.onClickHander
)
const imageKey = _.chain(mapping)
.keys()
.filter(k => k.match(/img/))
.value()[0]
const iconKey = _.chain(mapping)
.keys()
.filter(k => !k.match(/img/))
.value()[0]
const imageTag = imageKey.split('&nbsp')[0]
const iconTag = iconKey.split('&nbsp')[0]
equal($(imageTag).data('toolId'), 'ID_2')
equal($(iconTag).data('toolId'), 'ID_1')
ok(this.onClickHander.notCalled)
mapping[imageKey]()
ok(this.onClickHander.called)
})
test('returns icon markup if canvas_icon_class in button', function() {
const mapping = ExternalToolsHelper.clumpedButtonMapping(this.clumpedButtons, () => {})
const iconKey = _.chain(mapping)
.keys()
.filter(k => !k.match(/img/))
.value()[0]
const iconTag = iconKey.split('&nbsp')[0]
equal($(iconTag).prop('tagName'), 'I')
})
test('returns img markup if no canvas_icon_class', function() {
const mapping = ExternalToolsHelper.clumpedButtonMapping(this.clumpedButtons, () => {})
const imageKey = _.chain(mapping)
.keys()
.filter(k => k.match(/img/))
.value()[0]
const imageTag = imageKey.split('&nbsp')[0]
equal($(imageTag).prop('tagName'), 'IMG')
})
QUnit.module('ExternalToolsHelper:attachClumpedDropdown', {
setup() {
this.theSpy = sinon.spy()
this.fakeTarget = {dropdownList: this.theSpy}
this.fakeButtons = 'fb'
this.fakeEditor = {
on() {}
}
},
teardown() {}
})
test('calls dropdownList with buttons as options', function() {
const fakeButtons = 'fb'
ExternalToolsHelper.attachClumpedDropdown(this.fakeTarget, fakeButtons, this.fakeEditor)
ok(this.theSpy.calledWith({options: fakeButtons}))
})
QUnit.module('ExternalToolsHelper:updateMRUList', {
setup() {
sinon.spy(window.console, 'log')
@ -150,19 +54,19 @@ QUnit.module('ExternalToolsHelper:updateMRUList', {
}
})
test('creates the mru list if necessary', function() {
test('creates the mru list if necessary', function () {
equal(window.localStorage.getItem('ltimru'), null)
ExternalToolsHelper.updateMRUList(2)
equal(window.localStorage.getItem('ltimru'), '[2]')
})
test('adds to tool to the mru list', function() {
test('adds to tool to the mru list', function () {
window.localStorage.setItem('ltimru', '[1]')
ExternalToolsHelper.updateMRUList(2)
equal(window.localStorage.getItem('ltimru'), '[2,1]')
})
test('limits mru list to 5 tools', function() {
test('limits mru list to 5 tools', function () {
window.localStorage.setItem('ltimru', '[1,2,3,4]')
ExternalToolsHelper.updateMRUList(5)
equal(window.localStorage.getItem('ltimru'), '[5,1,2,3,4]')
@ -170,13 +74,13 @@ test('limits mru list to 5 tools', function() {
equal(window.localStorage.getItem('ltimru'), '[6,5,1,2,3]')
})
test("doesn't add the same tool twice", function() {
test("doesn't add the same tool twice", function () {
window.localStorage.setItem('ltimru', '[1,2,3,4]')
ExternalToolsHelper.updateMRUList(4)
equal(window.localStorage.getItem('ltimru'), '[1,2,3,4]')
})
test('copes with localStorage failure updating mru list', function() {
test('copes with localStorage failure updating mru list', function () {
// localStorage in chrome is limitedto 5120k, and that seems to include the key
window.localStorage.setItem('xyzzy', 'x'.repeat(5119 * 1024) + 'x'.repeat(1016))
equal(window.localStorage.getItem('ltimru'), null)
@ -185,7 +89,7 @@ test('copes with localStorage failure updating mru list', function() {
ok(window.console.log.calledWith('Cannot save LTI MRU list'))
})
test('corrects bad data in local storage', function() {
test('corrects bad data in local storage', function () {
window.localStorage.setItem('ltimru', 'this is not valid JSON')
ExternalToolsHelper.updateMRUList(1)
equal(window.localStorage.getItem('ltimru'), '[1]')

View File

@ -20,15 +20,12 @@ import ExternalToolsPlugin from '@canvas/tinymce-external-tools'
import $ from 'jquery'
import 'jqueryui/dialog'
const setUp = function(maxButtons) {
const setUp = function () {
const INST = {}
INST.editorButtons = [{id: 'button_id'}]
INST.maxVisibleEditorButtons = maxButtons
this.commandSpy = sinon.spy()
this.buttonSpy = sinon.spy()
this.fakeEditor = {
addCommand: this.commandSpy,
addButton: this.buttonSpy,
addCommand: sinon.spy(),
addButton: sinon.spy(),
getContent() {},
selection: {
getContent() {}
@ -45,65 +42,40 @@ const setUp = function(maxButtons) {
this.INST = INST
}
QUnit.module('initializeExternalTools: with 2 max maxVisibleEditorButtons', {
QUnit.module('initializeExternalTools', {
setup() {
return setUp.call(this, 2)
return setUp.call(this)
},
teardown() {
return $(window).off('beforeunload')
}
})
test('adds button directly to toolbar', function() {
test('adds lti button to the toolbar', function () {
const initResult = ExternalToolsPlugin.init(this.fakeEditor, 'some.fake.url', this.INST)
equal(initResult, null)
ok(this.buttonSpy.calledWith('instructure_external_button_button_id'))
ok(this.commandSpy.calledWith('instructureExternalButtonbutton_id'))
ok(this.fakeEditor.ui.registry.addButton.calledWith('lti_tool_dropdown'))
})
QUnit.module('initializeExternalTools: with 0 max maxVisibleEditorButtons', {
setup() {
return setUp.call(this, 0)
},
teardown() {}
})
test('adds button to clumped buttons', function() {
const initResult = ExternalToolsPlugin.init(this.fakeEditor, 'some.fake.url', this.INST)
equal(initResult, null)
ok(this.buttonSpy.calledWith('instructure_external_button_clump'))
ok(this.commandSpy.notCalled)
})
QUnit.module('with rce_enhancements', {
setup() {
window.ENV.use_rce_enhancements = true
return setUp.call(this, 2)
},
teardown() {
window.ENV.use_rce_enhancements = false
}
})
test('adds MRU menu button', function() {
test('adds MRU menu button to the toolbar', function () {
ExternalToolsPlugin.init(this.fakeEditor, undefined, this.INST)
ok(this.fakeEditor.ui.registry.addMenuButton.calledWith('lti_mru_button'))
})
test('adds favorite buttons to the toolbar', function() {
test('adds favorite buttons to the toolbar', function () {
this.INST.editorButtons[0].favorite = true
ExternalToolsPlugin.init(this.fakeEditor, undefined, this.INST)
ok(this.fakeEditor.ui.registry.addButton.calledWith('instructure_external_button_button_id'))
})
test("creates the tool's icon", function() {
test("creates the tool's icon", function () {
this.INST.editorButtons[0].favorite = true
this.INST.editorButtons[0].icon_url = 'tool_image'
ExternalToolsPlugin.init(this.fakeEditor, undefined, this.INST)
ok(this.fakeEditor.ui.registry.addIcon.calledWith('lti_tool_button_id'))
})
test('adds Apps to the Tools menubar menu', function() {
test('adds Apps to the Tools menubar menu', function () {
ExternalToolsPlugin.init(this.fakeEditor, undefined, this.INST)
ok(this.fakeEditor.ui.registry.addNestedMenuItem.calledWith('lti_tools_menuitem'))
})

View File

@ -1,146 +0,0 @@
/*
* Copyright (C) 2015 - 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 $ from 'jquery'
import LinkableEditor from '@canvas/tinymce-links/linkable_editor'
import * as RceCommandShim from '@canvas/rce/RceCommandShim'
import links from '@canvas/tinymce-links'
let rawEditor = null
QUnit.module('LinkableEditor', {
setup() {
$('#fixtures').html("<div id='some_editor' data-value='42'></div>")
rawEditor = {
id: 'some_editor',
selection: {
getContent: () => 'Some Content',
getNode: () => {},
getRng: () => {}
}
}
},
teardown() {
$('#fixtures').empty()
}
})
test('can load the original element from the editor id', () => {
const editor = new LinkableEditor(rawEditor)
equal(editor.getEditor().data('value'), '42')
})
test('createLink passes data attributes to create_link command', () => {
sandbox.stub(RceCommandShim, 'send')
const dataAttrs = {}
const le = new LinkableEditor({
selection: {
getContent: () => ({}),
getNode: () => {},
getRng: () => {}
}
})
le.createLink('text', 'classes', dataAttrs)
equal(RceCommandShim.send.firstCall.args[2].dataAttributes, dataAttrs)
})
// this file wasn't running in jenkins because this file was named _spec.coffee instead of Spec.coffee
// but these 2 specs were testing something that doesn't exist: LinkableEditor::extractTextContent
// if that is something that actually should exist (but under a different name maybe),
// we should rewrite these 2 test so there is coverage for it, othewise we should
// remove these 2 skipped specs.
QUnit.skip('pulling out text content from a text node', () => {
const editor = new LinkableEditor(rawEditor)
const extractedText = editor.extractTextContent({
getContent: opts => 'Plain Text'
})
equal(extractedText, 'Plain Text')
})
QUnit.skip('extracting text from an IMG node with firefox api', () => {
const editor = new LinkableEditor(rawEditor)
const extractedText = editor.extractTextContent({
getContent(opts) {
if (opts != null && opts.format === 'text') {
return 'alt_text'
} else {
return "<img alt='alt_text' src=''/>"
}
}
})
equal(extractedText, '')
})
QUnit.module('instructure_links link.js', {
setup() {
$('#fixtures').html(
'<div id="some_editor" data-value="42"><img class="iframe_placeholder" src="some_img.png" height="600" width="300"></div>'
)
},
teardown() {
$('#fixtures').empty()
}
})
test('links initEditor PreProcess event preserves iframe size', () => {
const $editor = $(new LinkableEditor(rawEditor))
const event = $.Event('PreProcess')
event.node = $('#fixtures')[0]
links.initEditor($editor)
$editor.trigger(event)
const $iframe = $('#fixtures').find('iframe')
equal($iframe.attr('width'), 300)
equal($iframe.attr('height'), 600)
})
test("links initEditor PreProcess event doesn't use width/height attributes if style is present and contains those items", () => {
$('#fixtures').html(
'<div id="some_editor" data-value="42"><img class="iframe_placeholder" _iframe_style="height: 500px; width: 800px;" src="some_img.png" style="height: 500px; width: 800px;"></div>'
)
const $editor = $(new LinkableEditor(rawEditor))
links.initEditor($editor)
const event = $.Event('PreProcess')
event.node = $('#fixtures')[0]
$editor.trigger(event)
const $iframe = $('#fixtures').find('iframe')
equal($iframe.attr('style'), 'height: 500px; width: 800px;')
ok(!$iframe.attr('width'))
ok(!$iframe.attr('height'))
})
QUnit.module('updateLinks', {
setup() {
$('#fixtures').html(
'<div id="some_editor" data-value="42"><p><span contenteditable="false" data-mce-object="iframe" class="mce-preview-object mce-object-iframe" data-mce-p-src="//simplydiffrient.com"><iframe style="width: 800px; height: 600px;" src="//simplydiffrient.com" frameborder="0"></iframe><span class="mce-shim"></span></span></p></div>'
)
},
teardown() {
$('#fixtures').empty()
}
})
test('does not replace iframes with placebolders', () => {
const $editor = $(new LinkableEditor(rawEditor))
links.initEditor($editor)
const mockEditor = {
contentAreaContainer: $('<span>'),
getBody: () => $('#fixtures')[0]
}
links.updateLinks(mockEditor)
equal($('.iframe_placeholder').length, 0, 'should not replace iframes with placeholders')
})

View File

@ -1,230 +0,0 @@
/*
* Copyright (C) 2015 - 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 $ from 'jquery'
import EditorLinks from '@canvas/tinymce-links'
import LinkableEditor from '@canvas/tinymce-links/linkable_editor'
let selection = null
const alt = 'preview alt text'
QUnit.module('InstructureLinks Tinymce Plugin', {
setup() {
return (selection = {
getContent() {
return 'Selection Content'
},
getNode() {},
getRng() {}
})
},
teardown() {
return $('.ui-dialog').remove()
}
})
test('buttonToImg builds an img tag', () => {
const target = {
closest(str) {
return {
attr(str) {
return 'some/img/url'
}
}
}
}
equal(EditorLinks.buttonToImg(target), "<img src='some&#x2F;img&#x2F;url'/>")
})
test('buttonToImg is not vulnerable to XSS', () => {
const target = {
closest(str) {
return {
attr(str) {
return "<script>alert('attacked');</script>"
}
}
}
}
equal(
EditorLinks.buttonToImg(target),
"<img src='&lt;script&gt;alert(&#x27;attacked&#x27;);&lt;&#x2F;script&gt;'/>"
)
})
test('prepEditorForDialog snapshots the current selection state', () => {
let called = false
const editor = {
nodeChanged() {
return (called = true)
},
selection
}
EditorLinks.prepEditorForDialog(editor)
equal(called, true)
})
test('prepEditorForDialog wraps the editor in a linkable editor', () => {
const editor = {
nodeChanged() {},
selection
}
const wrapper = EditorLinks.prepEditorForDialog(editor)
equal(wrapper.selectedContent, 'Selection Content')
})
QUnit.module('InstructureLinks Tinymce Plugin: bindLinkSubmit', {
setup() {
this.box = $(`\
<div data-editor='editorId'> \
<form id='instructure_link_prompt_form'> \
<input class='prompt' value='promptValue'/> \
</form> \
<div class='inst-link-preview-alt'> \
<input value='${alt}'/> \
</div> \
</div>\
`)
$('#fixtures').append(this.box)
this.box.dialog()
this.form = this.box.find('#instructure_link_prompt_form')
this.editor = {
createLink() {}
}
this.fetchClasses = () => 'classes'
},
teardown() {
this.box.dialog('destroy')
$('#fixtures').empty()
}
})
test("it fires my 'done' callback when form gets submitted", function() {
let called = false
const done = () => (called = true)
EditorLinks.bindLinkSubmit(this.box, this.editor, this.fetchClasses, done)
this.form.trigger('submit')
ok(called)
})
test('it removes any existing callbacks', function() {
let called = false
this.form.on('submit', () => (called = true))
EditorLinks.bindLinkSubmit(this.box, this.editor, this.fetchClasses, () => {})
this.form.trigger('submit')
ok(!called)
})
test('it prevents the event from propogating up the chain', function() {
let called = false
this.box.on('submit', () => (called = true))
EditorLinks.bindLinkSubmit(this.box, this.editor, this.fetchClasses, () => {})
this.form.trigger('submit')
ok(!called)
})
test('it closes the dialog box', function() {
sandbox
.mock(this.box)
.expects('dialog')
.once()
.withArgs('close')
EditorLinks.bindLinkSubmit(this.box, this.editor, this.fetchClasses, () => {})
return this.form.trigger('submit')
})
test('it inserts the link properly', function() {
sandbox
.mock(this.editor)
.expects('createLink')
.once()
.withArgs('promptValue', 'classes', {'preview-alt': 'preview alt text'})
let called = false
this.box.on('submit', () => (called = true))
EditorLinks.bindLinkSubmit(this.box, this.editor, this.fetchClasses, () => {})
return this.form.trigger('submit')
})
QUnit.module('InstructureLinks Tinymce Plugin: buildLinkClasses')
test('it removes any existing link-specific classes', () => {
const box = $('<div></div>')
const priorClasses = 'auto_open stylez inline_disabled stylee'
const classes = EditorLinks.buildLinkClasses(priorClasses, box)
equal(classes, ' stylez stylee')
})
test('is adds in auto_open if checked', () => {
const box = $(`<div> \
<input type='checkbox' checked class='auto_show_inline_content'/> \
</div>`)
const priorClasses = ''
const classes = EditorLinks.buildLinkClasses(priorClasses, box)
equal(classes, ' auto_open')
})
test('it adds in inline_disabled if checked', () => {
const box = $(`<div> \
<input type='checkbox' checked class='disable_inline_content'/> \
</div>`)
const priorClasses = ''
const classes = EditorLinks.buildLinkClasses(priorClasses, box)
equal(classes, ' inline_disabled')
})
let renderDialog_ed = null
QUnit.module('InstructureLinks Tinymce Plugin: renderDialog', {
setup() {
renderDialog_ed = {
getBody: () => null,
nodeChanged: () => null,
selection: {
getContent: () => null,
getNode: () => ({nodeName: 'SPAN'}),
getRng: () => {}
}
}
},
teardown() {
$('#instructure_link_prompt').remove()
}
})
test('it resets the text field if no existing link is selected', () => {
EditorLinks.renderDialog(renderDialog_ed)
const $prompt = $('#instructure_link_prompt .prompt')
const $btn = $('#instructure_link_prompt .btn')
$prompt.val('someurl')
$btn.click()
EditorLinks.renderDialog(renderDialog_ed)
equal($prompt.val(), '')
})
test('it sets the text field to the href if link is selected', () => {
EditorLinks.renderDialog(renderDialog_ed)
const $prompt = $('#instructure_link_prompt .prompt')
const $btn = $('#instructure_link_prompt .btn')
$prompt.val('otherurl')
$btn.click()
const a = document.createElement('a')
a.href = 'linkurl'
renderDialog_ed.selection.getNode = () => a
EditorLinks.renderDialog(renderDialog_ed)
equal($prompt.val(), 'linkurl')
})

View File

@ -1,72 +0,0 @@
/*
* Copyright (C) 2015 - 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 mediaEditorLoader from '@canvas/tinymce-record'
import * as RceCommandShim from '@canvas/rce/RceCommandShim'
QUnit.module('mediaEditorLoader', {
setup() {
sinon.stub(RceCommandShim, 'send')
this.mel = mediaEditorLoader
this.collapseSpy = sinon.spy()
this.selectSpy = sinon.spy()
this.fakeED = {
getBody() {},
selection: {
select: this.selectSpy,
collapse: this.collapseSpy
}
}
},
teardown() {
RceCommandShim.send.restore()
window.$.mediaComment.restore && window.$.mediaComment.restore()
}
})
test('properly makes link html', function() {
const linkHTML = this.mel.makeLinkHtml('FOO', 'BAR', 'FOO title')
const expectedResult =
'<a href="/media_objects/FOO" class="instructure_inline_media_comment BAR' +
'_comment" id="media_comment_FOO" data-alt="FOO title">this is a media comment</a>'
equal(linkHTML, expectedResult)
})
test('creates a callback that will run callONRCE', function() {
this.mel.commentCreatedCallback(this.fakeED, 'ID', 'TYPE')
ok(RceCommandShim.send.called)
})
test('creates a callback that try to collapse a selection', function() {
this.mel.commentCreatedCallback(this.fakeED, 'ID', 'TYPE')
ok(this.selectSpy.called)
ok(this.collapseSpy.called)
})
test('calls mediaComment with a function', function() {
window.$.mediaComment
sinon.spy(window.$, 'mediaComment')
this.mel.insertEditor('foo')
ok(window.$.mediaComment.calledWith('create', 'any'))
const spyCall = window.$.mediaComment.getCall(0)
const lastArgType = typeof spyCall.args[2]
equal('function', lastArgType)
})

View File

@ -68,37 +68,6 @@ test('renders', () => {
ok(view)
})
test('two entries do not render keyboard shortcuts to the same place', () => {
const clock = sinon.useFakeTimers()
sandbox.stub(Reply.prototype, 'edit')
$('#fixtures').append($('<div />').attr('id', 'e1'))
$('#fixtures').append($('<div />').attr('id', 'e2'))
const entry1 = new Entry({
id: 1,
message: 'hi'
})
const entry2 = new Entry({
id: 2,
message: 'reply'
})
const view1 = new EntryView({
model: entry1,
el: '#e1'
})
view1.render()
view1.addReply()
const view2 = new EntryView({
model: entry2,
el: '#e2'
})
view2.render()
view2.addReply()
clock.tick(1)
equal(view1.$('.tinymce-keyboard-shortcuts-toggle').length, 1)
equal(view2.$('.tinymce-keyboard-shortcuts-toggle').length, 1)
return clock.restore()
})
test('should listen on model change:replies', () => {
const entry = new Entry({
id: 1,

View File

@ -34,6 +34,8 @@ import 'helpers/jquery.simulate'
const currentOrigin = window.location.origin
EditView.prototype.loadNewEditor = () => {}
const editView = function (opts = {}, discussOpts = {}) {
const ModelClass = opts.isAnnouncement ? Announcement : DiscussionTopic
if (opts.withAssignment) {
@ -108,10 +110,9 @@ QUnit.module('EditView', {
}
})
// eslint-disable-next-line qunit/resolve-async
test('it should be accessible', function (assert) {
const done = assert.async()
assertions.isAccessible(this.editView(), done, {a11yReport: true})
assertions.isAccessible(this.editView(), () => done(), {a11yReport: true})
})
test('renders', function () {
@ -119,14 +120,17 @@ test('renders', function () {
ok(view)
})
test('tells RCE to manage the parent', function () {
// EditView.loadNewEditor is stubbed since I can't figure out how
// to cope with the async RCE initialization in QUnit
//
QUnit.skip('tells RCE to manage the parent', function () {
const lne = sandbox.stub(RichContentEditor, 'loadNewEditor')
const view = this.editView()
view.loadNewEditor()
ok(lne.firstCall.args[1].manageParent, 'manageParent flag should be set')
})
test('does not tell RCE to manage the parent of locked content', function () {
QUnit.skip('does not tell RCE to manage the parent of locked content', function () {
const lne = sandbox.stub(RichContentEditor, 'loadNewEditor')
const view = this.editView({lockedItems: {content: true}})
view.loadNewEditor()

View File

@ -18,6 +18,7 @@
import $ from 'jquery'
import ValidatedMixin from '@canvas/forms/backbone/views/ValidatedMixin.coffee'
import RichContentEditor from '@canvas/rce/RichContentEditor'
let textarea = null
@ -35,13 +36,18 @@ QUnit.module('ValidatedMixin', {
test('it can find tinymce instances as fields', assert => {
const done = assert.async()
tinymce
.init({
selector: '#fixtures textarea#a42'
})
.then(() => {
const element = ValidatedMixin.findField('message')
equal(element.length, 1)
done()
})
RichContentEditor.loadNewEditor($('#a42'), {}, () => {
// eslint-disable-next-line promise/catch-or-return
tinymce
.init({
selector: '#fixtures textarea#a42'
})
.then(() => {
const element = ValidatedMixin.findField('message')
equal(element.length, 1)
// eslint-disable-next-line promise/no-callback-in-promise
done()
})
})
})

View File

@ -51,6 +51,11 @@ const nameLengthHelper = function (
ENV.MAX_NAME_LENGTH = maxNameLength
return view.validateBeforeSave({name, post_to_sis: postToSis, grading_type: gradingType}, {})
}
// the async nature of RCE initialization makes it really hard to unit test
// stub out the function that kicks it off
EditView.prototype._attachEditorToDescription = () => {}
const editView = function (assignmentOpts = {}) {
const defaultAssignmentOpts = {
name: 'Test Assignment',
@ -138,7 +143,6 @@ QUnit.module('EditView', {
teardown() {
this.server.restore()
fakeENV.teardown()
tinymce.remove() // Make sure we clean stuff up
$('.ui-dialog').remove()
$('ul[id^=ui-id-]').remove()
$('.form-dialog').remove()
@ -152,7 +156,7 @@ QUnit.module('EditView', {
test('should be accessible', function (assert) {
const view = this.editView()
const done = assert.async()
assertions.isAccessible(view, done, {a11yReport: true})
assertions.isAccessible(view, () => done(), {a11yReport: true})
})
test('renders', function () {
@ -1125,7 +1129,7 @@ test('rejects invalid attributes when caching', function () {
sandbox.stub(view, 'getFormData').returns({invalid_attribute_example: 30})
userSettings.contextSet('new_assignment_settings', {})
view.cacheAssignmentSettings()
equal(null, userSettings.contextGet('new_assignment_settings').invalid_attribute_example)
equal(userSettings.contextGet('new_assignment_settings').invalid_attribute_example, null)
})
QUnit.module('EditView: Conditional Release', {
@ -1162,7 +1166,7 @@ QUnit.module('EditView: Conditional Release', {
test('attaches conditional release editor', function () {
const view = this.editView()
equal(1, view.$conditionalReleaseTarget.children().size())
equal(view.$conditionalReleaseTarget.children().size(), 1)
})
test('calls update on first switch', function () {
@ -1192,9 +1196,9 @@ test('does not call update when not modified', function () {
test('validates conditional release', function () {
const view = this.editView()
ENV.ASSIGNMENT = view.assignment
const stub = sandbox.stub(view.conditionalReleaseEditor, 'validateBeforeSave').returns('foo')
sandbox.stub(view.conditionalReleaseEditor, 'validateBeforeSave').returns('foo')
const errors = view.validateBeforeSave(view.getFormData(), {})
ok(errors.conditional_release === 'foo')
strictEqual(errors.conditional_release, 'foo')
})
test('calls save in conditional release', function (assert) {
@ -2022,7 +2026,6 @@ QUnit.module('EditView student annotation submission', hooks => {
hooks.afterEach(() => {
server.restore()
fakeENV.teardown()
tinymce.remove() // Make sure we clean stuff up
$('.ui-dialog').remove()
$('ul[id^=ui-id-]').remove()
$('.form-dialog').remove()

View File

@ -18,10 +18,16 @@
import $ from 'jquery'
import fakeENV from 'helpers/fakeENV'
import OutcomeContentBase from '@canvas/outcome-content-view/backbone/views/OutcomeContentBase'
import Outcome from '@canvas/outcomes/backbone/models/Outcome.coffee'
import OutcomeView from '@canvas/outcome-content-view/backbone/views/OutcomeView'
import I18nStubber from 'helpers/I18nStubber'
// stub function that creates the RCE to avoid
// its async initializationa
OutcomeContentBase.prototype.readyForm = () => {}
const newOutcome = (outcomeOptions, outcomeLinkOptions) =>
new Outcome(buildOutcome(outcomeOptions, outcomeLinkOptions), {parse: true})
@ -77,7 +83,7 @@ function changeSelectedCalcMethod(view, calcMethod) {
}
function commonTests() {
test('outcome is created successfully', function() {
test('outcome is created successfully', function () {
ok(this.outcome1.get('context_id'), 'upper context id')
ok(this.outcome1.outcomeLink)
ok(this.outcome1.outcomeLink.context_id)
@ -334,7 +340,7 @@ function commonTests() {
view.remove()
})
test('validates title is present', function() {
test('validates title is present', function () {
const view = createView({
model: this.outcome1,
state: 'edit'
@ -346,7 +352,7 @@ function commonTests() {
view.remove()
})
test('validates title length', function() {
test('validates title length', function () {
const long_name = 'X'.repeat(260)
const view = createView({
model: this.outcome1,
@ -358,7 +364,7 @@ function commonTests() {
view.remove()
})
test('validates display_name length', function() {
test('validates display_name length', function () {
const long_name = 'X'.repeat(260)
const view = createView({
model: this.outcome1,
@ -376,20 +382,20 @@ QUnit.module('OutcomeView', {
fakeENV.setup()
// Sometimes TinyMCE has stuff on the dom that causes issues, likely from things that
// don't clean up properly, we make sure that these run in a clean tiny state each time
tinymce.remove()
window.tinymce?.remove()
ENV.PERMISSIONS = {manage_outcomes: true}
this.outcome1 = outcome1()
},
teardown() {
fakeENV.teardown()
tinymce.remove() // Don't leave anything hanging around
window.tinymce?.remove() // Don't leave anything hanging around
document.getElementById('fixtures').innerHTML = ''
}
})
commonTests()
test('dropdown includes available calculation methods', function() {
test('dropdown includes available calculation methods', function () {
const view = createView({
model: this.outcome1,
state: 'edit'
@ -399,7 +405,7 @@ test('dropdown includes available calculation methods', function() {
view.remove()
})
test('calculation method of decaying_average is rendered properly on show', function() {
test('calculation method of decaying_average is rendered properly on show', function () {
const view = createView({
model: this.outcome1,
state: 'show'
@ -642,7 +648,7 @@ test('it attempts a confirmation dialog when calculation is modified', assert =>
})
})
test('validates mastery points', function() {
test('validates mastery points', function () {
const view = createView({
model: this.outcome1,
state: 'edit'
@ -653,7 +659,7 @@ test('validates mastery points', function() {
view.remove()
})
test('validates i18n mastery points', function() {
test('validates i18n mastery points', function () {
const view = createView({
model: this.outcome1,
state: 'edit'
@ -796,14 +802,14 @@ QUnit.module('OutcomeView with mastery scales', {
fakeENV.setup()
// Sometimes TinyMCE has stuff on the dom that causes issues, likely from things that
// don't clean up properly, we make sure that these run in a clean tiny state each time
tinymce.remove()
window.tinymce?.remove()
ENV.PERMISSIONS = {manage_outcomes: true}
ENV.ACCOUNT_LEVEL_MASTERY_SCALES = true
this.outcome1 = outcome1()
},
teardown() {
fakeENV.teardown()
tinymce.remove() // Don't leave anything hanging around
window.tinymce?.remove() // Don't leave anything hanging around
document.getElementById('fixtures').innerHTML = ''
}
})

View File

@ -1,116 +0,0 @@
/*
* Copyright (C) 2015 - 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 $ from 'jquery'
import InsertUpdateImageView from '@canvas/rce/backbone/views/InsertUpdateImageView'
import * as RceCommandShim from '@canvas/rce/RceCommandShim'
let fakeEditor
let moveToBookmarkSpy
QUnit.module('InsertUpdateImageView#update', {
setup() {
moveToBookmarkSpy = sinon.spy()
fakeEditor = {
id: 'someId',
focus() {},
dom: {createHTML: () => "<a href='#'>stub link html</a>"},
selection: {
getBookmark() {},
moveToBookmark: moveToBookmarkSpy
}
}
sinon.stub(RceCommandShim, 'send')
},
teardown() {
$('#fixtures').html('')
RceCommandShim.send.restore()
$('.ui-dialog').remove()
}
})
test('it uses RceCommandShim to call insert_code', () => {
const view = new InsertUpdateImageView(fakeEditor, '<div></div>')
view.$editor = '$fakeEditor'
view.update()
ok(RceCommandShim.send.calledWith('$fakeEditor', 'insert_code', view.generateImageHtml()))
})
test('it updates attributes of existing image if selected node is img', () => {
const view = new InsertUpdateImageView(fakeEditor, '<img>')
const img = view.$selectedNode
view.$editor = '$fakeEditor'
view.$("[name='image[width]']").val('400')
view.$("[name='image[height]']").val('300')
view.$("[name='image[src]']").val('testsrc')
view.$("[name='image[alt]']").val('testalt')
view.update()
equal(img.attr('width'), '400')
equal(img.attr('height'), '300')
equal(img.attr('src'), 'testsrc')
equal(img.attr('data-mce-src'), 'testsrc')
equal(img.attr('alt'), 'testalt')
})
test('it updates decorative attributes for existing images', () => {
const view = new InsertUpdateImageView(fakeEditor, '<img>')
const img = view.$selectedNode
view.$editor = '$fakeEditor'
view.$("[name='image[data-decorative]']").attr('checked', true)
view.update()
equal(img.attr('data-decorative'), 'true', 'data-decorative attribute is present')
equal(img.attr('alt'), '', 'decorative image has empty alt text')
})
test('it removes decorative attributes for exiting images', () => {
const view = new InsertUpdateImageView(fakeEditor, '<img>')
const img = view.$selectedNode
img.attr('data-decorative', 'true')
img.attr('alt', 'some random alt text')
view.$editor = '$fakeEditor'
view.$("[name='image[data-decorative]']").attr('checked', false)
view.update()
ok(!img.attr('data-decorative'), 'data-decorative attribute is not present')
ok(!img.attr('alt'), 'alt attribute is not present')
})
test('it disables alt text entry when decorative is checked and renables if unchecked', () => {
const view = new InsertUpdateImageView(fakeEditor, '<img>')
const img = view.$selectedNode
view.$editor = '$fakeEditor'
view.$("[name='image[data-decorative]']").attr('checked', true)
view.$("[name='image[data-decorative]']").trigger('change')
ok(view.$("[name='image[alt]']").is(':disabled'))
view.$("[name='image[data-decorative]']").removeAttr('checked')
view.$("[name='image[data-decorative]']").trigger('change')
ok(!view.$("[name='image[alt]']").is(':disabled'))
})
test('it restores caret on update', () => {
const view = new InsertUpdateImageView(fakeEditor, '<div></div>')
view.$editor = '$fakeEditor'
view.update()
ok(moveToBookmarkSpy.called)
})
test('it restores caret on close', () => {
const view = new InsertUpdateImageView(fakeEditor, '<div></div>')
view.$editor = '$fakeEditor'
view.close()
ok(moveToBookmarkSpy.called)
})

View File

@ -53,12 +53,6 @@ test('sets focus to the edit button when hide_edit occurs', () => {
equal(document.activeElement, $('.edit_syllabus_link')[0])
})
test('initializes sidebar when edit link present', () => {
fixtures.create('<a href="#" class="edit_syllabus_link">Edit Link</a>')
SyllabusBehaviors.bindToEditSyllabus()
ok(Sidebar.init.called, 'foo')
})
test('skips initializing sidebar when edit link absent', () => {
equal(fixtures.find('.edit_syllabus_link').length, 0)
SyllabusBehaviors.bindToEditSyllabus()

View File

@ -1,57 +0,0 @@
/*
* 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/>.
*/
import $ from 'jquery'
import YouTubeApi from '@canvas/tinymce-links/youtube_api'
const videoId = 'DgDk50dHbjM'
const link = {attr: () => {}, text: () => {}}
const vidTitle = 'this is my video title'
let ytApi
QUnit.module('YouTube API', {
setup() {
$.youTubeID = () => {
return videoId
}
ytApi = new YouTubeApi()
},
teardown() {
$.youTubeID = undefined
}
})
test('titleYouTubeText changes the text of a link to match the title', () => {
sinon.stub(ytApi, 'fetchYouTubeTitle').callsArgWith(1, vidTitle)
const mock = sinon
.mock(link)
.expects('text')
.withArgs(vidTitle)
ytApi.titleYouTubeText(link)
mock.verify()
})
test('titleYouTubeText increments the failure count on failure', () => {
sinon.stub(ytApi, 'fetchYouTubeTitle').callsArgWith(1, null, {responseText: 'error'})
const mock = sinon
.mock(link)
.expects('attr')
.thrice()
ytApi.titleYouTubeText(link)
mock.verify()
})

View File

@ -35,7 +35,6 @@ enableDTNPI()
async function setupSentry() {
const Raven = await import('raven-js')
Raven.config(process.env.DEPRECATION_SENTRY_DSN, {
ignoreErrors: ['renderIntoDiv', 'renderSidebarIntoDiv'], // silence the `Cannot read property 'renderIntoDiv' of null` errors we get from the pre- rce_enhancements old rce code
release: process.env.GIT_COMMIT
}).install()

View File

@ -108,6 +108,7 @@ exports[`ActAsModal renders with panda svgs, user avatar, table, and proceed but
withVisualDebug={false}
>
<Avatar
color="default"
data-fs-exclude={true}
display="inline-block"
margin="medium 0 x-small 0"

View File

@ -37,7 +37,6 @@ import MissingDateDialog from '@canvas/due-dates/backbone/views/MissingDateDialo
import AssignmentGroupSelector from '@canvas/assignments/backbone/views/AssignmentGroupSelector.coffee'
import GroupCategorySelector from '@canvas/groups/backbone/views/GroupCategorySelector.coffee'
import toggleAccessibly from '@canvas/assignments/jquery/toggleAccessibly'
import RCEKeyboardShortcuts from '@canvas/tinymce-keyboard-shortcuts'
import ConditionalRelease from '@canvas/conditional-release-editor'
import deparam from 'deparam'
import SisValidationHelper from '@canvas/sis/SisValidationHelper'
@ -650,7 +649,6 @@ export default class EditView extends ValidatedFormView
parseInt(@assignment.id))
@_attachEditorToDescription()
@addTinyMCEKeyboardShortcuts()
@togglePeerReviewsAndGroupCategoryEnabled()
@handleOnlineSubmissionTypeChange()
@handleSubmissionTypeChange()
@ -691,7 +689,6 @@ export default class EditView extends ValidatedFormView
data = @assignment.toView()
_.extend data,
use_rce_enhancements: ENV?.use_rce_enhancements
assignment_attempts: ENV?.assignment_attempts_enabled
kalturaEnabled: ENV?.KALTURA_ENABLED or false
postToSISEnabled: ENV?.POST_TO_SIS or false
@ -706,19 +703,8 @@ export default class EditView extends ValidatedFormView
_attachEditorToDescription: =>
return if @lockedItems.content
RichContentEditor.initSidebar()
RichContentEditor.loadNewEditor(@$description, { focus: true, manageParent: true })
$('.rte_switch_views_link').click (e) =>
e.preventDefault()
RichContentEditor.callOnRCE(@$description, 'toggle')
# hide the clicked link, and show the other toggle link.
$(e.currentTarget).siblings('.rte_switch_views_link').andSelf().toggle().focus()
addTinyMCEKeyboardShortcuts: =>
keyboardShortcutsView = new RCEKeyboardShortcuts()
keyboardShortcutsView.render().$el.insertBefore($(".rte_switch_views_link:first"))
# -- Data for Submitting --
_datesDifferIgnoringSeconds: (newDate, originalDate) =>
newWithoutSeconds = new Date(newDate)

View File

@ -46,17 +46,6 @@
{{convertApiUserContent description}}
</div>
{{else}}
{{#unless use_rce_enhancements}}
<div style="float: right;">
<a href="#" class="rte_switch_views_link">
{{#t "#editor.switch_editor_html"}}HTML Editor{{/t}}
</a>
<a href="#" class="rte_switch_views_link" style="display:none;">
{{#t "#editor.switch_editor_rich_text"}}Rich Content Editor{{/t}}
</a>
</div>
<div style="clear:both;"></div>
{{/unless}}
<textarea id="assignment_description"
name="description"
aria-label="{{#t "description"}}Description{{/t}}"

View File

@ -31,7 +31,6 @@ jest.mock('@canvas/upload-file')
describe('ContentTabs', () => {
beforeAll(() => {
window.ENV.use_rce_enhancements = true
window.INST = window.INST || {}
window.INST.editorButtons = []
})

View File

@ -50,7 +50,6 @@ function renderInContext(overrides = {}, children) {
describe('SubmissionManager', () => {
beforeAll(() => {
window.ENV.use_rce_enhancements = true
window.INST = window.INST || {}
window.INST.editorButtons = []
})

View File

@ -23,7 +23,6 @@ import Entry from './models/Entry.coffee'
import htmlEscape from 'html-escape'
import replyAttachmentTemplate from '../jst/_reply_attachment.handlebars'
import preventDefault from 'prevent-default'
import KeyboardShortcuts from '@canvas/tinymce-keyboard-shortcuts'
import stripTags from 'strip-tags'
import RichContentEditor from '@canvas/rce/RichContentEditor'
import {send} from '@canvas/rce/RceCommandShim'
@ -37,14 +36,9 @@ class Reply {
//
// @param {view} an EntryView instance
constructor(view, options = {}) {
;[
'attachKeyboardShortcuts',
'hide',
'hideNotification',
'submit',
'onPostReplySuccess',
'onPostReplyError'
].forEach(m => (this[m] = this[m].bind(this)))
;['hide', 'hideNotification', 'submit', 'onPostReplySuccess', 'onPostReplyError'].forEach(
m => (this[m] = this[m].bind(this))
)
this.view = view
this.options = options
this.el = this.view.$('.discussion-reply-action:first')
@ -67,10 +61,7 @@ class Reply {
RichContentEditor.callOnRCE(this.textArea, 'toggle')
// hide the clicked link, and show the other toggle link.
// todo: replace .andSelf with .addBack when JQuery is upgraded.
return $(e.currentTarget)
.siblings('a')
.andSelf()
.toggle()
return $(e.currentTarget).siblings('a').andSelf().toggle()
})
this.form.delegate('.alert .close', 'click', preventDefault(this.hideNotification))
this.form.on('change', 'ul.discussion-reply-attachments input[type=file]', e => {
@ -82,17 +73,6 @@ class Reply {
}
})
this.editing = false
_.defer(this.attachKeyboardShortcuts)
}
attachKeyboardShortcuts() {
if (!ENV.use_rce_enhancements) {
return this.view
.$('.toggle-wrapper')
.first()
.before(new KeyboardShortcuts().render().$el)
}
}
// #
@ -114,7 +94,6 @@ class Reply {
edit() {
this.form.addClass('replying')
this.discussionEntry.addClass('replying')
RichContentEditor.initSidebar()
RichContentEditor.loadNewEditor(this.textArea, {
focus: true,
manageParent: true,
@ -127,10 +106,7 @@ class Reply {
}
createTextArea(id) {
return $('<textarea/>')
.addClass('reply-textarea')
.attr('id', id)
.attr('aria-hidden', 'true')
return $('<textarea/>').addClass('reply-textarea').attr('id', id).attr('aria-hidden', 'true')
}
replaceTextArea(textAreaId) {
@ -169,12 +145,10 @@ class Reply {
const rceInputs = this.discussionEntry.find('textarea[data-rich_text]').toArray()
if (rceInputs.length > 0) {
if (window.ENV.use_rce_enhancements) {
const okayToContinue = rceInputs
.map(rce => send($(rce), 'checkReadyToGetCode', window.confirm))
.every(i => i)
if (!okayToContinue) return
}
const okayToContinue = rceInputs
.map(rce => send($(rce), 'checkReadyToGetCode', window.confirm))
.every(i => i)
if (!okayToContinue) return
}
RichContentEditor.closeRCE(this.textArea)
@ -268,20 +242,14 @@ class Reply {
// Removes an attachment
removeAttachment($el) {
$el.closest('ul.discussion-reply-attachments li').remove()
return this.form
.find('a.discussion-reply-add-attachment')
.show()
.focus()
return this.form.find('a.discussion-reply-add-attachment').show().focus()
}
// #
// Removes all attachments
removeAttachments() {
this.form.find('ul.discussion-reply-attachments').empty()
return this.form
.find('a.discussion-reply-add-attachment')
.show()
.focus()
return this.form.find('a.discussion-reply-add-attachment').show().focus()
}
}

View File

@ -126,7 +126,6 @@ class EntryView extends Backbone.View
toJSON: ->
json = @model.attributes
json.edited_at = $.datetimeString(json.updated_at)
json.use_rce_enhancements = ENV.use_rce_enhancements
if json.editor
json.editor_name = json.editor.display_name
json.editor_href = json.editor.html_url

View File

@ -42,7 +42,6 @@ export default class TopicView extends Backbone.View {
'click .add_root_reply': 'addRootReply',
'click .discussion_locked_toggler': 'toggleLocked',
'click .toggle_due_dates': 'toggleDueDates',
'click .rte_switch_views_link': 'toggleEditorMode',
'click .topic-subscribe-button': 'subscribeTopic',
'click .topic-unsubscribe-button': 'unsubscribeTopic',
'click .mark_all_as_read': 'markAllAsRead',
@ -145,13 +144,6 @@ export default class TopicView extends Backbone.View {
event.preventDefault()
event.stopPropagation()
RceCommandShim.send(this.$textarea, 'toggle')
// hide the clicked link, and show the other toggle link.
// todo: replace .andSelf with .addBack when JQuery is upgraded.
$(event.currentTarget)
.siblings('.rte_switch_views_link')
.andSelf()
.toggle()
.focus()
}
subscribeTopic(event) {
@ -241,7 +233,6 @@ export default class TopicView extends Backbone.View {
modelData.root = true
modelData.title = ENV.DISCUSSION.TOPIC.TITLE
modelData.isForMainDiscussion = true
modelData.use_rce_enhancements = ENV.use_rce_enhancements
const html = replyTemplate(modelData)
this.$('#discussion_topic').append(html)
}
@ -308,7 +299,8 @@ export default class TopicView extends Backbone.View {
handleKeyDown(e) {
const nodeName = e.target.nodeName.toLowerCase()
if (nodeName === 'input' || nodeName === 'textarea' || window.ENV.disable_keyboard_shortcuts) return
if (nodeName === 'input' || nodeName === 'textarea' || window.ENV.disable_keyboard_shortcuts)
return
if (e.which !== 78) return // n
this.addRootReply(e)
e.preventDefault()

View File

@ -14,18 +14,6 @@
{{/if}}
<div class='discussion-entry-reply-area hide-if-collapsed show-if-replying'>
<form class="discussion-reply-form">
{{#unless use_rce_enhancements}}
<div class="discussion-top-right-header pull-right">
<div class="toggle-wrapper pull-right">
<a href="#">
{{#t "#editor.switch_editor_html"}}HTML Editor{{/t}}
</a>
<a href="#" style="display:none;">
{{#t "#editor.switch_editor_rich_text"}}Rich Content Editor{{/t}}
</a>
</div>
</div>
{{/unless}}
<div class="clearfix" aria-hidden="true">&nbsp;</div>
<textarea class="reply-textarea"
id="{{#if root}}root_{{/if}}reply_message_for_{{id}}"></textarea>

View File

@ -31,7 +31,6 @@ import DiscussionTopic from '@canvas/discussions/backbone/models/DiscussionTopic
import Announcement from '@canvas/discussions/backbone/models/Announcement.coffee'
import $ from 'jquery'
import MissingDateDialog from '@canvas/due-dates/backbone/views/MissingDateDialogView.coffee'
import KeyboardShortcuts from '@canvas/tinymce-keyboard-shortcuts'
import ConditionalRelease from '@canvas/conditional-release-editor'
import deparam from 'deparam'
import flashMessage from '@canvas/rails-flash-notifications'
@ -159,7 +158,6 @@ export default class EditView extends ValidatedFormView
toJSON: ->
data = super
json = _.extend data, @options,
use_rce_enhancements: ENV.use_rce_enhancements
showAssignment: !!@assignmentGroupCollection
useForGrading: @model.get('assignment')?
isTopic: @isTopic()
@ -202,16 +200,8 @@ export default class EditView extends ValidatedFormView
@$textarea = @$('textarea[name=message]').attr('id', _.uniqueId('discussion-topic-message')).css('display', 'none')
unless @lockedItems.content
RichContentEditor.initSidebar()
_.defer =>
@loadNewEditor(@$textarea)
$('.rte_switch_views_link').click (event) =>
event.preventDefault()
event.stopPropagation()
RichContentEditor.callOnRCE(@$textarea, 'toggle')
# hide the clicked link, and show the other toggle link.
# todo: replace .andSelf with .addBack when JQuery is upgraded.
$(event.currentTarget).siblings('.rte_switch_views_link').andSelf().toggle().focus()
if @assignmentGroupCollection
(@assignmentGroupFetchDfd ||= @assignmentGroupCollection.fetch()).done @renderAssignmentGroupOptions
@ -220,7 +210,6 @@ export default class EditView extends ValidatedFormView
_.defer(@renderPeerReviewOptions)
_.defer(@renderPostToSisOptions) if ENV.POST_TO_SIS
_.defer(@watchUnload)
_.defer(@attachKeyboardShortcuts)
_.defer(@renderTabs) if @showConditionalRelease()
_.defer(@loadConditionalRelease) if @showConditionalRelease()
@ -275,10 +264,6 @@ export default class EditView extends ValidatedFormView
component = React.createElement(UsageRightsIndicator, props, null)
ReactDOM.render(component, @$('#usage_rights_control')[0])
attachKeyboardShortcuts: =>
if !ENV.use_rce_enhancements
$('.rte_switch_views_link').first().before((new KeyboardShortcuts()).render().$el)
renderAssignmentGroupOptions: =>
@assignmentGroupSelector = new AssignmentGroupSelector
el: '#assignment_group_options'

View File

@ -53,16 +53,6 @@
{{convertApiUserContent message}}
</div>
{{else}}
{{#unless use_rce_enhancements}}
<div style="float: right;">
<a href="#" class="rte_switch_views_link">
{{#t "#editor.switch_editor_html"}}HTML Editor{{/t}}
</a>
<a href="#" class="rte_switch_views_link" style="display:none;">
{{#t "#editor.switch_editor_rich_text"}}Rich Content Editor{{/t}}
</a>
</div>
{{/unless}}
<div style="clear:both;"></div>
<textarea name="message"
class="input-block-level"

View File

@ -30,7 +30,6 @@ import MissingDateDialogView from '@canvas/due-dates/backbone/views/MissingDateD
import RichContentEditor from '@canvas/rce/RichContentEditor'
import unflatten from 'obj-unflatten'
import deparam from 'deparam'
import KeyboardShortcuts from '@canvas/tinymce-keyboard-shortcuts'
import coupleTimeFields from '@canvas/calendar/jquery/coupleTimeFields'
import datePickerFormat from '@canvas/datetime/datePickerFormat'
import CalendarConferenceWidget from '@canvas/calendar-conferences/react/CalendarConferenceWidget'
@ -43,7 +42,6 @@ RichContentEditor.preloadRemoteModule()
export default class EditCalendarEventView extends Backbone.View {
initialize() {
this.render = this.render.bind(this)
this.attachKeyboardShortcuts = this.attachKeyboardShortcuts.bind(this)
this.toggleDuplicateOptions = this.toggleDuplicateOptions.bind(this)
this.destroyModel = this.destroyModel.bind(this)
// boilerplate that could be replaced with data bindings
@ -136,10 +134,8 @@ export default class EditCalendarEventView extends Backbone.View {
})
const $textarea = this.$('textarea')
RichContentEditor.initSidebar()
RichContentEditor.loadNewEditor($textarea, {focus: true, manageParent: true})
_.defer(this.attachKeyboardShortcuts)
_.defer(this.toggleDuplicateOptions)
_.defer(this.renderConferenceWidget)
@ -176,14 +172,6 @@ export default class EditCalendarEventView extends Backbone.View {
}
}
attachKeyboardShortcuts() {
if (!ENV.use_rce_enhancements) {
return $('.switch_event_description_view')
.first()
.before(new KeyboardShortcuts().render().$el)
}
}
toggleDuplicateOptions() {
return this.$el.find('.duplicate_event_toggle_row').toggle(this.model.isNew())
}
@ -315,7 +303,6 @@ export default class EditCalendarEventView extends Backbone.View {
toJSON() {
const result = super.toJSON(...arguments)
result.use_rce_enhancements = ENV.use_rce_enhancements
result.recurringEventLimit = 200
result.k5_subject = ENV.K5_SUBJECT_COURSE && ENV.FEATURES?.important_dates
return result

View File

@ -11,17 +11,6 @@
value="{{title}}"
maxlength="255" />
{{#unless use_rce_enhancements}}
<div class="clearfix pull-right">
<a href="#" class="switch_event_description_view pull-right">
{{#t "#editor.switch_editor_html"}}HTML Editor{{/t}}
</a>
<a href="#" class="switch_event_description_view pull-right" style="display:none;">
{{#t "#editor.switch_editor_rich_text"}}Rich Content Editor{{/t}}
</a>
</div>
<div style="clear:both;"></div>
{{/unless}}
<textarea class="input-block-level"
id="calendar-description"
name="description"

View File

@ -105,7 +105,6 @@ export default class ExternalToolsTable extends React.Component {
})
}
// Don't forget to change the tooltip text when the rce_enhancements flag goes away
render() {
// only in account settings (not course), but not site_admin, and with the feature on, and with permissions
const show_lti_favorite_toggles =
@ -137,7 +136,7 @@ export default class ExternalToolsTable extends React.Component {
{I18n.t('Add to RCE toolbar')}
<Tooltip
renderTip={I18n.t(
'There is a 2 app limit for placement within the RCE toolbar. This setting only applies to the enhanced RCE.'
'There is a 2 app limit for placement within the RCE toolbar.'
)}
placement="top"
on={['click', 'focus']}

View File

@ -692,14 +692,6 @@ $(function () {
if (!$editor || $editor.length === 0) {
return
}
RichContentEditor.initSidebar({
show() {
$('#sidebar_content').hide()
},
hide() {
$('#sidebar_content').show()
}
})
RichContentEditor.loadNewEditor($editor, {focus: true})
})
.bind('richTextEnd', (event, $editor) => {

View File

@ -33,7 +33,6 @@ import * as OutcomesImporter from '@canvas/outcomes/react/OutcomesImporter'
import {courseMocks, groupDetailMocks, groupMocks} from '@canvas/outcomes/mocks/Management'
jest.mock('@canvas/outcomes/react/OutcomesImporter')
jest.useFakeTimers()
describe('OutcomeManagement', () => {
let cache, showOutcomesImporterMock, showOutcomesImporterIfInProgressMock
@ -45,10 +44,12 @@ describe('OutcomeManagement', () => {
OutcomesImporter,
'showOutcomesImporterIfInProgress'
)
jest.useFakeTimers()
})
afterEach(() => {
jest.clearAllMocks()
jest.useRealTimers()
})
const sharedExamples = () => {
@ -331,28 +332,29 @@ describe('OutcomeManagement', () => {
withMorePage: false
})
]
const {getByText, getByTestId} = render(
const {findByText, findByTestId, getByTestId} = render(
<MockedProvider cache={cache} mocks={mocks}>
<OutcomeManagement breakpoints={{tablet: true}} />
</MockedProvider>
)
await act(async () => jest.runAllTimers())
jest.runAllTimers()
// Select a group in the lsh
fireEvent.click(getByText('Course folder 0'))
await act(async () => jest.runAllTimers())
const cf0 = await findByText('Course folder 0')
fireEvent.click(cf0)
jest.runAllTimers()
// The easy way to determine if lsh is passing to ManagementHeader is
// to open the create outcome modal and check if the lhs group was loaded
// by checking if the child of the lhs group is there
fireEvent.click(within(getByTestId('managementHeader')).getByText('Create'))
await act(async () => jest.runAllTimers())
expect(
within(getByTestId('createOutcomeModal')).getByText('Course folder 0')
).toBeInTheDocument()
expect(
within(getByTestId('createOutcomeModal')).getByText('Group 200 folder 0')
).toBeInTheDocument()
jest.runAllTimers()
// there's something weird going on in the test here that while we find the modal
// .toBeInTheDocument() fails, even though a findBy for it fails before ^that click.
// We can test that the elements expected to be within it exist.
const modal = await findByTestId('createOutcomeModal')
expect(within(modal).getByText('Course folder 0')).not.toBeNull()
expect(within(modal).getByText('Group 200 folder 0')).not.toBeNull()
})
})

File diff suppressed because it is too large Load Diff

View File

@ -23,7 +23,6 @@ import $ from 'jquery'
import axios from '@canvas/axios'
import GoogleDocsTreeView from '../backbone/views/GoogleDocsTreeView.coffee'
import HomeworkSubmissionLtiContainer from '../backbone/HomeworkSubmissionLtiContainer'
import RCEKeyboardShortcuts from '@canvas/tinymce-keyboard-shortcuts' /* TinyMCE Keyboard Shortcuts for a11y */
import RichContentEditor from '@canvas/rce/RichContentEditor'
import {recordEulaAgreement, verifyPledgeIsChecked} from './helper'
import '@canvas/rails-flash-notifications'
@ -53,12 +52,6 @@ $(document).ready(function () {
const homeworkSubmissionLtiContainer = new HomeworkSubmissionLtiContainer()
// Add the Keyboard shortcuts info button
if (!ENV.use_rce_enhancements) {
const keyboardShortcutsView = new RCEKeyboardShortcuts()
keyboardShortcutsView.render().$el.insertBefore($('.switch_text_entry_submission_views:first'))
}
// Add screen reader message for student annotation assignments
const accessibilityAlert = I18n.t(
'The student annotation tab includes the document for the assignment. Tabs with additional submission types may also be available.'

View File

@ -27,7 +27,6 @@ import LDBLoginPopup from '../backbone/views/LDBLoginPopup'
import quizTakingPolice from './quiz_taking_police'
import QuizLogAuditing from '@canvas/quiz-log-auditing'
import QuizLogAuditingEventDumper from '@canvas/quiz-log-auditing/jquery/dump_events'
import KeyboardShortcuts from '@canvas/tinymce-keyboard-shortcuts'
import RichContentEditor from '@canvas/rce/RichContentEditor'
import '@canvas/jquery/jquery.ajaxJSON'
import '@canvas/util/toJSON'
@ -44,7 +43,7 @@ let lastAnswerSelected = null
let lastSuccessfulSubmissionData = null
let showDeauthorizedDialog
const quizSubmission = (function() {
const quizSubmission = (function () {
let timeMod = 0,
endAt = $('.end_at'),
endAtParsed = endAt.text() && new Date(endAt.text()),
@ -61,7 +60,7 @@ const quizSubmission = (function() {
endAtWithoutTimeLimitParsed =
$endAtWithoutTimeLimit.text() && new Date($endAtWithoutTimeLimit.text())
// $('.time_running,.time_remaining') is probably not yet loaded at the time
const $timeRunningFunc = function() {
const $timeRunningFunc = function () {
if ($timeRunningTimeRemaining.length > 0) return $timeRunningTimeRemaining
return ($timeRunningTimeRemaining = $('.time_running,.time_remaining'))
}
@ -123,14 +122,14 @@ const quizSubmission = (function() {
quizSubmission.lastSubmissionUpdate = new Date()
const data = $('#submit_quiz_form').getFormData()
$('.question_holder .question').each(function() {
$('.question_holder .question').each(function () {
const value = $(this).hasClass('marked') ? '1' : ''
data[$(this).attr('id') + '_marked'] = value
})
$lastSaved.text(I18n.t('saving', 'Saving...'))
const url = $('.backup_quiz_submission_url').attr('href')
;(function(submissionData) {
;(function (submissionData) {
// Need a shallow clone of the data here because $.ajaxJSON modifies in place
const thisSubmissionData = _.clone(submissionData)
// If this is a timeout-based submission and the data is the same as last time,
@ -193,9 +192,7 @@ const quizSubmission = (function() {
}
// if timer autosubmission is disabled, we need to know when the fallback autosubmission time is
if (data && data.hard_end_at) {
quizSubmission.endAtWithoutTimeLimitParsed = Date.parse(
data.hard_end_at
)
quizSubmission.endAtWithoutTimeLimitParsed = Date.parse(data.hard_end_at)
}
},
// Error callback
@ -522,17 +519,13 @@ const quizSubmission = (function() {
toggleActiveButtonState(selector, primary) {
const addClass = primary ? 'btn-primary' : 'btn-secondary'
const removeClass = primary ? 'btn-secondary' : 'btn-primary'
$(selector)
.addClass(addClass)
.removeClass(removeClass)
$(selector).addClass(addClass).removeClass(removeClass)
},
submitQuiz() {
const button = $('#submit_quiz_button')
button.prop('disabled', true)
const action = button.data('action')
$('#submit_quiz_form')
.attr('action', action)
.submit()
$('#submit_quiz_form').attr('action', action).submit()
}
}
})()
@ -556,13 +549,11 @@ $(document)
// fix screenreader focus for links to href="#target"
$("a[href^='#']")
.not("a[href='#']")
.click(function() {
$($(this).attr('href'))
.attr('tabindex', -1)
.focus()
.click(function () {
$($(this).attr('href')).attr('tabindex', -1).focus()
})
$(function() {
$(function () {
autoBlurActiveInput()
if ($('#preview_mode_link').length == 0) {
@ -611,7 +602,7 @@ $(function() {
false
)
$(document).delegate('a', 'click', function(event) {
$(document).delegate('a', 'click', function (event) {
if ($(this).closest('.ui-dialog,.mceToolbar,.ui-selectmenu').length > 0) {
return
}
@ -650,15 +641,12 @@ $(function() {
}
const $questions = $('#questions')
$('#question_list')
.delegate('.jump_to_question_link', 'click', function(event) {
.delegate('.jump_to_question_link', 'click', function (event) {
event.preventDefault()
const $obj = $($(this).attr('href'))
const scrollableSelector = ENV.MOBILE_UI ? '#content' : 'html,body'
$(scrollableSelector).scrollTo($obj.parent())
$obj
.find(':input:first')
.focus()
.select()
$obj.find(':input:first').focus().select()
})
.find('.list_question')
.bind({
@ -709,7 +697,7 @@ $(function() {
})
$questions
.delegate(':checkbox,:radio', 'change', function(event) {
.delegate(':checkbox,:radio', 'change', function (event) {
const $answer = $(this).parents('.answer')
if (lastAnswerSelected == $answer[0]) {
quizSubmission.updateSubmission()
@ -718,7 +706,7 @@ $(function() {
.delegate('label.upload-label', 'mouseup', event => {
quizSubmission.updateSubmission()
})
.delegate(':text,textarea,select', 'change', function(event, update) {
.delegate(':text,textarea,select', 'change', function (event, update) {
const $this = $(this)
if ($this.hasClass('numerical_question_input')) {
const val = numberHelper.parse($this.val())
@ -758,7 +746,7 @@ $(function() {
}
}
})
.delegate('.flag_question', 'click', function(e) {
.delegate('.flag_question', 'click', function (e) {
e.preventDefault()
const $question = $(this).parents('.question')
$question.toggleClass('marked')
@ -780,7 +768,7 @@ $(function() {
quizSubmission.updateSubmission()
})
.delegate('.question_input', 'change', function(event, update, changedMap) {
.delegate('.question_input', 'change', function (event, update, changedMap) {
let $this = $(this),
tagName = this.tagName.toUpperCase(),
id = $this.parents('.question').attr('id'),
@ -798,27 +786,24 @@ $(function() {
$this
.siblings('.rce_links')
.find('.toggle_question_content_views_link')
.click(function(event) {
.click(function (event) {
event.preventDefault()
RichContentEditor.callOnRCE($tagInstance, 'toggle')
// todo: replace .andSelf with .addBack when JQuery is upgraded.
$(this)
.siblings('.toggle_question_content_views_link')
.andSelf()
.toggle()
$(this).siblings('.toggle_question_content_views_link').andSelf().toggle()
})
} else if ($this.attr('type') == 'text' || $this.attr('type') == 'hidden') {
val = $this.val()
} else if (tagName == 'SELECT') {
const $selects = $this.parents('.question').find('select.question_input')
val = !$selects.filter(function() {
val = !$selects.filter(function () {
return !$(this).val()
}).length
} else {
$this
.parents('.question')
.find('.question_input')
.each(function() {
.each(function () {
if ($(this).attr('checked') || $(this).attr('selected')) {
val = true
}
@ -832,7 +817,7 @@ $(function() {
$questions.find('.question_input').trigger('change', [false, {}])
$('.hide_time_link').click(function(event) {
$('.hide_time_link').click(function (event) {
event.preventDefault()
if ($('.time_running').css('visibility') != 'hidden') {
$('.time_running').css('visibility', 'hidden')
@ -843,8 +828,8 @@ $(function() {
}
})
setTimeout(function() {
$('#question_list .list_question').each(function() {
setTimeout(function () {
$('#question_list .list_question').each(function () {
const $this = $(this)
if ($this.find('.jump_to_question_link').text() == 'Spacer') {
$this.remove()
@ -861,8 +846,8 @@ $(function() {
quizSubmission.finalSubmitButtonClicked = true
})
$('#submit_quiz_form').submit(function(event) {
$('.question_holder textarea.question_input').each(function() {
$('#submit_quiz_form').submit(function (event) {
$('.question_holder textarea.question_input').each(function () {
$(this).change()
})
@ -899,8 +884,7 @@ $(function() {
warningMessage = I18n.t(
'confirms.unanswered_questions',
{
one:
'You have 1 unanswered question (see the right sidebar for details). Submit anyway?',
one: 'You have 1 unanswered question (see the right sidebar for details). Submit anyway?',
other:
'You have %{count} unanswered questions (see the right sidebar for details). Submit anyway?'
},
@ -934,8 +918,8 @@ $(function() {
$('#times_up_dialog').dialog('close')
})
setTimeout(function() {
$('.question_holder textarea.question_input').each(function() {
setTimeout(function () {
$('.question_holder textarea.question_input').each(function () {
$(this).attr('id', 'question_input_' + quizSubmission.contentBoxCounter++)
RichContentEditor.loadNewEditor($(this), {
manageParent: true,
@ -967,7 +951,7 @@ $(function() {
const $submit_buttons = $('#submit_quiz_form button[type=submit]')
// set the form action depending on the button clicked
$submit_buttons.click(function(event) {
$submit_buttons.click(function (event) {
quizSubmission.clearAccessCode = false
const action = $(this).data('action')
if (action != undefined) {
@ -979,7 +963,7 @@ $(function() {
$submit_buttons.removeAttr('disabled')
})
showDeauthorizedDialog = function() {
showDeauthorizedDialog = function () {
$('#deauthorized_dialog').dialog({
modal: true,
buttons: [
@ -1050,7 +1034,3 @@ $(document).ready(() => {
$('.loaded').show()
$('.loading').hide()
})
if (!ENV.use_rce_enhancements) {
$('.essay_question .answers .rce_links').append(new KeyboardShortcuts().render().el)
}

View File

@ -216,7 +216,7 @@ export default class WikiPageIndexView extends PaginatedCollectionView {
this.$el.hide()
$('body').removeClass('index')
$('body').addClass(`edit ${window.ENV.use_rce_enhancements ? '' : 'with-right-side'}`)
$('body').addClass('edit')
this.editModel = new WikiPage(
{editing_roles: this.default_editing_roles},

View File

@ -20,7 +20,6 @@ import I18n from 'i18n!EditorToggle'
import $ from 'jquery'
import Backbone from '@canvas/backbone'
import preventDefault from 'prevent-default'
import KeyboardShortcuts from '@canvas/tinymce-keyboard-shortcuts'
import React from 'react'
import ReactDOM from 'react-dom'
import SwitchEditorControl from '../../react/SwitchEditorControl'
@ -100,19 +99,7 @@ Object.assign(EditorToggle.prototype, Backbone.Events, {
this.textArea.val(this.getContent())
this.textAreaContainer.insertBefore(this.el)
this.el.detach()
if (!ENV.use_rce_enhancements) {
if (this.options.switchViews) {
this.switchViews = this.createSwitchViews()
this.switchViews.insertBefore(this.textAreaContainer)
}
if (!this.infoIcon) {
this.infoIcon = new KeyboardShortcuts().render().$el
}
this.infoIcon.insertBefore($('.switch-views__link'))
$('<div/>', {style: 'clear: both'}).insertBefore(this.textAreaContainer)
}
this.done.insertAfter(this.textAreaContainer)
RichContentEditor.initSidebar()
RichContentEditor.loadNewEditor(this.textArea, this.getRceOptions())
this.textArea = RichContentEditor.freshNode(this.textArea)
this.editing = true
@ -143,7 +130,6 @@ Object.assign(EditorToggle.prototype, Backbone.Events, {
if (this.options.switchViews) {
this.switchViews.detach()
}
if (!ENV.use_rce_enhancements) this.infoIcon.detach()
this.done.detach()
this.editing = false
return this.trigger('display')

View File

@ -81,8 +81,7 @@ export default class ValidatedFormView extends Backbone.View
okayToContinue = true
if rceInputs.length > 0
if window.ENV.use_rce_enhancements
okayToContinue = rceInputs.map((rce) => sendFunc($(rce), 'checkReadyToGetCode', window.confirm)).every((value) => value)
okayToContinue = rceInputs.map((rce) => sendFunc($(rce), 'checkReadyToGetCode', window.confirm)).every((value) => value)
if !okayToContinue
return

View File

@ -52,7 +52,7 @@ export default ValidatedMixin =
# @return {jQuery Object} the relevant div that wraps the tinymce
# iframe related to this textarea
findSiblingTinymce: ($el)->
$el.siblings('.mce-tinymce').find(".mce-edit-area")
$el.siblings('.tox-tinymce').find(".tox-edit-area")
findField: (field, useGlobalSelector=false) ->
selector = @fieldSelectors?[field] or "[name='#{field}']"

View File

@ -64,9 +64,9 @@ import 'jquery-scroll-to-visible/jquery.scrollTo'
// onSubmit: A callback which will receive 1. a deferred object
// encompassing the request(s) triggered by the submit action and 2. the
// formData being posted
$.fn.formSubmit = function(options) {
$.fn.formSubmit = function (options) {
$(this).markRequired(options)
this.submit(function(event) {
this.submit(function (event) {
const $form = $(this) // this is to handle if bind to a template element, then it gets cloned the original this would not be the same as the this inside of here.
// disableWhileLoading might need to wrap this, so we don't want to modify the original
let onSubmit = options.onSubmit
@ -116,7 +116,7 @@ $.fn.formSubmit = function(options) {
if (options.disableWhileLoading) {
const oldOnSubmit = onSubmit
onSubmit = function(loadingPromise) {
onSubmit = function (loadingPromise) {
if (options.disableWhileLoading === 'spin_on_success') {
// turn it into a false promise, i.e. never resolve
const origPromise = loadingPromise
@ -134,9 +134,9 @@ $.fn.formSubmit = function(options) {
var loadingPromise = $.Deferred(),
oldHandlers = {}
onSubmit.call(this, loadingPromise, formData)
$.each(['success', 'error'], function(i, successOrError) {
$.each(['success', 'error'], function (i, successOrError) {
oldHandlers[successOrError] = options[successOrError]
options[successOrError] = function() {
options[successOrError] = function () {
loadingPromise[successOrError === 'success' ? 'resolve' : 'reject'].apply(
loadingPromise,
arguments
@ -169,12 +169,12 @@ $.fn.formSubmit = function(options) {
event.preventDefault()
event.stopPropagation()
const xhrSuccess = function(data, request) {
const xhrSuccess = function (data, request) {
if ($.isFunction(options.success)) {
options.success.call($form, data, submitParam, request)
}
}
const xhrError = function(data, request) {
const xhrError = function (data, request) {
let $formObj = $form,
needValidForm = true
if ($.isFunction(options.error)) {
@ -217,7 +217,7 @@ $.fn.formSubmit = function(options) {
})
} else if (doUploadFile && $.handlesHTML5Files && $form.hasClass('handlingHTML5Files')) {
const args = $.extend({}, formData)
$form.find("input[type='file']").each(function() {
$form.find("input[type='file']").each(function () {
const $input = $(this),
file_list = $input.data('file_list')
if (file_list && file_list instanceof FileList) {
@ -269,7 +269,7 @@ $.fn.formSubmit = function(options) {
}
$.ajaxJSON.storeRequest(request, action, method, formData)
$frame.bind('form_response_loaded', function() {
$frame.bind('form_response_loaded', function () {
const i = $frame[0],
doc = i.contentDocument || i.contentWindow.document
if (doc.location.href == 'about:blank') return
@ -290,10 +290,7 @@ $.fn.formSubmit = function(options) {
$('#box_' + id).remove()
}, 5000)
})
$form
.data('submitting', true)
.submit()
.data('submitting', false)
$form.data('submitting', true).submit().data('submitting', false)
} else {
$.ajaxJSON(action, method, formData, xhrSuccess, xhrError)
}
@ -301,7 +298,7 @@ $.fn.formSubmit = function(options) {
return this
}
$.ajaxJSONPreparedFiles = function(options) {
$.ajaxJSONPreparedFiles = function (options) {
const list = []
const $this = this
const pre_list = options.files || options.file_elements || []
@ -314,7 +311,7 @@ $.ajaxJSONPreparedFiles = function(options) {
list.push(item)
}
const attachments = []
const ready = function() {
const ready = function () {
let data = options.formDataTarget == 'url' ? options.formData : {}
if (options.handle_files) {
let result = attachments
@ -328,7 +325,7 @@ $.ajaxJSONPreparedFiles = function(options) {
}
}
const uploadUrl = options.uploadDataUrl || '/files/pending'
const uploadFile = function(parameters, file) {
const uploadFile = function (parameters, file) {
// we want the s3 success url in the preflight response, not embedded in
// the upload_url. the latter doesn't work with the new ajax mechanism
parameters.no_redirect = true
@ -345,7 +342,7 @@ $.ajaxJSONPreparedFiles = function(options) {
;(options.upload_error || options.error).call($this, error)
})
}
var next = function() {
var next = function () {
const item = list.shift()
if (item) {
const attrs = $.extend(
@ -374,26 +371,23 @@ $.ajaxJSONPreparedFiles = function(options) {
next.call($this)
}
$.ajaxJSONFiles = function(url, submit_type, formData, files, success, error, options) {
$.ajaxJSONFiles = function (url, submit_type, formData, files, success, error, options) {
const $newForm = $(document.createElement('form'))
$newForm.attr('action', url).attr('method', submit_type)
// TODO: remove me once we stop proxying file uploads
formData.authenticity_token = authenticity_token()
const fileNames = {}
files.each(function() {
files.each(function () {
fileNames[$(this).attr('name')] = true
})
for (const idx in formData) {
if (!fileNames[idx]) {
const $input = $(document.createElement('input'))
$input
.attr('type', 'hidden')
.attr('name', idx)
.attr('value', formData[idx])
$input.attr('type', 'hidden').attr('name', idx).attr('value', formData[idx])
$newForm.append($input)
}
}
files.each(function() {
files.each(function () {
const $newFile = $(this).clone(true)
$(this).after($newFile)
$newForm.append($(this))
@ -411,20 +405,18 @@ $.ajaxJSONFiles = function(url, submit_type, formData, files, success, error, op
$.handlesHTML5Files = !!(window.File && window.FileReader && window.FileList && XMLHttpRequest)
if ($.handlesHTML5Files) {
$("input[type='file']").live('change', function(event) {
$("input[type='file']").live('change', function (event) {
const file_list = this.files
if (file_list) {
$(this).data('file_list', file_list)
$(this)
.parents('form')
.addClass('handlingHTML5Files')
$(this).parents('form').addClass('handlingHTML5Files')
}
})
}
$.ajaxFileUpload = function(options) {
$.ajaxFileUpload = function (options) {
// TODO: remove me once we stop proxying file uploads
options.data.authenticity_token = authenticity_token()
$.toMultipartForm(options.data, function(params) {
$.toMultipartForm(options.data, function (params) {
$.sendFormAsBinary(
{
url: options.url,
@ -457,7 +449,7 @@ $.ajaxFileUpload = function(options) {
})
}
$.httpSuccess = function(r) {
$.httpSuccess = function (r) {
try {
return (
(!r.status && location.protocol == 'file:') ||
@ -470,7 +462,7 @@ $.httpSuccess = function(r) {
return false
}
$.sendFormAsBinary = function(options, not_binary) {
$.sendFormAsBinary = function (options, not_binary) {
const body = options.body
const url = options.url
const method = options.method
@ -478,7 +470,7 @@ $.sendFormAsBinary = function(options, not_binary) {
if (xhr.upload) {
xhr.upload.addEventListener(
'progress',
function(event) {
function (event) {
if (options.progress && $.isFunction(options.progress)) {
options.progress.call(this, event)
}
@ -487,7 +479,7 @@ $.sendFormAsBinary = function(options, not_binary) {
)
xhr.upload.addEventListener(
'error',
function(event) {
function (event) {
if (options.error && $.isFunction(options.error)) {
options.error.call(this, 'uploading error', xhr, event)
}
@ -496,7 +488,7 @@ $.sendFormAsBinary = function(options, not_binary) {
)
xhr.upload.addEventListener(
'abort',
function(event) {
function (event) {
if (options.error && $.isFunction(options.error)) {
options.error.call(this, 'aborted by the user', xhr, event)
}
@ -504,7 +496,7 @@ $.sendFormAsBinary = function(options, not_binary) {
false
)
}
xhr.onreadystatechange = function(event) {
xhr.onreadystatechange = function (event) {
if (xhr.readyState == 4) {
let json = null
try {
@ -543,7 +535,7 @@ $.sendFormAsBinary = function(options, not_binary) {
}
}
$.fileData = function(file_object) {
$.fileData = function (file_object) {
return {
name: file_object.name || file_object.fileName,
size: file_object.size || file_object.fileSize,
@ -552,7 +544,7 @@ $.fileData = function(file_object) {
}
}
$.toMultipartForm = function(params, callback) {
$.toMultipartForm = function (params, callback) {
let boundary = '-----AaB03x' + _.uniqueId(),
result = {content_type: 'multipart/form-data; boundary=' + boundary},
body = '--' + boundary + '\r\n',
@ -617,11 +609,11 @@ $.toMultipartForm = function(params, callback) {
for (const jdx in value) {
fileList.push(value)
}
const finishedFiles = function() {
const finishedFiles = function () {
body += '--' + innerBoundary + '--\r\n' + '--' + boundary + '\r\n'
nextParam()
}
var nextFile = function() {
var nextFile = function () {
if (fileList.length === 0) {
finishedFiles()
return
@ -630,7 +622,7 @@ $.toMultipartForm = function(params, callback) {
fileData = $.fileData(file),
reader = new FileReader()
reader.onloadend = function() {
reader.onloadend = function () {
body +=
'--' +
innerBoundary +
@ -652,7 +644,7 @@ $.toMultipartForm = function(params, callback) {
} else if (window.File && value instanceof File) {
const fileData = $.fileData(value),
reader = new FileReader()
reader.onloadend = function() {
reader.onloadend = function () {
body +=
'Content-Disposition: file; name="' +
sanitizeQuotedString(name) +
@ -714,7 +706,7 @@ $.toMultipartForm = function(params, callback) {
// and "bad" and "assignment[bad]" with false.
// call_change: Specifies whether to trigger the onchange event
// for form elements that are set.
$.fn.fillFormData = function(data, opts) {
$.fn.fillFormData = function (data, opts) {
if (this.length) {
data = data || []
const options = $.extend({}, $.fn.fillFormData.defaults, opts)
@ -722,7 +714,7 @@ $.fn.fillFormData = function(data, opts) {
if (options.object_name) {
data = $._addObjectName(data, options.object_name, true)
}
this.find(':input').each(function() {
this.find(':input').each(function () {
const $obj = $(this)
const name = $obj.attr('name')
const inputType = $obj.attr('type')
@ -757,14 +749,14 @@ $.fn.fillFormData.defaults = {object_name: null, call_change: true}
// the result will include both "assignment[good]" and "good"
// values: specify the set of values to retrieve (if they exist)
// by default retrieves all it can find.
$.fn.getFormData = function(options) {
$.fn.getFormData = function (options) {
var options = $.extend({}, $.fn.getFormData.defaults, options),
result = {},
$form = this
$form
.find(':input')
.not(':button')
.each(function() {
.each(function () {
const $input = $(this),
inputType = $input.attr('type')
if ((inputType == 'radio' || inputType == 'checkbox') && !$input.attr('checked')) return
@ -816,7 +808,7 @@ $.fn.getFormData.defaults = {object_name: null}
// Used internally to prepend object_name to data key names
// Supports nested names, e.g.
// assignment[id] => discussion_topic[assignment][id]
$._addObjectName = function(data, object_name, include_original) {
$._addObjectName = function (data, object_name, include_original) {
if (!data) {
return data
}
@ -866,7 +858,7 @@ $._addObjectName = function(data, object_name, include_original) {
// Used internally to strip object_name from data key names
// Supports nested names, e.g.
// discussion_topic[assignment][id] => assignment[id]
$._stripObjectName = function(data, object_name, include_original) {
$._stripObjectName = function (data, object_name, include_original) {
let new_result = {}
let short_name
if (data instanceof Array) {
@ -917,7 +909,7 @@ $._stripObjectName = function(data, object_name, include_original) {
// labels: map of element names to labels to be used in error reporting. The validation
// will attempt to determine the appropriate label via HTML <label for="..."> elements
// if not specified
$.fn.validateForm = function(options) {
$.fn.validateForm = function (options) {
if (this.length === 0) {
return false
}
@ -1013,7 +1005,7 @@ $.fn.validateForm.defaults = {object_name: null, required: null, dates: null, ti
// Takes in an errors object and creates little pop-up message boxes over
// each errored form field displaying the error text. Still needs some
// css lovin'.
$.fn.formErrors = function(data_errors, options) {
$.fn.formErrors = function (data_errors, options) {
if (this.length === 0) {
return
}
@ -1094,11 +1086,7 @@ $.fn.formErrors = function(data_errors, options) {
.filter(':not(:visible)')
.first()
if ($hiddenInput && $hiddenInput.length > 0) {
if (
$hiddenInput[0].tagName === 'TEXTAREA' &&
$hiddenInput.data('remoteEditor') &&
ENV.use_rce_enhancements
) {
if ($hiddenInput[0].tagName === 'TEXTAREA' && $hiddenInput.data('remoteEditor')) {
// this textarea is tied to the new rce
$obj = $hiddenInput.next()
} else {
@ -1141,7 +1129,7 @@ $.fn.formErrors = function(data_errors, options) {
// Pops up a small box containing the given message. The box is connected to the given form element, and will
// go away when the element is selected.
$.fn.errorBox = function(message, scroll, override_position) {
$.fn.errorBox = function (message, scroll, override_position) {
if (this.length) {
const $obj = this,
$oldBox = $obj.data('associated_error_box')
@ -1186,7 +1174,7 @@ $.fn.errorBox = function(message, scroll, override_position) {
})
.fadeIn('fast')
const cleanup = function() {
const cleanup = function () {
const $screenReaderErrors = $('#flash_screenreader_holder').find('span')
const srError = _.find($screenReaderErrors, node => $(node).text() == $box.text())
$box.remove()
@ -1197,7 +1185,7 @@ $.fn.errorBox = function(message, scroll, override_position) {
$obj.removeData('associated_error_object')
}
const fade = function() {
const fade = function () {
$box.stop(true, true).fadeOut('slow', cleanup)
}
@ -1209,7 +1197,7 @@ $.fn.errorBox = function(message, scroll, override_position) {
.click(fade)
.keypress(fade)
$box.click(function() {
$box.click(function () {
$(this).fadeOut('fast', cleanup)
})
@ -1224,7 +1212,7 @@ $.fn.errorBox = function(message, scroll, override_position) {
}
}
$.fn.errorBox.errorBoxes = []
$.moveErrorBoxes = function() {
$.moveErrorBoxes = function () {
const list = []
const prevList = $.fn.errorBox.errorBoxes
// ember does silly things with arrays
@ -1263,7 +1251,7 @@ $.moveErrorBoxes = function() {
}
}
// Hides all error boxes for the given form element and its input elements.
$.fn.hideErrors = function(options) {
$.fn.hideErrors = function (options) {
if (this.length) {
const $oldBox = this.data('associated_error_box')
const $screenReaderErrors = $('#flash_screenreader_holder').find('span')
@ -1271,7 +1259,7 @@ $.fn.hideErrors = function(options) {
$oldBox.remove()
this.data('associated_error_box', null)
}
this.find(':input').each(function() {
this.find(':input').each(function () {
const $obj = $(this),
$oldBox = $obj.data('associated_error_box')
if ($oldBox) {
@ -1287,7 +1275,7 @@ $.fn.hideErrors = function(options) {
return this
}
$.fn.markRequired = function(options) {
$.fn.markRequired = function (options) {
if (!options.required) {
return
}
@ -1296,13 +1284,13 @@ $.fn.markRequired = function(options) {
required = $._addObjectName(required, options.object_name)
}
const $form = $(this)
$.each(required, function(i, name) {
$.each(required, function (i, name) {
const field = $form.find('[name="' + name + '"]')
if (!field.length) {
return
}
field.attr({'aria-required': 'true'})
field.each(function() {
field.each(function () {
if (!this.id) {
return
}
@ -1322,7 +1310,7 @@ $.fn.markRequired = function(options) {
})
}
$.fn.getFieldLabelString = function(name) {
$.fn.getFieldLabelString = function (name) {
const field = $(this).find('[name="' + name + '"]')
if (!field.length || !field[0].id) {
return

View File

@ -20,7 +20,6 @@ import I18n from 'i18n!OutcomeContentBase'
import $ from 'jquery'
import _ from 'underscore'
import ValidatedFormView from '@canvas/forms/backbone/views/ValidatedFormView.coffee'
import RCEKeyboardShortcuts from '@canvas/tinymce-keyboard-shortcuts'
import RichContentEditor from '@canvas/rce/RichContentEditor'
import '@canvas/rails-flash-notifications'
import '@canvas/jquery/jquery.disableWhileLoading'
@ -245,39 +244,15 @@ export default class OutcomeContentBase extends ValidatedFormView {
return this.model.set(this._modelAttributes)
}
setupTinyMCEViewSwitcher() {
$('.rte_switch_views_link').click(e => {
e.preventDefault()
RichContentEditor.callOnRCE(this.$('textarea'), 'toggle')
// hide the clicked link, and show the other toggle link.
$(e.currentTarget)
.siblings('.rte_switch_views_link')
.andSelf()
.toggle()
.focus()
})
}
addTinyMCEKeyboardShortcuts() {
if (!ENV.use_rce_enhancements) {
const keyboardShortcutsView = new RCEKeyboardShortcuts()
return keyboardShortcutsView.render().$el.insertBefore($('.rte_switch_views_link:first'))
}
}
// Called from subclasses in render.
readyForm() {
return setTimeout(() => {
RichContentEditor.loadNewEditor(this.$('textarea'), {
getRenderingTarget(t) {
const wrappedTextarea = $(t)
.wrap(`<div id='parent-of-${t.id}'></div>`)
.get(0)
const wrappedTextarea = $(t).wrap(`<div id='parent-of-${t.id}'></div>`).get(0)
return wrappedTextarea.parentNode
}
}) // tinymce initializer
this.setupTinyMCEViewSwitcher()
this.addTinyMCEKeyboardShortcuts()
})
return this.$('input:first').focus()
})
}

View File

@ -141,19 +141,14 @@ export default class OutcomeView extends OutcomeContentBase {
editRating(e) {
e.preventDefault()
const childIdx = $(e.currentTarget)
.closest('.rating')
.index()
const childIdx = $(e.currentTarget).closest('.rating').index()
const $th = $(`.criterion thead tr > th:nth-child(${childIdx + 1})`)
const $showWrapper = $(e.currentTarget).parents('.show:first')
const $editWrapper = $showWrapper.next()
$showWrapper.attr('aria-expanded', 'false').hide()
$editWrapper.attr('aria-expanded', 'true').show()
$th
.find('h5')
.attr('aria-expanded', 'false')
.hide()
$th.find('h5').attr('aria-expanded', 'false').hide()
return $editWrapper.find('.outcome_rating_description').focus()
}
@ -164,15 +159,9 @@ export default class OutcomeView extends OutcomeContentBase {
const deleteBtn = $(e.currentTarget)
const childIdx = deleteBtn.closest('.rating').index()
const $th = $(`.criterion thead tr > th:nth-child(${childIdx + 1})`)
let focusTarget = deleteBtn
.closest('.rating')
.prev()
.find('.insert_rating')
let focusTarget = deleteBtn.closest('.rating').prev().find('.insert_rating')
if (focusTarget.length === 0) {
focusTarget = deleteBtn
.closest('.rating')
.next()
.find('.edit_rating')
focusTarget = deleteBtn.closest('.rating').next().find('.edit_rating')
}
$th.remove()
deleteBtn.closest('td').remove()
@ -183,9 +172,7 @@ export default class OutcomeView extends OutcomeContentBase {
saveRating(e) {
e.preventDefault()
const childIdx = $(e.currentTarget)
.closest('.rating')
.index()
const childIdx = $(e.currentTarget).closest('.rating').index()
const $th = $(`.criterion thead tr > th:nth-child(${childIdx + 1})`)
const $editWrapper = $(e.currentTarget).parents('.edit:first')
const $showWrapper = $editWrapper.prev()
@ -199,10 +186,7 @@ export default class OutcomeView extends OutcomeContentBase {
$showWrapper.find('.points').text(points)
$editWrapper.attr('aria-expanded', 'false').hide()
$showWrapper.attr('aria-expanded', 'true').show()
$th
.find('h5')
.attr('aria-expanded', 'true')
.show()
$th.find('h5').attr('aria-expanded', 'true').show()
$showWrapper.find('.edit_rating').focus()
return this.updateRatings()
}
@ -210,20 +194,12 @@ export default class OutcomeView extends OutcomeContentBase {
insertRating(e) {
e.preventDefault()
const $rating = $(criterionTemplate({description: '', points: '', _index: 99}))
const childIdx = $(e.currentTarget)
.closest('.rating-header')
.index()
const childIdx = $(e.currentTarget).closest('.rating-header').index()
const $ratingHeader = $(criterionHeaderTemplate({description: '', _index: 99}))
const $tr = $('.criterion tbody tr')
$(e.currentTarget)
.closest('.rating-header')
.after($ratingHeader)
$(e.currentTarget).closest('.rating-header').after($ratingHeader)
$tr.find(`> td:nth-child(${childIdx + 1})`).after($rating)
$rating
.find('.show')
.hide()
.next()
.show(200)
$rating.find('.show').hide().next().show(200)
$ratingHeader.hide().show(200)
$rating.find('.edit input:first').focus()
return this.updateRatings()
@ -260,10 +236,7 @@ export default class OutcomeView extends OutcomeContentBase {
const iterable = this.$('.rating')
for (let index = 0; index < iterable.length; index++) {
const r = iterable[index]
const rating =
$(r)
.find('.outcome_rating_points')
.val() || 0
const rating = $(r).find('.outcome_rating_points').val() || 0
total = _.max([total, numberHelper.parse(rating)])
for (const i of Array.from($(r).find('input'))) {
// reset indices
@ -298,7 +271,6 @@ export default class OutcomeView extends OutcomeContentBase {
outcomeFormTemplate(
_.extend(data, {
calculationMethods: this.model.calculationMethods(),
use_rce_enhancements: ENV.use_rce_enhancements,
hideMasteryScale: ENV.ACCOUNT_LEVEL_MASTERY_SCALES
})
)

View File

@ -8,18 +8,6 @@
<label for="description">{{#t "description"}}Describe this outcome{{/t}}:</label>
{{#unless use_rce_enhancements}}
<div class="pull-right">
<a href="#" class="rte_switch_views_link">
{{t "HTML Editor"}}
</a>
<a href="#" class="rte_switch_views_link" style="display:none;">
{{t "Rich Content Editor"}}
</a>
</div>
<div style="clear:both;"></div>
{{/unless}}
<textarea cols="40" name="description" id=description rows="20" style="display: none; width: 100%; height: 150px;">{{description}}</textarea>
{{#unless hideMasteryScale}}

View File

@ -26,7 +26,6 @@ export const ACCOUNT_GROUP_ID = '-1'
export const getContext = isMobileView => {
const [snakeContextType, contextId] = ENV.context_asset_string.split('_')
const contextType = snakeContextType === 'course' ? 'Course' : 'Account'
const useRceEnhancements = ENV.use_rce_enhancements
const rootOutcomeGroup = ENV.ROOT_OUTCOME_GROUP
const friendlyDescriptionFF = ENV.OUTCOMES_FRIENDLY_DESCRIPTION
const canManage = ENV.PERMISSIONS?.manage_outcomes
@ -42,7 +41,6 @@ export const getContext = isMobileView => {
env: {
contextType,
contextId,
useRceEnhancements,
rootOutcomeGroup,
friendlyDescriptionFF,
isMobileView,

View File

@ -24,7 +24,6 @@ const useCanvasContext = () => {
const contextType = context?.env?.contextType
const contextId = context?.env?.contextId
const isCourse = context?.env?.contextType === 'Course'
const useRceEnhancements = context?.env?.useRceEnhancements
const rootOutcomeGroup = context?.env?.rootOutcomeGroup
const friendlyDescriptionFF = context?.env?.friendlyDescriptionFF
const isMobileView = context?.env?.isMobileView
@ -41,7 +40,6 @@ const useCanvasContext = () => {
contextType,
contextId,
isCourse,
useRceEnhancements,
rootOutcomeGroup,
friendlyDescriptionFF,
isMobileView,

View File

@ -55,8 +55,6 @@ export function getTinymce() {
export function send($target, methodName, ...args) {
const remoteEditor = $target.data('remoteEditor') || $target[0]?.remoteEditor
if (methodName === 'RCEClosed' && !ENV.use_rce_enhancements) return
if (remoteEditor) {
let ret
if (methodName === 'get_code' && remoteEditor.isHidden()) {

View File

@ -24,18 +24,14 @@
import serviceRCELoader from './serviceRCELoader'
import {RCELOADED_EVENT_NAME, send, destroy, focus} from './RceCommandShim'
import deprecated from './util/deprecated'
import $ from 'jquery'
const Sidebar = !ENV.use_rce_enhancements && require('./Sidebar').default
function loadServiceRCE(target, tinyMCEInitOptions, callback) {
target.css('display', 'none')
const originalOnFocus = tinyMCEInitOptions.onFocus
tinyMCEInitOptions.onFocus = (...args) => {
if (!ENV.use_rce_enhancements) RichContentEditor.showSidebar()
if (originalOnFocus instanceof Function) {
originalOnFocus(...args)
}
@ -112,13 +108,10 @@ function freshNode(target) {
return newTarget
}
const deprecationMsg =
"with the new RCE you don't need to call this method, it is a no op since there is no sidebar"
const RichContentEditor = {
/**
* start the remote module (if the feature flag is on) loading so that it's
* hopefully done by the time initSidebar and loadNewEditor are called.
* hopefully done by the time loadNewEditor is called.
* should typically be called at the top of any source file that calls one
* of those.
*
@ -128,43 +121,12 @@ const RichContentEditor = {
return serviceRCELoader.preload(cb)
},
/**
* load the sidebar. can pass callbacks to execute any time the sidebar is
* shown (`show`) or hidden (`hide`).
*
* @public
*/
initSidebar: deprecated(deprecationMsg, (subscriptions = {}) => {
if (!ENV.use_rce_enhancements) Sidebar.init(subscriptions)
}),
/**
* show the sidebar if it's around
*
* @public
*/
showSidebar: deprecated(deprecationMsg, () => {
if (!ENV.use_rce_enhancements) Sidebar.show()
}),
/**
* hide the sidebar if it's around
*
* @public
*/
hideSidebar: deprecated(deprecationMsg, () => {
if (!ENV.use_rce_enhancements) Sidebar.hide()
}),
/**
* load an editor into the target element with the given options. most
* options are passed on to tinymce, but locally:
*
* focus (boolean)
* claim the new editor as active immediately after it's loaded
* (including showing the sidebar if any)
*
* manageParent (boolean)
* ensure the target element has a containing div that doesn't contain
@ -241,7 +203,7 @@ const RichContentEditor = {
},
/**
* remove the target editor. if there's a sidebar, hide it
* remove the target editor.
*
* @public
*/
@ -249,7 +211,6 @@ const RichContentEditor = {
let $target = node2jquery(target)
$target = this.freshNode($target)
destroy($target)
if (!ENV.use_rce_enhancements) Sidebar.hide()
},
/**
@ -258,14 +219,11 @@ const RichContentEditor = {
* @public
*/
closeRCE(target) {
if (window.ENV.use_rce_enhancements) {
this.callOnRCE(target, 'RCEClosed')
}
this.callOnRCE(target, 'RCEClosed')
},
/**
* make the target the active editor, including to be recipient of sidebar
* events. if there's a sidebar, make sure it's showing
* make the target the active editor
*
* @private
*/
@ -273,7 +231,6 @@ const RichContentEditor = {
let $target = node2jquery(target)
$target = this.freshNode($target)
focus($target)
if (!ENV.use_rce_enhancements) Sidebar.show()
},
freshNode,

View File

@ -20,7 +20,6 @@ const RichContentEditor = {
preloadRemoteModule() {},
loadNewEditor() {},
destroyRCE() {},
initSidebar() {},
callOnRCE(textarea, opName) {
if (opName === 'get_code') return textarea.innerHTML
},

View File

@ -17,9 +17,6 @@
import getRCSProps from '../getRCSProps'
describe('getRCSProps', () => {
beforeEach(() => {
ENV.use_rce_enhancements = true
})
it('returns null if there is no context_asset_string in the environment', () => {
expect(getRCSProps()).toBeNull()
})

View File

@ -0,0 +1,116 @@
/*
* Copyright (C) 2021 - 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 EquationEditorView from '@canvas/rce/backbone/views/EquationEditorView'
import loadEventListeners from '@canvas/rce/loadEventListeners'
import 'jquery'
import 'jqueryui/tabs'
import 'browser-sniffer'
describe('loadEventListeners', () => {
let fakeEditor, dispatchEvent
beforeAll(() => {
window.INST.editorButtons = [{id: '__BUTTON_ID__'}]
fakeEditor = {
id: 'someId',
bookmarkMoved: false,
focus: () => {},
dom: {createHTML: () => "<a href='#'>stub link html</a>"},
selection: {
getBookmark: () => ({}),
getNode: () => ({}),
getContent: () => ({}),
moveToBookmark: _prevSelect => (fakeEditor.bookmarkMoved = true)
},
addCommand: () => ({}),
addButton: () => ({}),
ui: {
registry: {
addButton: () => {},
addMenuButton: () => {},
addIcon: () => {},
addNestedMenuItem: () => {}
}
}
}
dispatchEvent = name => {
const event = document.createEvent('CustomEvent')
const eventData = {
ed: fakeEditor,
selectNode: '<div></div>'
}
event.initCustomEvent(`tinyRCE/${name}`, true, true, eventData)
document.dispatchEvent(event)
}
})
afterAll(() => {
window.alert.restore && window.alert.restore()
console.log.restore && console.log.restore() // eslint-disable-line no-console
})
afterEach(() => {
jest.restoreAllMocks()
})
it('initializes equation editor plugin', done => {
loadEventListeners({
equationCB: view => {
expect(view instanceof EquationEditorView).toBeTruthy()
expect(view.$editor.selector).toEqual('#someId')
done()
}
})
dispatchEvent('initEquation')
})
it('initializes equella plugin', done => {
window.alert = jest.fn()
loadEventListeners({
equellaCB() {
expect(window.alert).toHaveBeenCalledWith(
'Equella is not properly configured for this account, please notify your system administrator.'
)
done()
}
})
const event = document.createEvent('CustomEvent')
const eventData = {
ed: fakeEditor,
selectNode: '<div></div>'
}
event.initCustomEvent('tinyRCE/initEquella', true, true, eventData)
document.dispatchEvent(event)
})
it('initializes external tools plugin', () => {
fakeEditor.addCommand = jest.fn()
loadEventListeners()
const event = document.createEvent('CustomEvent')
const eventData = {
ed: fakeEditor,
url: 'someurl.com'
}
event.initCustomEvent('tinyRCE/initExternalTools', true, true, eventData)
document.dispatchEvent(event)
expect(fakeEditor.addCommand).toHaveBeenCalledWith(
'instructureExternalButton__BUTTON_ID__',
expect.any(Function)
)
})
})

View File

@ -1,264 +0,0 @@
//
// Copyright (C) 2012 - 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 I18n from 'i18n!InsertUpdateImageView'
import $ from 'jquery'
import _ from 'underscore'
import React from 'react'
import ReactDOM from 'react-dom'
import htmlEscape from 'html-escape'
import FileBrowser from '../../FileBrowser'
import DialogBaseView from '@canvas/dialog-base-view'
import template from '../../jst/InsertUpdateImageView.handlebars'
import {send} from '../../RceCommandShim'
import FindFlickrImageView from './FindFlickrImageView.coffee'
import 'jqueryui/tabs.js'
export default class InsertUpdateImageView extends DialogBaseView {
toJSON() {
return {show_quiz_warning: ENV.SHOW_QUIZ_ALT_TEXT_WARNING}
}
initialize(editor, selectedNode) {
this.update = this.update.bind(this)
this.setSelectedImage = this.setSelectedImage.bind(this)
this.constrainProportions = this.constrainProportions.bind(this)
this.onFileLinkClick = this.onFileLinkClick.bind(this)
this.editor = editor
this.$editor = $(`#${this.editor.id}`)
this.prevSelection = this.editor.selection.getBookmark()
this.$selectedNode = $(selectedNode)
super.initialize(...arguments)
this.render()
this.show()
this.dialog
.parent()
.find('.ui-dialog-titlebar-close')
.click(() => this.restoreCaret())
if (this.$selectedNode.prop('nodeName') === 'IMG') {
return this.setSelectedImage({
src: this.$selectedNode.attr('src'),
alt: this.$selectedNode.attr('alt'),
width: this.$selectedNode.width(),
height: this.$selectedNode.height(),
'data-decorative': this.$selectedNode.attr('data-decorative')
})
}
}
afterRender() {
this.$('.imageSourceTabs').tabs()
}
onTabsshow(event, ui) {
const loadTab = fn => {
if (this[`${ui.panel.id}IsLoaded`]) return
this[`${ui.panel.id}IsLoaded`] = true
const loadingDfd = $.Deferred()
$(ui.panel).disableWhileLoading(loadingDfd)
fn(loadingDfd.resolve)
}
switch (ui.panel.id) {
case 'tabUploaded':
loadTab(done => {
ReactDOM.render(
<FileBrowser
allowUpload
contentTypes={['image/*']}
selectFile={this.setSelectedImage}
useContextAssets
/>,
this.$el[0].querySelector('#tabUploaded'),
done
)
})
break
case 'tabFlickr':
loadTab(done => {
new FindFlickrImageView().render().$el.appendTo(ui.panel)
done()
})
break
}
}
setAspectRatio() {
const width = Number(this.$("[name='image[width]']").val())
const height = Number(this.$("[name='image[height]']").val())
if (width && height) {
return (this.aspectRatio = width / height)
} else {
delete this.aspectRatio
}
}
constrainProportions(event) {
const val = Number($(event.target).val())
if (this.aspectRatio && (val || val === 0)) {
if ($(event.target).is('[name="image[height]"]')) {
this.$('[name="image[width]"]').val(Math.round(val * this.aspectRatio))
} else {
this.$('[name="image[height]"]').val(Math.round(val / this.aspectRatio))
}
}
}
setSelectedImage(attributes = {}) {
// set given attributes immediately; update width and height after image loads
let value
for (var key in attributes) {
value = attributes[key]
this.$(`[name='image[${key}]']`).val(value)
}
const dfd = $.Deferred()
const onLoad = ({target: img}) => {
const newAttributes = _.defaults(attributes, {
width: img.width,
height: img.height
})
for (key in newAttributes) {
value = newAttributes[key]
if (this.$(`[name='image[${key}]']`).attr('type') === 'checkbox') {
this.$(`[name='image[${key}]']`).attr('checked', !!value)
} else {
this.$(`[name='image[${key}]']`).val(value)
}
}
if (newAttributes['data-decorative']) {
this.$("[name='image[alt]']").attr('disabled', true)
}
this.setAspectRatio()
dfd.resolve(newAttributes)
}
const onError = ({target: _img}) => {
const newAttributes = {
width: '',
height: ''
}
for (key in newAttributes) {
value = newAttributes[key]
this.$(`[name='image[${key}]']`).val(value)
}
}
this.$img = $('<img>', attributes)
.load(onLoad)
.error(onError)
return dfd
}
getAttributes() {
let val
const res = {}
for (var key of ['width', 'height']) {
val = Number(this.$(`[name='image[${key}]']`).val())
if (val && val > 0) res[key] = val
}
for (key of ['src', 'alt']) {
val = this.$(`[name='image[${key}]']`).val()
if (val) res[key] = val
}
if (this.$("[name='image[data-decorative]']").is(':checked')) {
res.alt = ''
res['data-decorative'] = true
}
res['data-mce-src'] = res.src
return res
}
onFileLinkClick(event) {
event.preventDefault()
this.$('.active')
.removeClass('active')
.parent()
.removeAttr('aria-selected')
const $a = $(event.currentTarget).addClass('active')
$a.parent().attr('aria-selected', true)
this.flickr_link = $a.attr('data-linkto')
this.setSelectedImage({
src: $a.attr('data-fullsize'),
alt: $a.attr('title')
})
this.$("[name='image[alt]']").focus()
}
onFileLinkDblclick = () => {
this.update()
}
onImageUrlChange(event) {
this.flickr_link = null
return this.setSelectedImage({src: $(event.currentTarget).val()})
}
onDecorativeChange() {
if (this.$("[name='image[data-decorative]']").is(':checked')) {
this.$("[name='image[alt]']").attr('disabled', true)
} else {
this.$("[name='image[alt]']").removeAttr('disabled')
}
}
close() {
super.close(...arguments)
this.restoreCaret()
}
restoreCaret() {
this.editor.selection.moveToBookmark(this.prevSelection)
ReactDOM.unmountComponentAtNode(this.$el[0].querySelector('#tabUploaded'))
}
generateImageHtml() {
let imgHtml = this.editor.dom.createHTML('img', this.getAttributes())
if (this.flickr_link) {
imgHtml = `<a href='${htmlEscape(this.flickr_link)}'>${imgHtml}</a>`
}
return imgHtml
}
update() {
this.restoreCaret()
if (this.$selectedNode.is('img')) {
// Kill the alt/decorative props (but they get added back if needed)
this.$selectedNode.removeAttr('alt')
this.$selectedNode.removeAttr('data-decorative')
this.$selectedNode.attr(this.getAttributes())
} else {
send(this.$editor, 'insert_code', this.generateImageHtml())
}
this.editor.focus()
this.close()
}
}
InsertUpdateImageView.prototype.template = template
InsertUpdateImageView.prototype.events = {
'change [name="image[width]"]': 'constrainProportions',
'change [name="image[height]"]': 'constrainProportions',
'click .flickrImageResult, .treeFile': 'onFileLinkClick',
'change [name="image[src]"]': 'onImageUrlChange',
'tabsshow .imageSourceTabs': 'onTabsshow',
'dblclick .flickrImageResult, .treeFile': 'onFileLinkDblclick',
'change [name="image[data-decorative]"]': 'onDecorativeChange'
}
InsertUpdateImageView.prototype.dialogOptions = {
id: 'rce__insert_edit_image',
width: 625,
title: I18n.t('titles.insert_edit_image', 'Insert / Edit Image'),
destroy: true
}

View File

@ -24,16 +24,7 @@ function editorOptions(width, id, tinyMCEInitOptions, enableBookmarkingOverride,
const editorConfig = new EditorConfig(tinymce, INST, width, id)
const config = {
...editorConfig.defaultConfig(),
setup: ed => {
if (!ENV.use_rce_enhancements) {
ed.on('init', () => {
const getDefault = mod => (mod.default ? mod.default : mod)
const EditorAccessibility = getDefault(require('./jquery/editorAccessibility'))
new EditorAccessibility(ed).accessiblize()
})
}
}
...editorConfig.defaultConfig()
}
return {

View File

@ -30,7 +30,7 @@ export default function getRCSProps() {
// set in rich_content.rb if user has :manage_files_add right
// though comment says it may (eventually) be in the jwt
// TODO: look into that.
const canUploadFiles = !ENV.use_rce_enhancements || ENV.RICH_CONTENT_CAN_UPLOAD_FILES
const canUploadFiles = ENV.RICH_CONTENT_CAN_UPLOAD_FILES
if (!canUploadFiles || contextType === 'account') {
contextId = userId
contextType = 'user'

View File

@ -16,24 +16,12 @@
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {getInstance, setLocale} from 'tinymce-a11y-checker'
import {beforeCheck, afterCheck} from './a11yCheckerHooks'
import {setLocale} from 'tinymce-a11y-checker'
if (ENV.use_rce_enhancements) {
console.error('We should not have gotten here')
} else {
getInstance(c =>
c.setConfig({
beforeCheck,
afterCheck
})
)
if (ENV && ENV.LOCALE) {
let locale = ENV.LOCALE
if (locale === 'zh-Hant') {
locale = 'zh-HK'
}
setLocale(locale)
if (ENV?.LOCALE) {
let locale = ENV.LOCALE
if (locale === 'zh-Hant') {
locale = 'zh-HK'
}
setLocale(locale)
}

View File

@ -1,86 +0,0 @@
//
// Copyright (C) 2013 - 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 I18n from 'i18n!editor_accessibility'
import $ from 'jquery'
// #
// Used to insert accessibility titles into core TinyMCE components
export default class EditorAccessiblity {
constructor(editor) {
this.editor = editor
this.id_prepend = editor.id
this.$el = $(`#${editor.editorContainer.id}`)
}
accessiblize() {
this._cacheElements()
this._addTitles()
this._addLabels()
this._accessiblizeMenubar()
this._removeStatusbarFromTabindex()
}
/* PRIVATE FUNCTIONS */
_cacheElements() {
this.$iframe = this.$el.find('.mce-edit-area iframe')
}
_addLabels() {
this.$el.attr('aria-label', I18n.t('Rich Content Editor'))
$(this.editor.getBody()).attr('aria-label', $(`label[for="${this.id_prepend}"]`).text())
this.$el
.find("div[aria-label='Font Sizes']")
.attr('aria-label', I18n.t('titles.font_size', 'Font Size, press down to select'))
this.$el
.find('div.mce-listbox.mce-last:not([aria-label])')
.attr('aria-label', I18n.t('titles.formatting', 'Formatting, press down to select'))
this.$el
.find("div[aria-label='Text color']")
.attr('aria-label', I18n.t('accessibles.forecolor', 'Text Color, press down to select'))
this.$el
.find("div[aria-label='Background color'")
.attr(
'aria-label',
I18n.t('accessibles.background_color', 'Background Color, press down to select')
)
}
_addTitles() {
this.$iframe.attr('title', I18n.t('titles.rte_help', 'Rich Text Area. Press ALT+F8 for help'))
}
// Hide the menubar until ALT+F9 is pressed.
_accessiblizeMenubar() {
const $menubar = this.$el.find('.mce-menubar')
const $firstMenu = $menubar.find('.mce-menubtn').first()
$menubar.hide()
this.editor.addShortcut('Alt+F9', '', () => {
$menubar.show()
$firstMenu.focus()
// Once it's shown, we don't need to show it again, so replace this handler with one that just focuses.
this.editor.addShortcut('Alt+F9', '', () => $firstMenu.focus())
})
}
// keyboard only nav gets permastuck in the statusbar in FF. If you can't
// click with a mouse, the only way out is to refresh the page.
_removeStatusbarFromTabindex() {
const $statusbar = this.$el.find('.mce-statusbar > .mce-container-body')
$statusbar.attr('tabindex', -1)
}
}

View File

@ -1,68 +0,0 @@
<div class="insertUpdateImage bootstrap-form form-horizontal" >
<fieldset>
<legend>{{#t "image_source"}}Image Source{{/t}}</legend>
<div class="ui-tabs-minimal imageSourceTabs">
<ul>
<li><a href="#tabUrl">{{#t "url"}}URL{{/t}}</a></li>
<li><a href="#tabUploaded">{{#t "canvas"}}Canvas{{/t}}</a></li>
<li><a href="#tabFlickr">{{#t "flickr"}}Flickr{{/t}}</a></li>
</ul>
<div id="tabUrl">
<input type="url"
name="image[src]"
class="input-xxlarge"
placeholder="http://example.com/image.png"
aria-label="{{#t "image_url_field_label" }}Image URL{{/t}}"
style="margin-bottom: 20px;">
</div>
<div class="insertUpdateImageTabpane" id="tabUploaded"></div>
<div id="tabFlickr"></div>
</div>
</fieldset>
<fieldset>
<legend>{{#t "attributes"}}Attributes{{/t}}</legend>
<div class="control-group">
<label class="control-label" for="image_alt">{{#t "alt_text"}}Alt text{{/t}}</label>
<div class="controls">
<input type="text"
class="input-xlarge"
name="image[alt]"
id="image_alt"
aria-describedby="alt_text_description">
<span><p class="help-block" id="alt_text_description">{{#t "alt_help_text"}}Describe the image to improve accessibility{{/t}}</p></span>
{{#if show_quiz_warning}}
<br/>
<span><p class="help-block">
{{#t "quiz_warning"}}Note: This label can be viewed by students taking the quiz.{{/t}}
</p></span>
{{/if}}
</div>
</div>
<div class="control-group">
<label class="control-label" for="decorative_image">{{#t}}Decorative Image{{/t}}</label>
<div class="controls">
<input
type="checkbox"
id="decorative_image"
name="image[data-decorative]"
/>
<span><p class="help-block" id="decorative_image_description">{{#t}}Indicates the image is for decorative purposes only and should not be read by screenreaders{{/t}}</p></span>
</div>
</div>
<div class="control-group">
<label class="control-label" for="dimensions_controls">{{#t "dimensions"}}Dimensions{{/t}}</label>
<div class="controls" id="dimensions_controls" aria-describedby="aspect_ratio_note">
<input class="span1"
name="image[width]"
type="text"
aria-label="{{#t "image_width"}}Image Width{{/t}}">
x
<input class="span1"
name="image[height]"
type="text"
aria-label="{{#t "image_height"}}Image Height{{/t}}">
<span><p class="help-block" id="aspect_ratio_note">{{#t "dimension_help_text"}}Aspect ratio will be preserved{{/t}}</p></span>
</div>
</div>
</fieldset>
</div>

View File

@ -1,4 +0,0 @@
{
"brandableCSSBundle": "jst/tinymce/InsertUpdateImageView",
"i18nScope": "tinymce.insert_update_image_view"
}

View File

@ -17,67 +17,40 @@
*/
import initializeExternalTools from '@canvas/tinymce-external-tools'
import INST from 'browser-sniffer'
import Links from '@canvas/tinymce-links'
import I18n from 'i18n!loadEventListeners'
import {showFlashError} from '@canvas/alerts/react/FlashAlert'
export default function loadEventListeners(callbacks = {}) {
const validCallbacks = [
'equationCB',
'linksCB',
'imagePickerCB',
'equellaCB',
'externalToolCB',
'recordCB'
]
const validCallbacks = ['equationCB', 'equellaCB', 'externalToolCB']
validCallbacks.forEach(cbName => {
if (callbacks[cbName] === undefined) {
callbacks[cbName] = function() {
callbacks[cbName] = function () {
/* no-op */
}
}
})
document.addEventListener('tinyRCE/initEquation', ({detail}) => {
import('./backbone/views/EquationEditorView').then(({default: EquationEditorView}) => {
const view = new EquationEditorView(detail.ed)
callbacks.equationCB(view)
})
})
document.addEventListener('tinyRCE/initLinks', ({detail}) => {
Links.renderDialog(detail.ed)
callbacks.linksCB()
})
document.addEventListener('tinyRCE/initImagePicker', e => {
import('./backbone/views/InsertUpdateImageView').then(
({default: InsertUpdateImageView}) => {
const view = new InsertUpdateImageView(e.detail.ed, e.detail.selectedNode)
callbacks.imagePickerCB(view)
}
)
import('./backbone/views/EquationEditorView')
.then(({default: EquationEditorView}) => {
const view = new EquationEditorView(detail.ed)
callbacks.equationCB(view)
})
.catch(showFlashError(I18n.t('Something went wrong loading the equation editor')))
})
document.addEventListener('tinyRCE/initEquella', e => {
import('@canvas/tinymce-equella').then(
({default: initializeEquella}) => {
import('@canvas/tinymce-equella')
.then(({default: initializeEquella}) => {
initializeEquella(e.detail.ed)
callbacks.equellaCB()
}
)
})
.catch(showFlashError(I18n.t('Something went wrong loading Equella')))
})
document.addEventListener('tinyRCE/initExternalTools', e => {
initializeExternalTools.init(e.detail.ed, e.detail.url, INST)
callbacks.externalToolCB()
})
document.addEventListener('tinyRCE/initRecord', e => {
import('@canvas/tinymce-record').then(
({default: mediaEditorLoader}) => {
mediaEditorLoader.insertEditor(e.detail.ed)
callbacks.recordCB()
}
)
})
}

View File

@ -24,9 +24,9 @@ import polyfill from './polyfill'
import getRCSProps from './getRCSProps'
import closedCaptionLanguages from '@canvas/util/closedCaptionLanguages'
let loadingPromise
const RCELoader = {
loadingPromise: null,
preload(cb) {
// since we are just preloading, let other stuff waiting to run go first so we don't slow pageload
;(window.requestIdleCallback || window.setTimeout)(() => this.loadRCE(cb))
@ -53,20 +53,6 @@ const RCELoader = {
})
},
loadSidebarOnTarget(target, callback) {
if (ENV.RICH_CONTENT_SKIP_SIDEBAR) {
return
}
const props = getRCSProps()
this.loadRCE(RCE => {
RCE.renderSidebarIntoDiv(target, props, remoteSidebar => {
callback(polyfill.wrapSidebar(remoteSidebar))
})
})
},
/**
* properties for managing several requests to load
* the module from various pieces of canvas code.
@ -84,24 +70,18 @@ const RCELoader = {
* @private
*/
loadRCE(cb = () => {}) {
if (!loadingPromise) {
loadingPromise = (
window.ENV.use_rce_enhancements
? import(/* webpackChunkName: "canvas-rce-async-chunk" */ './canvas-rce')
: import(
/* webpackChunkName: "canvas-rce-old-async-chunk" */ './canvas-rce-old-and-a11y-checker'
)
).then(RCE => {
return import(/* webpackChunkName: "canvas-rce-async-chunk" */ './canvas-rce')
.then(RCE => {
this.RCE = RCE
loadEventListeners()
return RCE
})
}
return loadingPromise.then(() => {
this.loadingCallbacks.forEach(loadingCallback => loadingCallback(this.RCE))
this.loadingCallbacks = []
cb(this.RCE)
})
.then(() => {
this.loadingCallbacks.forEach(loadingCallback => loadingCallback(this.RCE))
this.loadingCallbacks = []
// eslint-disable-next-line promise/no-callback-in-promise
cb(this.RCE)
})
},
/**
@ -196,10 +176,10 @@ const RCELoader = {
}
})
// when rce_auto_save flag is removed, remember to default
// the autosave property in RCEWrapper to reasonable values
// TODO: let client pass autosave_enabled in as a prop from the outside
// Assignmens2 student view is going to be doing their own autosave
const autosave = {
enabled: ENV.use_rce_enhancements && ENV.rce_auto_save,
enabled: ENV.rce_auto_save,
maxAge: Number.isNaN(ENV.rce_auto_save_max_age_ms) ? 3600000 : ENV.rce_auto_save_max_age_ms
}

View File

@ -41,9 +41,7 @@ export default class EditorConfig {
* @return {EditorConfig}
*/
constructor(tinymce, inst, width, domId) {
this.new_rce = window.ENV.use_rce_enhancements
this.baseURL = tinymce.baseURL
this.maxButtons = inst.maxVisibleEditorButtons
this.extraButtons = inst.editorButtons
this.instConfig = inst
this.viewportWidth = width
@ -74,22 +72,14 @@ export default class EditorConfig {
? 'elementary-theme'
: 'default-theme',
selector: `#${this.idAttribute}`,
[!this.new_rce && 'toolbar']: this.toolbar(), // handled in RCEWrapper
[!this.new_rce && 'theme']: 'modern',
[!this.new_rce && 'skin']: false,
directionality: getDirection(),
// RCEWrapper includes instructure_equation, so it shouldn't be necessary here
// but if I leave it out equation_spec.rb and new_ui_spec selenium specs fail
// in jenkins (but not locally) and I can't explain why. Doesn't hurt to put it here
plugins: this.new_rce
? 'instructure_equation'
: 'autolink,media,paste,table,lists,textcolor,link,directionality,a11y_checker,wordcount,' +
'instructure_image,instructure_links,instructure_equation,instructure_external_tools,instructure_record',
plugins: ['instructure_equation'],
content_css: window.ENV.url_to_what_gets_loaded_inside_the_tinymce_editor_css,
menubar: this.new_rce ? undefined : true,
init_instance_callback: ed => {
$(`#tinymce-parent-of-${ed.id}`) // eslint-disable-line no-undef
.css('visibility', 'visible')
@ -99,106 +89,4 @@ export default class EditorConfig {
show_media_upload: !!INST.kalturaSettings && !INST.kalturaSettings.hide_rte_button
}
}
/**
* builds the configuration information that decides whether to clump
* up external buttons or not based on the number of extras we
* want to add.
*
* @private
* @return {String} comma delimited set of external buttons
*/
external_buttons() {
let externals = ''
for (let idx = 0; this.extraButtons && idx < this.extraButtons.length; idx++) {
if (this.extraButtons.length <= this.maxButtons || idx < this.maxButtons - 1) {
externals = `${externals} instructure_external_button_${this.extraButtons[idx].id}`
} else if (!externals.match(/instructure_external_button_clump/)) {
externals += ' instructure_external_button_clump'
}
}
return externals
}
/**
* uses externally provided settings to decide which instructure
* plugin buttons to enable, and returns that string of button names.
*
* @private
* @return {String} comma delimited set of non-core buttons
*/
buildInstructureButtons() {
let instructure_buttons = ` instructure_image instructure_equation${
this.new_rce ? ' lti_tool_dropdown' : ''
}`
instructure_buttons += this.external_buttons()
if (
this.instConfig &&
this.instConfig.allowMediaComments &&
this.instConfig.kalturaSettings &&
!this.instConfig.kalturaSettings.hide_rte_button
) {
instructure_buttons += ' instructure_record'
}
const equella_button =
this.instConfig && this.instConfig.equellaEnabled ? ' instructure_equella' : ''
instructure_buttons += equella_button
return instructure_buttons
}
/**
* groups of buttons that are always found together, so updating a config
* name doesn't need to happen 3 places or not work.
* @private
*/
formatBtnGroup =
'bold italic underline forecolor backcolor removeformat alignleft aligncenter alignright'
positionBtnGroup = 'outdent indent superscript subscript bullist numlist'
fontBtnGroup = 'ltr rtl fontsizeselect formatselect check_a11y'
/**
* uses the width to decide how many lines of buttons to break
* up the toolbar over.
*
* @private
* @return {Array<String>} each element is a string of button names
* representing the buttons to appear on the n-th line of the toolbar
*/
balanceButtons(instructure_buttons) {
const instBtnGroup = `table media instructure_links unlink${instructure_buttons}`
let buttons1 = ''
let buttons2 = ''
let buttons3 = ''
if (this.viewportWidth < 359 && this.viewportWidth > 0) {
buttons1 = this.formatBtnGroup
buttons2 = `${this.positionBtnGroup} ${instBtnGroup}`
buttons3 = this.fontBtnGroup
} else if (this.viewportWidth < 1200) {
buttons1 = `${this.formatBtnGroup} ${this.positionBtnGroup}`
buttons2 = `${instBtnGroup} ${this.fontBtnGroup}`
} else {
buttons1 = `${this.formatBtnGroup} ${this.positionBtnGroup} ${instBtnGroup} ${this.fontBtnGroup}`
}
if (this.new_rce) {
return [buttons1, buttons2, buttons3]
} else {
return [buttons1, buttons2, buttons3].map(b => b.split(' ').join(','))
}
}
/**
* builds the custom buttons, and hands them off to be munged
* in with the core buttons and balanced across the toolbar.
*
* @private
* @return {Array<String>} each element is a string of button names
* representing the buttons to appear on the n-th line of the toolbar
*/
toolbar() {
const instructure_buttons = this.buildInstructureButtons()
return this.balanceButtons(instructure_buttons)
}
}

View File

@ -19,7 +19,6 @@
import $ from 'jquery'
import {changeMonth} from '../../jquery/calendar_move' // calendarMonths
import RichContentEditor from '@canvas/rce/RichContentEditor'
import KeyboardShortcuts from '@canvas/tinymce-keyboard-shortcuts'
import '@canvas/datetime' // dateString, datepicker
import '@canvas/forms/jquery/jquery.instructure_forms' // formSubmit, formErrors
import '@canvas/jquery/jquery.instructure_misc_plugins' // ifExists, showIf
@ -46,10 +45,8 @@ function highlightDaysWithEvents() {
wrapper.removeAttr('role')
wrapper.removeAttr('tabindex')
$syllabus.find('tr.date:visible').each(function() {
const date = $(this)
.find('.day_date')
.attr('data-date')
$syllabus.find('tr.date:visible').each(function () {
const date = $(this).find('.day_date').attr('data-date')
events = $mini_month.find(`#mini_day_${date}`)
events.addClass('has_event')
wrapper = events.find('.day_wrapper')
@ -77,10 +74,7 @@ function highlightRelated(related_id, self) {
$syllabus.find('tr.related_event').removeClass('related_event')
if (related_id && $syllabus) {
$syllabus
.find(`tr.related-${related_id}`)
.not(self)
.addClass('related_event')
$syllabus.find(`tr.related-${related_id}`).not(self).addClass('related_event')
}
}
@ -88,16 +82,13 @@ function highlightRelated(related_id, self) {
// Called to bind behaviors to #syllabus after it's rendered.
function bindToSyllabus() {
const $syllabus = $('#syllabus')
$syllabus.on('mouseenter mouseleave', 'tr.date', function(ev) {
$syllabus.on('mouseenter mouseleave', 'tr.date', function (ev) {
let date
if (ev.type === 'mouseenter')
date = $(this)
.find('.day_date')
.attr('data-date')
if (ev.type === 'mouseenter') date = $(this).find('.day_date').attr('data-date')
highlightDate(date)
})
$syllabus.on('mouseenter mouseleave', 'tr.date.detail_list', function(ev) {
$syllabus.on('mouseenter mouseleave', 'tr.date.detail_list', function (ev) {
let related_id = null
if (ev.type === 'mouseenter') {
const classNames = ($(this).attr('class') || '').split(/\s+/)
@ -122,18 +113,13 @@ function selectRow($row) {
$('tr.selected').removeClass('selected')
$row.addClass('selected')
$('html, body').scrollTo($row)
$row
.find('a')
.first()
.focus()
$row.find('a').first().focus()
}
}
function selectDate(date) {
$('.mini_month .day.selected').removeClass('selected')
$('.mini_month')
.find(`#mini_day_${date}`)
.addClass('selected')
$('.mini_month').find(`#mini_day_${date}`).addClass('selected')
}
// Binds to mini calendar dom events
@ -141,17 +127,15 @@ function bindToMiniCalendar() {
const $mini_month = $('.mini_month')
const prev_next_links = $mini_month.find('.next_month_link, .prev_month_link')
prev_next_links.on('click', function(ev) {
prev_next_links.on('click', function (ev) {
ev.preventDefault()
changeMonth($mini_month, $(this).hasClass('next_month_link') ? 1 : -1)
highlightDaysWithEvents()
})
const miniCalendarDayClick = function(ev) {
const miniCalendarDayClick = function (ev) {
ev.preventDefault()
const date = $(ev.target)
.closest('.mini_calendar_day')[0]
.id.slice(9)
const date = $(ev.target).closest('.mini_calendar_day')[0].id.slice(9)
const [year, month, day] = Array.from(date.split('_'))
changeMonth($mini_month, `${month}/${day}/${year}`)
highlightDaysWithEvents()
@ -168,9 +152,7 @@ function bindToMiniCalendar() {
$mini_month.on('focus blur mouseover mouseout', '.day_wrapper', ev => {
let date
if (ev.type !== 'mouseout' && ev.type !== 'blur') {
date = $(ev.target)
.closest('.mini_calendar_day')[0]
.id.slice(9)
date = $(ev.target).closest('.mini_calendar_day')[0].id.slice(9)
}
highlightDate(date)
})
@ -179,10 +161,8 @@ function bindToMiniCalendar() {
ev.preventDefault()
const todayString = $.datepicker.formatDate('yy_mm_dd', new Date())
let $lastBefore
$('tr.date').each(function() {
const dateString = $(this)
.find('.day_date')
.attr('data-date')
$('tr.date').each(function () {
const dateString = $(this).find('.day_date').attr('data-date')
if (dateString) {
if (dateString > todayString) return false
@ -201,7 +181,7 @@ function bindToMiniCalendar() {
}
// Binds to edit syllabus dom events
const bindToEditSyllabus = function(course_summary_enabled) {
const bindToEditSyllabus = function (course_summary_enabled) {
const $course_syllabus = $('#course_syllabus')
$course_syllabus.data('syllabus_body', ENV.SYLLABUS_BODY)
const $edit_syllabus_link = $('.edit_syllabus_link')
@ -212,13 +192,6 @@ const bindToEditSyllabus = function(course_summary_enabled) {
// syllabus_right_side view)
if (!$edit_syllabus_link.length) return
// Add the backbone view for keyboardshortup help here
if (!ENV.use_rce_enhancements) {
$('.toggle_views_link')
.first()
.before(new KeyboardShortcuts().render().$el)
}
function resetToggleLinks() {
$('.toggle_html_editor_link').show()
$('.toggle_rich_editor_link').hide()
@ -228,15 +201,6 @@ const bindToEditSyllabus = function(course_summary_enabled) {
let $course_syllabus_body = $('#course_syllabus_body')
const $course_syllabus_details = $('#course_syllabus_details')
RichContentEditor.initSidebar({
show() {
$('#sidebar_content, #course_show_secondary').hide()
},
hide() {
$('#sidebar_content, #course_show_secondary').show()
}
})
$edit_course_syllabus_form.on('edit', () => {
$edit_course_syllabus_form.show()
$edit_syllabus_link.hide()
@ -280,11 +244,7 @@ const bindToEditSyllabus = function(course_summary_enabled) {
RichContentEditor.callOnRCE($course_syllabus_body, 'toggle')
// hide the clicked link, and show the other toggle link.
// todo: replace .andSelf with .addBack when JQuery is upgraded.
$(ev.currentTarget)
.siblings('.toggle_views_link')
.andSelf()
.toggle()
.focus()
$(ev.currentTarget).siblings('.toggle_views_link').andSelf().toggle().focus()
})
$edit_course_syllabus_form.on('click', '.cancel_button', ev => {

View File

@ -17,9 +17,7 @@
*/
import I18n from 'i18n!ExternalToolsPlugin'
import htmlEscape from 'html-escape'
import './jquery/jquery.dropdownList'
import '@canvas/jquery/jquery.instructure_misc_helpers'
import $ from 'jquery'
// setting ENV.MAX_MRU_LTI_TOOLS can make it easier to test
const MAX_MRU_LTI_TOOLS = ENV.MAX_MRU_LTI_TOOLS || 5
@ -53,98 +51,23 @@ export default {
* complete with title, cmd, image, and classes
*/
buttonConfig(button, editor) {
const useRceEnhancements = ENV.use_rce_enhancements
const config = {
title: button.name,
classes: 'widget btn instructure_external_tool_button'
}
if (useRceEnhancements) {
config.id = button.id
config.onAction = () => {
editor.execCommand(`instructureExternalButton${button.id}`)
this.updateMRUList(button.id)
this.showHideButtons(editor)
}
config.description = button.description
config.favorite = button.favorite
} else {
config.cmd = `instructureExternalButton${button.id}`
}
if (
!useRceEnhancements && // New RCE does not support custom icon classes
button.canvas_icon_class &&
typeof button.canvas_icon_class === 'string'
) {
config.icon = `hack-to-avoid-mce-prefix ${button.canvas_icon_class}`
} else {
// default to image
config.image = button.icon_url
config.id = button.id
config.onAction = () => {
editor.execCommand(`instructureExternalButton${button.id}`)
this.updateMRUList(button.id)
this.showHideButtons(editor)
}
config.description = button.description
config.favorite = button.favorite
config.image = button.icon_url
return config
},
/**
* convert the button clump configuration to
* an associative array where the key is an image tag
* with the name and the value is the thing to do
* when that button gets clicked. This gives us
* a decent structure for mapping click events for
* each dynamically generated button in the button clump
* list.
*
* @param {Array<Hash (representing a button)>} clumpedButtons an array of
* button configs, like the ones passed into "buttonConfig"
* above as parameters
*
* @param {function(Hash), editor} onClickHandler the function that should get
* called when this button gets clicked
*
* @returns {Hash<string,function(Hash)>} the hash we can use
* for generating a dropdown list in jquery
*/
clumpedButtonMapping(clumpedButtons, ed, onClickHandler) {
return clumpedButtons.reduce((items, button) => {
let key
// added data-tool-id='"+ button.id +"' to make elements unique when the have the same name
if (button.canvas_icon_class) {
key = `<i class='${htmlEscape(button.canvas_icon_class)}' data-tool-id='${button.id}'></i>`
} else {
// icon_url is implied
key = `<img src='${htmlEscape(button.icon_url)}' data-tool-id='${button.id}'/>`
}
key += `&nbsp;${htmlEscape(button.name)}`
items[key] = function () {
onClickHandler(button, ed)
}
return items
}, {})
},
/**
* extend the dropdown menu for all the buttons
* clumped up into the "externalButtonClump", and attach
* an event to the editor so that whenever you click
* anywhere else on the editor the dropdown goes away.
*
* @param {jQuery Object} target the Dom element we're attaching
* this dropdown list to
* @param {Hash<string,function(Hash)>} buttons the buttons to put
* into the dropdown list, typically generated from 'clumpedButtonMapping'
* @param {tinymce.Editor} editor the relevant editor for this
* dropdown list, to whom we will listen for any click events
* outside the dropdown menu
*/
attachClumpedDropdown(target, buttons, editor) {
target.dropdownList({options: buttons})
editor.on('click', () => {
target.dropdownList('hide')
})
},
showHideButtons(ed) {
const label = I18n.t('Apps')
const menubutton = ed.$(

View File

@ -40,31 +40,11 @@ describe('buttonConfig()', () => {
}
})
describe('with new RCE', () => {
beforeEach(() => {
window.ENV.use_rce_enhancements = true
})
it('does not set the custom icon class', () => {
expect(subject().icon).toBeUndefined()
})
it('uses the icon url', () => {
expect(subject().image).toEqual(button.icon_url)
})
it('does not set the custom icon class', () => {
expect(subject().icon).toBeUndefined()
})
describe('without new RCE', () => {
beforeEach(() => {
window.ENV.use_rce_enhancements = false
})
it('sets the custom icon and "hack" class', () => {
expect(subject().icon).toEqual('hack-to-avoid-mce-prefix custom-class')
})
it('deso not use the icon url', () => {
expect(subject().image).toBeUndefined()
})
it('uses the icon url', () => {
expect(subject().image).toEqual(button.icon_url)
})
})

View File

@ -0,0 +1,165 @@
/*
* Copyright (C) 2021 - 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 ExternalToolsHelper from '@canvas/tinymce-external-tools/ExternalToolsHelper'
import $ from 'jquery'
describe('ExternalToolsHelper', () => {
describe('buttonConfig', () => {
let fakeEditor
beforeAll(() => {
fakeEditor = {
execCommand: () => {}
}
})
it('transforms button data for tiymce', () => {
const button = {
id: 'b0',
name: 'some tool',
description: 'this is a cool tool',
favorite: true,
icon_url: '/path/to/cool_icon'
}
const result = ExternalToolsHelper.buttonConfig(button, fakeEditor)
expect(result).toEqual(
expect.objectContaining({
id: button.id,
description: button.description,
title: button.name,
image: expect.stringContaining(button.icon_url)
})
)
})
it('uses button icon_url if there is no icon_class', () => {
const button = {
id: 'b0',
name: 'some tool',
description: 'this is a cool tool',
favorite: true,
icon_url: 'path/to/icon'
}
const result = ExternalToolsHelper.buttonConfig(button, fakeEditor)
expect(result).toEqual(
expect.objectContaining({
id: button.id,
description: button.description,
title: button.name,
image: button.icon_url
})
)
expect(result).toEqual(
expect.not.objectContaining({
icon: expect.anything()
})
)
})
})
describe('showHideButtons', () => {
let fakeEditor, button, menuButton
beforeAll(() => {
const edContainer = document.createElement('div')
document.body.appendChild(edContainer)
fakeEditor = {
$,
editorContainer: edContainer
}
})
beforeEach(() => {
button = document.createElement('div')
button.setAttribute('class', 'tox-tbtn')
button.setAttribute('aria-label', 'Apps')
button.setAttribute('style', 'display: flex')
button.innerHTML = 'Apps'
fakeEditor.editorContainer.appendChild(button)
menuButton = document.createElement('div')
menuButton.setAttribute('class', 'tox-tbtn--select')
menuButton.setAttribute('aria-label', 'Apps')
menuButton.setAttribute('style', 'display: flex')
menuButton.innerHTML = 'Apps'
fakeEditor.editorContainer.appendChild(menuButton)
})
afterEach(() => {
fakeEditor.editorContainer.innerHTML = ''
})
it('shows button if there is no MRU', () => {
ExternalToolsHelper.showHideButtons(fakeEditor)
expect(button.getAttribute('aria-hidden')).toEqual('false')
expect(button.style.display).toEqual('flex')
expect(menuButton.getAttribute('aria-hidden')).toEqual('true')
expect(menuButton.style.display).toEqual('none')
})
it('shows MRU button if there is an MRU', () => {
window.localStorage.setItem('ltimru', 'anything')
ExternalToolsHelper.showHideButtons(fakeEditor)
expect(button.getAttribute('aria-hidden')).toEqual('true')
expect(button.style.display).toEqual('none')
expect(menuButton.getAttribute('aria-hidden')).toEqual('false')
expect(menuButton.style.display).toEqual('flex')
})
})
describe('updateMRUList', () => {
it('deals with malformed saved data', () => {
window.localStorage.setItem('ltimru', 'not what is expected')
expect(() => {
ExternalToolsHelper.updateMRUList(1)
}).not.toThrow()
})
it('creates the MRU list', () => {
ExternalToolsHelper.updateMRUList(1)
expect(window.localStorage.getItem('ltimru')).toEqual('[1]')
})
it('adds to the MRU list', () => {
window.localStorage.setItem('ltimru', '[1]')
ExternalToolsHelper.updateMRUList(2)
expect(JSON.parse(window.localStorage.getItem('ltimru'))).toEqual([2, 1])
})
it('does not add a duplicate to the MRU list', () => {
window.localStorage.setItem('ltimru', '[2, 1]')
ExternalToolsHelper.updateMRUList(1)
expect(JSON.parse(window.localStorage.getItem('ltimru'))).toEqual([2, 1])
})
it('limits the MRU list to the max length', () => {
window.localStorage.setItem('ltimru', '[1,2,3,4,5]')
ExternalToolsHelper.updateMRUList(6)
expect(JSON.parse(window.localStorage.getItem('ltimru'))).toEqual([6, 1, 2, 3, 4])
})
})
})

View File

@ -0,0 +1,108 @@
/*
* Copyright (C) 2021 - 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 ExternalToolsPlugin from '@canvas/tinymce-external-tools'
import ExternalToolsHelper from '@canvas/tinymce-external-tools/ExternalToolsHelper'
describe('initializeExternalTools', () => {
let fakeEditor
beforeEach(() => {
global.INST = {
editorButtons: [
{id: 'button_id'},
{id: 'fav_id', favorite: true, name: 'fav tool', canvas_icon_class: 'fav-tool'}
]
}
fakeEditor = {
getContent() {},
selection: {
getContent() {}
},
addCommand: jest.fn(),
ui: {
registry: {
addButton: jest.fn(),
addMenuButton: jest.fn(),
addIcon: jest.fn(),
addNestedMenuItem: jest.fn()
}
}
}
})
it('adds Apps button to the toolbar', () => {
ExternalToolsPlugin.init(fakeEditor, 'some.fake.url', INST)
expect(fakeEditor.ui.registry.addButton).toHaveBeenCalledWith('lti_tool_dropdown', {
onAction: expect.any(Function),
icon: 'lti',
tooltip: 'Apps',
onSetup: expect.any(Function)
})
})
it('adds MRU menu button to the toolbar', () => {
ExternalToolsPlugin.init(fakeEditor, 'some.fake.url', INST)
expect(fakeEditor.ui.registry.addMenuButton).toHaveBeenCalledWith('lti_mru_button', {
tooltip: 'Apps',
icon: 'lti',
fetch: expect.any(Function),
onSetup: expect.any(Function)
})
})
it('adds favorite tool button to toolbar', () => {
ExternalToolsPlugin.init(fakeEditor, 'some.fake.url', INST)
const favButtonConfig = ExternalToolsHelper.buttonConfig(INST.editorButtons[1])
expect(fakeEditor.ui.registry.addButton).toHaveBeenCalledWith(
'instructure_external_button_fav_id',
{
onAction: expect.any(Function),
tooltip: favButtonConfig.title,
icon: favButtonConfig.icon,
title: favButtonConfig.title
}
)
})
it('adds external tools item to the menu bar', () => {
ExternalToolsPlugin.init(fakeEditor, 'some.fake.url', INST)
expect(fakeEditor.ui.registry.addNestedMenuItem).toHaveBeenCalledWith('lti_tools_menuitem', {
text: 'Apps',
icon: 'lti',
getSubmenuItems: expect.any(Function)
})
})
it('adds the command to open each tool', () => {
ExternalToolsPlugin.init(fakeEditor, 'some.fake.url', INST)
expect(fakeEditor.addCommand).toHaveBeenCalledWith(
`instructureExternalButton${INST.editorButtons[0].id}`,
expect.any(Function)
)
expect(fakeEditor.addCommand).toHaveBeenCalledWith(
`instructureExternalButton${INST.editorButtons[1].id}`,
expect.any(Function)
)
})
})

View File

@ -21,7 +21,6 @@ import $ from 'jquery'
import htmlEscape from 'html-escape'
import ExternalToolsHelper from './ExternalToolsHelper'
import iframeAllowances from '@canvas/external-apps/iframeAllowances'
import Links from '@canvas/tinymce-links'
import React from 'react'
import ReactDOM from 'react-dom'
@ -33,7 +32,6 @@ const TRANSLATIONS = {
const ExternalToolsPlugin = {
init(ed, url, _INST) {
Links.initEditor(ed)
if (!_INST || !_INST.editorButtons || !_INST.editorButtons.length) {
return
}
@ -55,68 +53,26 @@ const ExternalToolsPlugin = {
deepLinkingOrigin={ENV.DEEP_LINKING_POST_MESSAGE_ORIGIN}
/>,
dialogContainer,
function() {
function () {
dialog = this
}
)
})
const clumpedButtons = []
const ltiButtons = []
for (let idx = 0; _INST.editorButtons && idx < _INST.editorButtons.length; idx++) {
const current_button = _INST.editorButtons[idx]
// eslint-disable-next-line no-loop-func
const openDialog = () => dialog.open(current_button)
if (ENV.use_rce_enhancements) {
ltiButtons.push(ExternalToolsHelper.buttonConfig(current_button, ed))
ed.addCommand(`instructureExternalButton${current_button.id}`, openDialog)
} else if (
_INST.editorButtons.length > _INST.maxVisibleEditorButtons &&
idx >= _INST.maxVisibleEditorButtons - 1
) {
clumpedButtons.push(current_button)
} else {
ed.addCommand(`instructureExternalButton${current_button.id}`, openDialog)
ed.addButton(
`instructure_external_button_${current_button.id}`,
ExternalToolsHelper.buttonConfig(current_button, ed)
)
}
ltiButtons.push(ExternalToolsHelper.buttonConfig(current_button, ed))
ed.addCommand(`instructureExternalButton${current_button.id}`, openDialog)
}
if (ltiButtons.length && ENV.use_rce_enhancements) {
if (ltiButtons.length) {
buildToolsButton(ed, ltiButtons)
buildFavoriteToolsButtons(ed, ltiButtons)
buildMRUMenuButton(ed, ltiButtons)
buildMenubarItem(ed, ltiButtons)
}
if (clumpedButtons.length) {
const handleClick = function() {
const items = ExternalToolsHelper.clumpedButtonMapping(clumpedButtons, ed, button =>
dialog.open(button)
)
ExternalToolsHelper.attachClumpedDropdown($(`#${this._id}`), items, ed)
}
if (ENV.use_rce_enhancements) {
ed.ui.registry.addButton('instructure_external_button_clump', {
title: TRANSLATIONS.more_external_tools,
image: '/images/downtick.png',
onAction: handleClick
})
} else {
ed.addButton('instructure_external_button_clump', {
title: TRANSLATIONS.more_external_tools,
image: '/images/downtick.png',
onkeyup(event) {
if (event.keyCode === 32 || event.keyCode === 13) {
event.stopPropagation()
handleClick.call(this)
}
},
onclick: handleClick
})
}
}
}
}

View File

@ -1,112 +0,0 @@
#
# Copyright (C) 2014 - 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 I18n from 'i18n!editor.keyboard_shortcuts'
import $ from 'jquery'
import Backbone from '@canvas/backbone'
import Template from '../../jst/index.handlebars'
HELP_KEYCODES = [
48 # regular 0 (not numpad 0)
119 # F8
]
##
# A dialog that lists available keybindings for TinyMCE.
#
# The dialog can be launched by pressing ALT+0, or by clicking a little ? icon
# in the editor action bar.
KeyboardShortcuts = Backbone.View.extend
className: 'tinymce-keyboard-shortcuts-toggle'
tagName: 'a'
events:
'click': 'openDialog'
keybindings: [
{
key: 'ALT+F9',
description: I18n.t('keybindings.open_menubar', 'Open the editor\'s menubar')
},
{
key: 'ALT+F10',
description: I18n.t('keybindings.open_toolbar', 'Open the editor\'s toolbar')
},
{
key: 'ESC',
description: I18n.t('keybindings.close_submenu', 'Close menu or dialog, also gets you back to editor area')
},
{
key: 'TAB/Arrows',
description: I18n.t('keybindings.navigate_toolbar', 'Navigate left/right through menu/toolbar')
},
{
key: 'ALT+F8',
description: I18n.t('Open this keyboard shortcuts dialog')
}
]
template: Template
initialize: ->
this.el.href = '#' # for keyboard accessibility
$(this.el).attr("title", I18n.t('dialog_title', 'Keyboard Shortcuts'))
$('<i class="icon-keyboard-shortcuts" aria-hidden="true" />').appendTo(this.el)
$('<span class="screenreader-only" />')
.text(I18n.t('dialog_title', 'Keyboard Shortcuts'))
.appendTo(this.el)
render: () ->
templateData = {
keybindings: this.keybindings
}
this.$dialog = $(this.template(templateData)).dialog({
title: I18n.t('dialog_title', 'Keyboard Shortcuts'),
width: 600,
resizable: true
autoOpen: false
})
@bindEvents()
return this
bindEvents: ()->
unless ENV.use_rce_enhancements
$(document).on('keyup.tinymce_keyboard_shortcuts', @openDialogByKeybinding.bind(this))
#special event for keyups in the editor iframe, fired from "wrapInitCb.js"
$(document).on('editorKeyUp', ((e, originalEvent)->
@openDialogByKeybinding(originalEvent)
).bind(this))
remove: () ->
$(document).off('keyup.tinymce_keyboard_shortcuts')
$(document).off('editorKeyUp')
this.$dialog.dialog('destroy')
openDialog: ->
unless this.$dialog.dialog('isOpen')
this.$dialog.dialog('open')
openDialogByKeybinding: (e) ->
if HELP_KEYCODES.indexOf(e.keyCode) > -1 && e.altKey
this.openDialog()
export default KeyboardShortcuts

View File

@ -1,8 +0,0 @@
<ul class="tinymce-keyboard-shortcuts">
{{#each keybindings}}
<li>
<code>{{key}}</code>
<span>{{description}}</span>
</li>
{{/each}}
</ul>

View File

@ -1,4 +0,0 @@
{
"brandableCSSBundle": "jst/editor/KeyboardShortcuts",
"i18nScope": "editor.keyboard_shortcuts"
}

View File

@ -1,7 +0,0 @@
{
"name": "@canvas/tinymce-keyboard-shortcuts",
"private": true,
"version": "1.0.0",
"author": "neme",
"main": "./backbone/views/index.coffee"
}

View File

@ -1,471 +0,0 @@
/*
* 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/>.
*/
import $ from 'jquery'
import htmlEscape from 'html-escape'
import LinkableEditor from './linkable_editor'
import {send} from '@canvas/rce/RceCommandShim'
import {findLinkForService, getUserServices} from '@canvas/services/findLinkForService'
import '@canvas/jquery/jquery.instructure_misc_helpers'
import 'jqueryui/dialog'
import '@canvas/jquery/jquery.instructure_misc_plugins'
import YouTubeApi from './youtube_api'
// TODO: Allow disabling of inline media as well. Right now
// the link is just '#' so disabling it actually ruins it. It'd
// be nice if the link were a URL to download the media file.
const inlineContentClasses = ['instructure_scribd_file']
// Only allow non-intrusive types to be auto-opened (i.e. don't
// allow auto-playing of video files on page load)
const autoShowContentClasses = ['instructure_scribd_file']
const initializedEditors = new WeakMap()
/**
* Finds the closest img tag and extracts the 'src' attribute,
* which then gets pulled into a new string as the src attribute
* for an img tag to be written through into a tinymce IFrame editor.
*
* @param {JQuery Object} target the dom element to grab the nearest img to
*
* @returns {string} an image tag string with the src pulled in
*/
function buttonToImg(target) {
const src = target.closest('img').attr('src')
return "<img src='" + htmlEscape(src) + "'/>"
}
/**
* snapshots the current state of the editor (nodeChanged) so that a refocus
* later will git the right selection involved, then wraps the editor
* in a linkable editor which knows some of the selection state
* at the time of link activation and can proxy the actual link call
* through to our custom linkification method in tinymce.editor_box
*
* @param {tinymce.Editor} editor the instance we want to linkify something
* within
*
* @returns {LinkableEditor}
*/
function prepEditorForDialog(editor) {
editor.nodeChanged()
return new LinkableEditor(editor)
}
/**
* When inserting a link into the editor, we only want to have control
* classes on them if they've been explicitly asked for through
* the UI checkboxes. This function is used to transform
* link attributes at the time of edit/insertion. It strips off any
* control classes that are prexisting, and then only adds them on
* if the checkboxes are populated.
*
* @param {String} priorClasses existing class list from the link
* element (which would be empty for new links, populated for editing
* links)
*
* @param {JQuery Object} box the dialog box the UI for creating a link
* is within
*
* @returns {String} a transformed class list based on the rules listed
* above
*/
function buildLinkClasses(priorClasses, $box) {
let classes = priorClasses.replace(/(auto_open|inline_disabled)/g, '')
if ($box.find('.auto_show_inline_content').attr('checked')) {
classes += ' auto_open'
}
if ($box.find('.disable_inline_content').attr('checked')) {
classes += ' inline_disabled'
}
return classes
}
/**
* this takes the dialog box that provides the form for inputting
* a link target, clears off any submit callbacks that are currently
* attached to it, and attaches a *new* submit callback to populate
* link data into the correct editor.
*
* @param {JQuery Object} box this is the dialog box div we want to address
*
* @param {LinkableEditor} linkableEditor the wrapped editor that knows how
* to attach links to selected content
*
* @param {function} fetchClasses I hate that we need this parameter.
* the priorClasses state is maintained in a pseudo-global string
* that gets modulated throughout the life of this plugin. That
* means just passing it in at the time we do the binding gives us
* a blank value. The callback delays the query until the submit
* button fires, by which time priorClasses might be populated. The
* real solution here is to de-global-ify the priorClasses variable,
* but that refactor is for another day.
*
* @param {function} done any behavior you want to happen after the link
* has been inserted into the editor
*/
function bindLinkSubmit($box, linkableEditor, fetchClasses, done) {
const $form = $box.find('#instructure_link_prompt_form')
$form.off('submit')
$form.on('submit', function(event) {
event.preventDefault()
event.stopPropagation()
const $editor = $box.data('editor')
const text = $(this)
.find('.prompt')
.val()
const alt = $box.find('.inst-link-preview-alt input').val()
const classes = buildLinkClasses(fetchClasses.call(), $box)
const dataAttrs = {'preview-alt': alt}
$box.dialog('close')
linkableEditor.createLink(text, classes, dataAttrs)
done.call()
})
}
function renderDialog(ed) {
const linkableEditor = prepEditorForDialog(ed)
const $editor = linkableEditor.getEditor()
var $box = $('#instructure_link_prompt')
let priorClasses = ''
$box
.removeClass('for_inline_content')
.find('.disable_enhancement')
.hide()
.end()
.find('.auto_show')
.hide()
.end()
.find('.insert_button')
.text('Insert Link')
.end()
.find('.disable_inline_content')
.attr('checked', false)
.end()
.find('.auto_show_inline_content')
.attr('checked', false)
if ($box.length == 0) {
var $box = $(document.createElement('div'))
getUserServices('BookmarkService', function(data) {
const $editor = $box.data('editor')
const $services = $("<div style='text-align: left; margin-left: 20px;'/>")
let service, $service
for (const idx in data) {
service = data[idx].user_service
if (service) {
$service = $("<a href='#' class='bookmark_service no-hover'/>")
$service.addClass(service.service)
$service.data('service', service)
$service.attr('title', 'Find links using ' + service.service)
const $img = $('<img/>')
$img.attr('src', '/images/' + service.service + '_small_icon.png')
$service.append($img)
$service.click(function(event) {
event.preventDefault()
$('#instructure_link_prompt').dialog('close')
findLinkForService($(this).data('service').service, data => {
$('#instructure_link_prompt').dialog('close')
send($editor, 'create_link', {
title: data.title,
url: data.url,
classes: priorClasses
})
})
})
$services.append($service)
$services.append('&nbsp;&nbsp;')
}
}
$box.find('#instructure_link_prompt_form').after($services)
})
$box
.append(
"<p><em>This will make the selected text a link, or insert a new link if nothing is selected.</em></p> <label for='instructure_link_prompt_form_input'>Paste or type a url or wiki page in the box below:</label><form id='instructure_link_prompt_form' class='form-inline'><input type='text' id='instructure_link_prompt_form_input' class='prompt' class='btn' value='http://'/> <button type='submit' class='insert_button btn'>Insert Link</button></form>"
)
.append("<div class='actions'></div><div class='clear'></div>")
.append(
'<div class="inst-link-preview-alt" style="display: none;"><label>Alt text for inline preview: <input type="text" style="display: block;" /></label></div>'
)
.append(
"<div class='disable_enhancement' style='display: none;'><input type='checkbox' class='disable_inline_content' id='disable_inline_content'/><label for='disable_inline_content'> Disable inline previews for this link</label></div>"
)
.append(
"<div class='auto_show' style='display: none;'><input type='checkbox' class='auto_show_inline_content' id='auto_show_inline_content'/><label for='auto_show_inline_content'> Auto-open the inline preview for this link</label></div>"
)
$box.find('.disable_inline_content').change(function() {
if ($(this).attr('checked')) {
$box.find('.auto_show_inline_content').attr('checked', false)
}
$box
.find('.auto_show')
.showIf(!$(this).attr('checked') && $box.hasClass('for_inline_content_can_auto_show'))
})
$box.find('.actions').delegate('.embed_image_link', 'click', event => {
const $editor = $box.data('editor')
const $target = $(event.target)
event.preventDefault()
send($editor, 'insert_code', buttonToImg($target))
$box.dialog('close')
})
// http://img.youtube.com/vi/BOegH4uYe-c/3.jpg
$box.find('.actions').delegate('.embed_youtube_link', 'click', event => {
event.preventDefault()
$box.find('#instructure_link_prompt_form').triggerHandler('submit')
})
$box.find('#instructure_link_prompt_form .prompt').bind('change keyup', function() {
const $alt = $box.find('.inst-link-preview-alt')
$alt.hide()
$('#instructure_link_prompt .actions').empty()
const val = $(this).val()
// If the user changes the link then it should no longer
// have inline content classes or be configurable
const data = $box.data('original_data')
if (!data || val != data.url) {
$box.removeClass('for_inline_content').removeClass('for_inline_content_can_auto_show')
const re = new RegExp('(' + inlineContentClasses.join('|') + ')', 'g')
priorClasses = priorClasses.replace(re, '')
} else {
$box
.toggleClass('for_inline_content', data.for_inline_content)
.toggleClass('for_inline_content_can_auto_show', data.for_inline_content_can_auto_show)
.find('.disable_enhancement')
.showIf(data.for_inline_content)
.end()
.find('.auto_show')
.showIf(data.for_inline_content_can_auto_show)
priorClasses = data.prior_classes
}
let hideDisableEnhancement = !$box.hasClass('for_inline_content')
const hideShowInline = !$box.hasClass('for_inline_content_can_auto_show')
if (val.match(/\.(gif|png|jpg|jpeg)$/)) {
var $div = $(document.createElement('div'))
$div.css('textAlign', 'center')
var $img = $(document.createElement('img'))
$img.attr('src', val)
$img.addClass('embed_image_link')
$img.css('cursor', 'pointer')
const img = new Image()
img.src = val
var checkCompletion = function() {
if (img.complete) {
if (img.height < 100 || (img.height > 100 && img.height < 200)) {
$img.height(img.height)
}
} else {
setTimeout(checkCompletion, 500)
}
}
setTimeout(checkCompletion, 500)
$img.height(100)
$img.attr('title', 'Click to Embed the Image')
$div.append($img)
$('#instructure_link_prompt .actions').append($div)
} else if (val.match(INST.youTubeRegEx)) {
$alt.show()
const id = $.youTubeID(val) // val.match(INST.youTubeRegEx)[2];
var $div = $(document.createElement('div'))
$div.css('textAlign', 'center')
if (
!$box.find('.disable_inline_content').attr('checked') &&
$box.hasClass('for_inline_content_can_auto_show')
) {
$box.find('.auto_show').show()
}
hideDisableEnhancement = false
$box.find('.disable_enhancement').show()
var $img = $(document.createElement('img'))
$img.attr('src', 'http://img.youtube.com/vi/' + id + '/2.jpg')
$img.css({
paddingLeft: 100,
background: 'url(/images/youtube_logo.png) no-repeat left center',
height: 90,
display: 'inline-block'
})
$img.attr('alt', val)
$img.addClass('embed_youtube_link')
$img.css('cursor', 'pointer')
$img.attr('title', 'Click to Embed YouTube Video')
$div.append($img)
$('#instructure_link_prompt .actions').append($div)
}
if (hideDisableEnhancement) {
$box.find('.disable_enhancement').hide()
$box.find('.disable_inline_content').attr('checked', false)
}
if (hideShowInline) {
$box.find('.auto_show').hide()
$box.find('.auto_show_inline_content').attr('checked', false)
}
})
$box.attr('id', 'instructure_link_prompt')
$('body').append($box)
} // END of if($box.length == 0), everything above only happens once
// Bind in the callback to fire when the user has entered
// the link target they want and hit "submit"
const fetchClasses = function() {
return priorClasses
}
const done = function() {
updateLinks(ed, true)
}
bindLinkSubmit($box, linkableEditor, fetchClasses, done)
$box.data('editor', $editor)
$box.data('original_data', null)
let e = ed.selection.getNode()
while (e.nodeName != 'A' && e.nodeName != 'BODY' && e.parentNode) {
e = e.parentNode
}
const $a = e.nodeName == 'A' ? $(e) : null
if ($a) {
$box
.find('.prompt')
.val($a.attr('href'))
.change()
$box.find('.inst-link-preview-alt input').val($a.data('preview-alt'))
priorClasses = ($a.attr('class') || '').replace(/youtube_link_to_box/, '')
var re = new RegExp('(' + inlineContentClasses.join('|') + ')')
if (($a.attr('class') || '').match(re)) {
$box
.addClass('for_inline_content')
.find('.disable_enhancement')
.show()
}
var re = new RegExp('(' + autoShowContentClasses.join('|') + ')')
if (($a.attr('class') || '').match(re)) {
$box
.addClass('for_inline_content_can_auto_show')
.find('.auto_show')
.show()
}
$box.data('original_data', {
url: $a.attr('href'),
for_inline_content: $box.hasClass('for_inline_content'),
for_inline_content_can_auto_show: $box.hasClass('for_inline_content_can_auto_show'),
prior_classes: priorClasses,
preview_alt: $a.data('preview-alt')
})
$box
.find('.disable_inline_content')
.attr('checked', $a.hasClass('inline_disabled'))
.triggerHandler('change')
$box
.find('.auto_show_inline_content')
.attr('checked', $a.hasClass('auto_open'))
.triggerHandler('change')
$box.find('.insert_button').text('Update Link')
} else {
$box
.find('.prompt')
.val('')
.change()
}
$box.dialog({
width: 425,
height: 'auto',
title: 'Link to Website URL',
open() {
$(this)
.find('.prompt')
.focus()
.select()
}
})
}
function updateLinks(ed, arg) {
updateLinks.counter = updateLinks.counter || 0
if (arg == true && updateLinks.counter != 0) {
updateLinks.counter = (updateLinks.counter + 1) % 5
} else {
$(ed.getBody())
.find('a')
.each(function() {
const yt_api = new YouTubeApi()
const $link = $(this)
if (
!ENV.use_rce_enhancements &&
$link.attr('href') &&
!$link.hasClass('inline_disabled') &&
$link.attr('href').match(INST.youTubeRegEx)
) {
const yttFailCnt = +$link.attr('data-ytt-failcnt') || 0
$link.addClass('youtube_link_to_box')
if ($link.text() === $link.attr('href') && yttFailCnt < 1) {
yt_api.titleYouTubeText($link)
}
}
})
}
}
function initEditor(ed) {
if (initializedEditors.get(ed) || ed.on === undefined) {
return
}
ed.on('PreProcess', function(event) {
$(event.node)
.find('a.youtube_link_to_box')
.removeClass('youtube_link_to_box')
$(event.node)
.find('img.iframe_placeholder')
.each(function() {
const $holder = $(this)
const $frame = $('<iframe/>')
const height = $holder.attr('height') || $holder.css('height')
const width = $holder.hasClass('fullWidth')
? '100%'
: $holder.attr('width') || $holder.css('width')
$holder.attr('width', width)
$holder.css('width', width)
$frame.attr('src', $holder.attr('rel'))
$frame.attr('style', $holder.attr('_iframe_style'))
if (!$frame[0].style.height.length) {
$frame.attr('height', height)
$frame.css('height', height)
}
if (!$frame[0].style.width.length) {
$frame.attr('width', width)
$frame.css('width', width)
}
$(this).after($frame)
$(this).remove()
})
})
ed.on('change', () => {
updateLinks(ed)
})
ed.on('SetContent', () => {
updateLinks(ed, 'contentJustSet')
})
initializedEditors.set(ed, true)
}
export default {
buttonToImg,
prepEditorForDialog,
buildLinkClasses,
bindLinkSubmit,
renderDialog,
updateLinks,
initEditor
}

View File

@ -1,86 +0,0 @@
/*
* Copyright (C) 2015 - 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 $ from 'jquery'
import {send} from '@canvas/rce/RceCommandShim'
/**
* This is not yet a complete extraction, but the idea is to continue
* moving direct interactions with the relevant tinymce editor
* out of the link plugin itself and into this proxy object.
*
* The need first arose in response to an IE11 bug where some state
* (specifically the currently selected content) needed to be extracted
* and held onto at the time the link modal is generated, because IE11
* loses the editor carat on a modal activation. Rather than
* use variables with very broad scope in the plugin itself to capture the
* state at one point and use in another, this hides the temporary
* persistance inside a kind of decorator.
*
* @param {tinymce.Editor} editor the tinymce instance we want
* to add links to
* @param {jquery.Object} $editorEl an optional override for the editor target
* that can be found in normal circumstances by calling "getEditor"
*/
const LinkableEditor = function(editor, $editorEl) {
this.id = editor.id
this.selectedContent = editor.selection.getContent()
this.selectionDetails = {
node: editor.selection.getNode(),
range: editor.selection.getRng()
}
this.$editorEl = $editorEl
/**
* Builds a jquery object wrapping the target text area for the
* wrapped tinymce editor. Can be overridden in the constructor with
* an optional second parameter.
*
* @returns {jquery.Object}
*/
this.getEditor = function() {
if (this.$editorEl !== undefined) {
return this.$editorEl
}
return $('#' + this.id)
}
/**
* proxies through a call to our jquery extension that puts new link
* html into an existing tinymce editor. Specifically useful
* because of the "selectedContent" and "selectedRange" which are stored
* at the time the link creation dialog is created (this is important
* because in IE11 that information is lost as soon as the modal dialog
* comes up)
*
* @param {String} text the interior content for the a tag
* @param {String} classes any css classes to apply to the new link
* @param {Object} [dataAttrs] key value pairs for link data attributes
*/
this.createLink = function(text, classes, dataAttrs) {
send(this.getEditor(), 'create_link', {
url: text,
classes,
selectedContent: this.selectedContent,
dataAttributes: dataAttrs,
selectionDetails: this.selectionDetails
})
}
}
export default LinkableEditor

View File

@ -1,7 +0,0 @@
{
"name": "@canvas/tinymce-links",
"private": true,
"version": "1.0.0",
"author": "neme",
"main": "./index.js"
}

View File

@ -1,53 +0,0 @@
/*
* 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/>.
*/
import $ from 'jquery'
export default class YouTubeApi {
fetchYouTubeTitle(id, cb) {
const jwt = ENV.JWT
const appHost = ENV.RICH_CONTENT_APP_HOST
const url = `//${appHost}/api/youtube_title?vid_id=${id}`
$.ajax({
headers: {Authorization: `Bearer ${jwt}`},
url
})
.success(data => {
if (data.id === id) {
cb(data.title)
}
})
.error(err => {
cb(null, err)
})
}
titleYouTubeText($link) {
const id = $.youTubeID($link.attr('href'))
this.fetchYouTubeTitle(id, (vidTitle, err) => {
if (err) {
console.error(`failed to get video title from youtube for "${id}":`, err.responseText)
const yttFailCnt = (+$link.attr('data-ytt-failcnt') || 0) + 1
$link.attr('data-ytt-failcnt', yttFailCnt)
} else {
$link.text(vidTitle)
$link.attr('data-preview-alt', vidTitle)
}
})
}
}

View File

@ -1,61 +0,0 @@
/*
* Copyright (C) 2015 - 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 $ from 'jquery'
import htmlEscape from 'html-escape'
import {send} from '@canvas/rce/RceCommandShim'
import '@canvas/media-comments'
const mediaEditorLoader = {
insertCode(ed, mediaCommentId, mediaType, title) {
const $editor = $('#' + ed.id)
const linkCode = this.makeLinkHtml(mediaCommentId, mediaType, title)
send($editor, 'insert_code', linkCode)
},
makeLinkHtml(mediaCommentId, mediaType, title) {
return $('<a />')
.attr({href: `/media_objects/${htmlEscape(mediaCommentId)}`})
.addClass('instructure_inline_media_comment')
.addClass(`${htmlEscape(mediaType || 'video')}_comment`)
.attr({id: `media_comment_${htmlEscape(mediaCommentId)}`})
.attr({'data-alt': htmlEscape(title)})
.text('this is a media comment')[0].outerHTML
},
getComment(ed, mediaCommentId) {
return $(ed.getBody()).find('#media_comment_' + mediaCommentId + ' + br')[0]
},
collapseMediaComment(ed, mediaCommentId) {
const commentDiv = this.getComment(ed)
ed.selection.select(commentDiv)
ed.selection.collapse(true)
},
commentCreatedCallback(ed, mediaCommentId, mediaType, title) {
this.insertCode(ed, mediaCommentId, mediaType, title)
this.collapseMediaComment(ed, mediaCommentId)
},
insertEditor(ed) {
$.mediaComment('create', 'any', this.commentCreatedCallback.bind(this, ed))
}
}
export default mediaEditorLoader

View File

@ -1,7 +0,0 @@
{
"name": "@canvas/tinymce-record",
"private": true,
"version": "1.0.0",
"author": "neme",
"main": "./index.js"
}

View File

@ -24,9 +24,7 @@ import ValidatedFormView from '@canvas/forms/backbone/views/ValidatedFormView.co
import WikiPageDeleteDialog from './WikiPageDeleteDialog'
import WikiPageReloadView from './WikiPageReloadView'
import I18n from 'i18n!pages'
import KeyboardShortcuts from '@canvas/tinymce-keyboard-shortcuts'
import DueDateCalendarPicker from '@canvas/due-dates/react/DueDateCalendarPicker'
import {send} from '@canvas/rce/RceCommandShim'
import '@canvas/datetime'
RichContentEditor.preloadRemoteModule()
@ -74,7 +72,6 @@ export default class WikiPageEditView extends ValidatedFormView {
toJSON() {
let IS
const json = super.toJSON(...arguments)
json.use_rce_enhancements = ENV.use_rce_enhancements
json.IS = IS = {
TEACHER_ROLE: false,
@ -175,7 +172,6 @@ export default class WikiPageEditView extends ValidatedFormView {
this.$studentTodoAtContainer.hide()
}
RichContentEditor.initSidebar()
RichContentEditor.loadNewEditor(this.$wikiPageBody, {focus: true, manageParent: true})
this.checkUnsavedOnLeave = true
@ -183,11 +179,7 @@ export default class WikiPageEditView extends ValidatedFormView {
if (!this.firstRender) {
this.firstRender = true
$(() =>
$('[autofocus]:not(:focus)')
.eq(0)
.focus()
)
$(() => $('[autofocus]:not(:focus)').eq(0).focus())
}
this.reloadPending = false
@ -208,8 +200,6 @@ export default class WikiPageEditView extends ValidatedFormView {
})
this.reloadView.on('reload', () => this.render())
this.reloadView.pollForChanges()
return this.$helpDialog.html(new KeyboardShortcuts().render().$el)
}
destroyEditor() {
@ -224,11 +214,7 @@ export default class WikiPageEditView extends ValidatedFormView {
RichContentEditor.callOnRCE(this.$wikiPageBody, 'toggle')
// hide the clicked link, and show the other toggle link.
// todo: replace .andSelf with .addBack when JQuery is upgraded.
$(event.currentTarget)
.siblings('a')
.andSelf()
.toggle()
.focus()
$(event.currentTarget).siblings('a').andSelf().toggle().focus()
}
// Validate they entered in a title.

View File

@ -11,19 +11,6 @@
{{else}}
<h2>{{title}}</h2>
{{/if}}
{{#unless content_is_locked}}
{{#unless use_rce_enhancements}}
<div class="switch_views_container">
<div class="help_dialog"></div>
<a href="#" class="switch_views">
{{#t "#editor.switch_editor_html"}}HTML Editor{{/t}}
</a>
<a href="#" class="switch_views" style="display:none;">
{{#t "#editor.switch_editor_rich_text"}}Rich Content Editor{{/t}}
</a>
</div>
{{/unless}}
{{/unless}}
</div>
{{#if content_is_locked}}

1526
yarn.lock

File diff suppressed because it is too large Load Diff