Add video options tray to the rce
closes COREFE-315 test plan: - load a page with the RCE and have a video in it - click on the video > expect the Options popup toolbar button - click it > expect the video options tray to open > expect the title to default to the video's file name > expect the default size to be Large (400px on the long side) - delete the title > expect the Done button to be disabled - set a new title - change the size to Custom - delete the width or height > expect an error message and the Done button to be disabled - enter a new size, or select one from the dropdown - click Done > expect the video to be resized to the new size - if you're using a screenreader, expect the video to be announced as "Video player for {your title}" Change-Id: Id7e29520cc91c02645b92d666216e64f6619bbbb Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/211355 Product-Review: Lauren Williams <lcwilliams@instructure.com> Tested-by: Jenkins Reviewed-by: Clay Diffrient <cdiffrient@instructure.com> QA-Review: Jeremy Putnam <jeremyp@instructure.com>
This commit is contained in:
parent
518a0aa6fe
commit
67e551c5e5
|
@ -30,6 +30,10 @@ body {
|
|||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
td {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
@ -284,7 +284,7 @@
|
|||
"build:css:watch": "brandable_css --watch",
|
||||
"build:js": "yarn run webpack-development",
|
||||
"build:js:watch": "yarn run webpack",
|
||||
"build:packages": "yarn workspace-run build:canvas",
|
||||
"build:packages": "yarn workspace-run-serial build:canvas",
|
||||
"lint:browser-code": "es-check es8 ./public/dist/**/*.js",
|
||||
"lint:staged": "lint-staged",
|
||||
"lint:js:coffeescripts": "eslint ./app/coffeescripts/**/*.js",
|
||||
|
|
|
@ -27,3 +27,5 @@ if (!('MutationObserver' in window)) {
|
|||
value: require('@sheerun/mutationobserver-shim')
|
||||
})
|
||||
}
|
||||
|
||||
window.scroll = () => {}
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
"@instructure/ui-themes": "^6.8.1",
|
||||
"@instructure/ui-toggle-details": "^6.8.1",
|
||||
"@instructure/uid": "^6.8.1",
|
||||
"@sheerun/mutationobserver-shim": "^0.3.2",
|
||||
"axios": "^0.18.0",
|
||||
"prop-types": "^15",
|
||||
"react": "^16",
|
||||
|
|
|
@ -37,7 +37,7 @@ if (!('MutationObserver' in window)) {
|
|||
}
|
||||
|
||||
if (typeof window.URL.createObjectURL === 'undefined') {
|
||||
Object.defineProperty(window.URL, 'createObjectURL', {value: () => {}})
|
||||
Object.defineProperty(window.URL, 'createObjectURL', {value: () => 'http://example.com/whatever'})
|
||||
}
|
||||
|
||||
window.scroll = () => {}
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
"lint:fix": "eslint --fix \"src/**/*.js\" \"test/**/*.js\"",
|
||||
"_test": "Test cafe will be added back to test as part of CORE-2995",
|
||||
"test": "yarn test:mocha && yarn test:jest",
|
||||
"test:mocha": "BABEL_ENV=test-node mocha 'test/**/*.test.js' --require @instructure/canvas-theme --require @babel/register --timeout 5000 --reporter mocha-multi-reporters --reporter-options configFile=mocha-reporter-config.json",
|
||||
"test:mocha:one": "BABEL_ENV=test-node mocha --require @instructure/canvas-theme --require @babel/register --timeout 5000 --reporter mocha-multi-reporters --reporter-options configFile=mocha-reporter-config.json",
|
||||
"test:jest": "jest",
|
||||
"test:mocha": "BABEL_ENV=test-node mocha 'test/**/*.test.js' --require @instructure/canvas-theme --require jsdom-global/register --require @babel/register --timeout 5000 --reporter mocha-multi-reporters --reporter-options configFile=mocha-reporter-config.json",
|
||||
"test:mocha:one": "BABEL_ENV=test-node mocha --require @instructure/canvas-theme --require jsdom-global/register --require @babel/register --timeout 5000 --reporter mocha-multi-reporters --reporter-options configFile=mocha-reporter-config.json",
|
||||
"test:jest": "jest --runInBand",
|
||||
"test:cafe": "yarn build:cafe && yarn test:cafe:only",
|
||||
"test:cafe:only": "testcafe chrome testcafe/**/*.test.js",
|
||||
"test:cafe:all": "yarn build:cafe && testcafe chrome,firefox,safari testcafe/**/*.test.js",
|
||||
|
@ -77,8 +77,9 @@
|
|||
"@instructure/ui-themeable": "6",
|
||||
"@instructure/ui-themes": "6",
|
||||
"@instructure/ui-toggle-details": "6",
|
||||
"@instructure/uid": "6",
|
||||
"@instructure/ui-utils": "6",
|
||||
"@instructure/uid": "6",
|
||||
"@sheerun/mutationobserver-shim": "^0.3.2",
|
||||
"@tinymce/tinymce-react": "^3.0.1",
|
||||
"aphrodite": "^2",
|
||||
"bloody-offset": "0.0.0",
|
||||
|
|
|
@ -277,7 +277,7 @@ describe('contentInsertion', () => {
|
|||
const video = videoFromUpload()
|
||||
const result = contentInsertion.insertVideo(editor, video)
|
||||
expect(editor.insertContent).toHaveBeenCalledWith(
|
||||
'<iframe allow="fullscreen" allowfullscreen="" data-media-id="m-media-id" src="/url/to/m-media-id?type=video" style="width:400px;height:225px;display:inline-block"></iframe>'
|
||||
'<iframe allow="fullscreen" allowfullscreen="" data-media-id="m-media-id" data-media-type="video" src="/url/to/m-media-id?type=video" style="width:400px;height:225px;display:inline-block"></iframe>'
|
||||
)
|
||||
expect(result).toEqual('the inserted iframe')
|
||||
})
|
||||
|
@ -287,7 +287,7 @@ describe('contentInsertion', () => {
|
|||
const video = videoFromTray()
|
||||
const result = contentInsertion.insertVideo(editor, video)
|
||||
expect(editor.insertContent).toHaveBeenCalledWith(
|
||||
'<iframe allow="fullscreen" allowfullscreen="" data-media-id="17" src="/media_objects_iframe?mediahref=%2Furl%2Fto%2Fcourse%2Ffile&type=video" style="width:400px;height:225px;display:inline-block"></iframe>'
|
||||
'<iframe allow="fullscreen" allowfullscreen="" data-media-id="17" data-media-type="video" src="/media_objects_iframe?mediahref=%2Furl%2Fto%2Fcourse%2Ffile&type=video" style="width:400px;height:225px;display:inline-block" title="filename.mov"></iframe>'
|
||||
)
|
||||
expect(result).toEqual('the inserted iframe')
|
||||
})
|
||||
|
@ -306,7 +306,7 @@ describe('contentInsertion', () => {
|
|||
const audio = audioFromUpload()
|
||||
const result = contentInsertion.insertAudio(editor, audio)
|
||||
expect(editor.insertContent).toHaveBeenCalledWith(
|
||||
'<iframe data-media-id="m-media-id" src="/url/to/m-media-id?type=audio" style="width:300px;height:2.813rem;display:inline-block"></iframe>'
|
||||
'<iframe data-media-id="m-media-id" data-media-type="audio" src="/url/to/m-media-id?type=audio" style="width:300px;height:2.813rem;display:inline-block"></iframe>'
|
||||
)
|
||||
expect(result).toEqual('the inserted iframe')
|
||||
})
|
||||
|
@ -316,7 +316,7 @@ describe('contentInsertion', () => {
|
|||
const audio = audioFromTray()
|
||||
const result = contentInsertion.insertAudio(editor, audio)
|
||||
expect(editor.insertContent).toHaveBeenCalledWith(
|
||||
'<iframe data-media-id="29" src="/media_objects_iframe?mediahref=url%2Fto%2Fcourse%2Ffile&type=audio" style="width:300px;height:2.813rem;display:inline-block"></iframe>'
|
||||
'<iframe data-media-id="29" data-media-type="audio" src="/media_objects_iframe?mediahref=url%2Fto%2Fcourse%2Ffile&type=audio" style="width:300px;height:2.813rem;display:inline-block" title="filename.mp3"></iframe>'
|
||||
)
|
||||
expect(result).toEqual('the inserted iframe')
|
||||
})
|
||||
|
|
|
@ -157,7 +157,7 @@ describe('contentRendering', () => {
|
|||
const video = videoFromTray()
|
||||
const rendered = contentRendering.renderVideo(video)
|
||||
expect(rendered).toEqual(
|
||||
'<iframe allow="fullscreen" allowfullscreen="" data-media-id="17" src="/media_objects_iframe?mediahref=%2Furl%2Fto%2Fcourse%2Ffile&type=video" style="width:400px;height:225px;display:inline-block"></iframe>'
|
||||
'<iframe allow="fullscreen" allowfullscreen="" data-media-id="17" data-media-type="video" src="/media_objects_iframe?mediahref=%2Furl%2Fto%2Fcourse%2Ffile&type=video" style="width:400px;height:225px;display:inline-block" title="filename.mov"></iframe>'
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -165,7 +165,7 @@ describe('contentRendering', () => {
|
|||
const video = videoFromUpload()
|
||||
const rendered = contentRendering.renderVideo(video)
|
||||
expect(rendered).toEqual(
|
||||
'<iframe allow="fullscreen" allowfullscreen="" data-media-id="m-media-id" src="/url/to/m-media-id?type=video" style="width:400px;height:225px;display:inline-block"></iframe>'
|
||||
'<iframe allow="fullscreen" allowfullscreen="" data-media-id="m-media-id" data-media-type="video" src="/url/to/m-media-id?type=video" style="width:400px;height:225px;display:inline-block"></iframe>'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
@ -187,7 +187,7 @@ describe('contentRendering', () => {
|
|||
const audio = audioFromTray()
|
||||
const rendered = contentRendering.renderAudio(audio)
|
||||
expect(rendered).toEqual(
|
||||
'<iframe data-media-id="29" src="/media_objects_iframe?mediahref=url%2Fto%2Fcourse%2Ffile&type=audio" style="width:300px;height:2.813rem;display:inline-block"></iframe>'
|
||||
'<iframe data-media-id="29" data-media-type="audio" src="/media_objects_iframe?mediahref=url%2Fto%2Fcourse%2Ffile&type=audio" style="width:300px;height:2.813rem;display:inline-block" title="filename.mp3"></iframe>'
|
||||
)
|
||||
})
|
||||
|
||||
|
@ -195,7 +195,7 @@ describe('contentRendering', () => {
|
|||
const audio = audioFromUpload()
|
||||
const rendered = contentRendering.renderAudio(audio)
|
||||
expect(rendered).toEqual(
|
||||
'<iframe data-media-id="m-media-id" src="/url/to/m-media-id?type=audio" style="width:300px;height:2.813rem;display:inline-block"></iframe>'
|
||||
'<iframe data-media-id="m-media-id" data-media-type="audio" src="/url/to/m-media-id?type=audio" style="width:300px;height:2.813rem;display:inline-block"></iframe>'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -108,13 +108,14 @@ function constructJSXVideoEmbedding(video) {
|
|||
allow="fullscreen"
|
||||
allowFullScreen
|
||||
data-media-id={`${video.media_id || video.id}`}
|
||||
data-media-type="video"
|
||||
src={src}
|
||||
style={{
|
||||
width: VIDEO_SIZE_DEFAULT.width,
|
||||
height: VIDEO_SIZE_DEFAULT.height,
|
||||
display: 'inline-block'
|
||||
}}
|
||||
title={video.name}
|
||||
title={video.name || video.text}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -128,13 +129,14 @@ function constructJSXAudioEmbedding(audio) {
|
|||
return (
|
||||
<iframe
|
||||
data-media-id={`${audio.media_id || audio.id}`}
|
||||
data-media-type="audio"
|
||||
src={src}
|
||||
style={{
|
||||
width: AUDIO_PLAYER_SIZE.width,
|
||||
height: AUDIO_PLAYER_SIZE.height,
|
||||
display: 'inline-block'
|
||||
}}
|
||||
title={audio.name}
|
||||
title={audio.name || audio.text}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ export const EXTRA_LARGE = 'extra-large'
|
|||
export const CUSTOM = 'custom'
|
||||
|
||||
export const imageSizes = [SMALL, MEDIUM, LARGE, EXTRA_LARGE, CUSTOM]
|
||||
export const videoSizes = [MEDIUM, LARGE, EXTRA_LARGE, CUSTOM]
|
||||
export const defaultImageSize = 320
|
||||
|
||||
const sizeByMaximumDimension = {
|
||||
|
@ -74,6 +75,42 @@ export function fromImageEmbed($element) {
|
|||
return imageOptions
|
||||
}
|
||||
|
||||
export function fromVideoEmbed($element) {
|
||||
// $element will be the <span> tinuymce wraps around the iframe
|
||||
// that's hosting the video player
|
||||
let $videoElem = null
|
||||
let naturalWidth, naturalHeight
|
||||
if ($element.firstElementChild.tagName === 'IFRAME') {
|
||||
const videoDoc = $element.firstElementChild.contentDocument
|
||||
if (videoDoc) {
|
||||
$videoElem = videoDoc.querySelector('video')
|
||||
}
|
||||
if ($videoElem && ($videoElem.loadedmetadata || $videoElem.readyState >= 1)) {
|
||||
naturalWidth = $videoElem.videoWidth
|
||||
naturalHeight = $videoElem.videoHeight
|
||||
}
|
||||
}
|
||||
|
||||
// because tinymce doesn't always put the title attribute on the iframe,
|
||||
// but it does maintain it on the span it adds around it.
|
||||
const title =
|
||||
$element.firstElementChild.getAttribute('data-titleText') ||
|
||||
$element.getAttribute('data-mce-p-data-titleText')
|
||||
const rect = $element.getBoundingClientRect()
|
||||
const videoOptions = {
|
||||
titleText: title || '',
|
||||
appliedHeight: rect.height,
|
||||
appliedWidth: rect.width,
|
||||
naturalHeight,
|
||||
naturalWidth,
|
||||
source: $videoElem && $videoElem.querySelector('source')
|
||||
}
|
||||
|
||||
videoOptions.videoSize = imageSizeFromKnownOptions(videoOptions)
|
||||
|
||||
return videoOptions
|
||||
}
|
||||
|
||||
export function scaleImageForHeight(naturalWidth, naturalHeight, targetHeight) {
|
||||
const constraints = {minHeight: MIN_HEIGHT, minWidth: MIN_WIDTH}
|
||||
return scaleForHeight(naturalWidth, naturalHeight, targetHeight, constraints)
|
||||
|
|
|
@ -18,11 +18,9 @@
|
|||
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import {camelize} from '@instructure/ui-utils'
|
||||
|
||||
import bridge from '../../../../bridge'
|
||||
import {asImageEmbed} from '../../shared/ContentSelection'
|
||||
import {renderImage} from '../../../contentRendering'
|
||||
import ImageOptionsTray from '.'
|
||||
|
||||
export const CONTAINER_ID = 'instructure-image-options-tray-container'
|
||||
|
@ -67,24 +65,19 @@ export default class TrayController {
|
|||
const {$img} = this
|
||||
|
||||
if (imageOptions.displayAs === 'embed') {
|
||||
editor.dom.setAttribs($img, {
|
||||
alt: imageOptions.isDecorativeImage ? '' : imageOptions.altText,
|
||||
'data-is-decorative': imageOptions.isDecorativeImage ? 'true' : null,
|
||||
width: imageOptions.appliedWidth,
|
||||
height: imageOptions.appliedHeight
|
||||
})
|
||||
|
||||
// when the image was first added to the rce, we applied
|
||||
// max-width and max-height. Remove them from the style now
|
||||
const style = this._parseStyle($img)
|
||||
delete style.maxWidth
|
||||
delete style.maxHeight
|
||||
|
||||
const newImg = renderImage({
|
||||
id: $img.id,
|
||||
url: $img.src,
|
||||
alt_text: {
|
||||
decorativeSelected: imageOptions.isDecorativeImage,
|
||||
altText: imageOptions.altText
|
||||
},
|
||||
width: imageOptions.appliedWidth,
|
||||
height: imageOptions.appliedHeight,
|
||||
style
|
||||
editor.dom.setStyles($img, {
|
||||
'max-height': '',
|
||||
'max-width': ''
|
||||
})
|
||||
editor.selection.setContent(newImg)
|
||||
|
||||
// tell tinymce so the context toolbar resets
|
||||
editor.fire('ObjectResized', {
|
||||
|
@ -137,18 +130,4 @@ export default class TrayController {
|
|||
)
|
||||
ReactDOM.render(element, this.$container)
|
||||
}
|
||||
|
||||
_parseStyle(elem) {
|
||||
const styl = elem.getAttribute('style')
|
||||
if (styl) {
|
||||
return styl.split(/;\s*/).reduce((sobj, oneStyle) => {
|
||||
const [name, val] = oneStyle.split(/:\s*/)
|
||||
if (name) {
|
||||
sobj[camelize(name)] = val
|
||||
}
|
||||
return sobj
|
||||
}, {})
|
||||
}
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,11 +19,14 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
import bridge from '../../../../bridge'
|
||||
import formatMessage from '../../../../format-message'
|
||||
import {asVideoElement} from '../../shared/ContentSelection'
|
||||
import VideoOptionsTray from '.'
|
||||
|
||||
export const CONTAINER_ID = 'instructure-video-options-tray-container'
|
||||
|
||||
export const VIDEO_SIZE_DEFAULT = {height: '225px', width: '400px'}
|
||||
export const VIDEO_SIZE_DEFAULT = {height: '225px', width: '400px'} // AKA "LARGE"
|
||||
export const AUDIO_PLAYER_SIZE = {width: '300px', height: '2.813rem'}
|
||||
|
||||
export default class TrayController {
|
||||
|
@ -50,6 +53,7 @@ export default class TrayController {
|
|||
|
||||
showTrayForEditor(editor) {
|
||||
this._editor = editor
|
||||
this.$videoContainer = editor.selection.getNode()
|
||||
this._shouldOpen = true
|
||||
this._renderTray()
|
||||
}
|
||||
|
@ -61,16 +65,40 @@ export default class TrayController {
|
|||
}
|
||||
|
||||
_applyVideoOptions(videoOptions) {
|
||||
const editor = this._editor
|
||||
const $videoContainer = editor.selection.getNodeVIDEO_SIZE_DEFAULTVIDEO_SIZE_DEFAULT
|
||||
$videoContainer.setAttribute(
|
||||
'style',
|
||||
`height: ${VIDEO_SIZE_DEFAULT[videoOptions.videoSize].height}; width:${VIDEO_SIZE_DEFAULT[videoOptions.videoSize].width}`
|
||||
if (this.$videoContainer && this.$videoContainer.firstElementChild?.tagName === 'IFRAME') {
|
||||
const styl = {
|
||||
height: `${videoOptions.appliedHeight}px`,
|
||||
width: `${videoOptions.appliedWidth}px`
|
||||
}
|
||||
this._editor.dom.setStyles(this.$videoContainer, styl)
|
||||
this._editor.dom.setStyles(this.$videoContainer.firstElementChild, styl)
|
||||
|
||||
const title = formatMessage('Video player for {title}', {title: videoOptions.titleText})
|
||||
this._editor.dom.setAttrib(this.$videoContainer, 'data-mce-p-title', title)
|
||||
this._editor.dom.setAttrib(
|
||||
this.$videoContainer,
|
||||
'data-mce-p-data-titleText',
|
||||
videoOptions.titleText
|
||||
)
|
||||
this._editor.dom.setAttrib(this.$videoContainer.firstElementChild, 'title', title)
|
||||
this._editor.dom.setAttrib(
|
||||
this.$videoContainer.firstElementChild,
|
||||
'data-titleText',
|
||||
videoOptions.titleText
|
||||
)
|
||||
|
||||
// tell tinymce so the context toolbar resets
|
||||
this._editor.fire('ObjectResized', {
|
||||
target: this.$videoContainer,
|
||||
width: videoOptions.appliedWidth,
|
||||
height: videoOptions.appliedHeight
|
||||
})
|
||||
}
|
||||
this._dismissTray()
|
||||
}
|
||||
|
||||
_dismissTray() {
|
||||
this._editor.selection.select(this.$videoContainer)
|
||||
this._shouldOpen = false
|
||||
this._renderTray()
|
||||
this._editor = null
|
||||
|
@ -92,10 +120,12 @@ export default class TrayController {
|
|||
const element = (
|
||||
<VideoOptionsTray
|
||||
key={this._renderId}
|
||||
videoOptions={asVideoElement(this.$videoContainer)}
|
||||
onEntered={() => {
|
||||
this._isOpen = true
|
||||
}}
|
||||
onExited={() => {
|
||||
bridge.focusActiveEditor(false)
|
||||
this._isOpen = false
|
||||
}}
|
||||
onSave={videoOptions => {
|
||||
|
|
|
@ -21,6 +21,51 @@ import ReactDOM from 'react-dom'
|
|||
import TrayController, {CONTAINER_ID} from '../TrayController'
|
||||
import FakeEditor from '../../../shared/__tests__/FakeEditor'
|
||||
import VideoOptionsTrayDriver from './VideoOptionsTrayDriver'
|
||||
import * as contentSelection from '../../../shared/ContentSelection'
|
||||
|
||||
const mockVideoPlayers = [
|
||||
{
|
||||
titleText: 'video title 0',
|
||||
appliedWidth: 400,
|
||||
appliedHeight: 300,
|
||||
naturalWidth: 800,
|
||||
naturalHeight: 600,
|
||||
source: '/path/to/video0.mp4',
|
||||
type: 'video-embed',
|
||||
id: 'm-video-id0'
|
||||
},
|
||||
{
|
||||
titleText: 'video title 1',
|
||||
appliedWidth: 400,
|
||||
appliedHeight: 300,
|
||||
naturalWidth: 800,
|
||||
naturalHeight: 600,
|
||||
source: '/path/to/video1.mp4',
|
||||
type: 'video-embed',
|
||||
id: 'm-video-id1'
|
||||
},
|
||||
{
|
||||
titleText: 'video title2',
|
||||
appliedWidth: 400,
|
||||
appliedHeight: 300,
|
||||
naturalWidth: 800,
|
||||
naturalHeight: 600,
|
||||
source: '/path/to/video2.mp4',
|
||||
type: 'video-embed',
|
||||
id: 'm-video-id2'
|
||||
}
|
||||
]
|
||||
|
||||
beforeAll(() => {
|
||||
contentSelection.asVideoElement = jest.fn(elem => {
|
||||
const vid = elem.getAttribute('id')
|
||||
return mockVideoPlayers.find(vp => vp.id === vid)
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('RCE "Videos" Plugin > VideoOptionsTray > TrayController', () => {
|
||||
let $videos
|
||||
|
@ -30,9 +75,9 @@ describe('RCE "Videos" Plugin > VideoOptionsTray > TrayController', () => {
|
|||
beforeEach(() => {
|
||||
$videos = []
|
||||
editors = [new FakeEditor(), new FakeEditor()]
|
||||
editors.forEach(editor => {
|
||||
editors.forEach((editor, i) => {
|
||||
editor.initialize()
|
||||
const $video = createVideo(320, 320)
|
||||
const $video = createVideo(i)
|
||||
$videos.push($video)
|
||||
editor.appendElement($video)
|
||||
editor.setSelectedNode($video)
|
||||
|
@ -49,23 +94,37 @@ describe('RCE "Videos" Plugin > VideoOptionsTray > TrayController', () => {
|
|||
}
|
||||
})
|
||||
|
||||
function createVideo(height = 200, width = 200, id = '12345bseds') {
|
||||
const $el = document.createElement('div')
|
||||
$el.setAttribute('style', `height: ${height}; width:${width}`)
|
||||
$el.id = id
|
||||
return $el
|
||||
function createVideo(i) {
|
||||
const velem = document.createElement('div')
|
||||
velem.setAttribute('id', mockVideoPlayers[i].id)
|
||||
velem.setAttribute('title', mockVideoPlayers[i].titleText)
|
||||
return velem
|
||||
}
|
||||
|
||||
function getTray() {
|
||||
return VideoOptionsTrayDriver.find()
|
||||
}
|
||||
|
||||
function getVideoOptionsFromTray() {
|
||||
const driver = VideoOptionsTrayDriver.find()
|
||||
return {
|
||||
titleText: driver.titleText,
|
||||
displayAs: driver.displayAs,
|
||||
size: driver.size
|
||||
}
|
||||
}
|
||||
|
||||
describe('#showTrayForEditor()', () => {
|
||||
describe('when the tray is not already open', () => {
|
||||
it('opens the tray', async () => {
|
||||
trayController.showTrayForEditor(editors[0])
|
||||
expect(getTray()).not.toBeNull()
|
||||
})
|
||||
|
||||
it('uses the selected video from the editor', async () => {
|
||||
trayController.showTrayForEditor(editors[0])
|
||||
expect(getVideoOptionsFromTray().titleText).toEqual($videos[0].getAttribute('title'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the tray is open for a different editor', () => {
|
||||
|
@ -85,7 +144,7 @@ describe('RCE "Videos" Plugin > VideoOptionsTray > TrayController', () => {
|
|||
beforeEach(async () => {
|
||||
trayController.showTrayForEditor(editors[0])
|
||||
|
||||
$otherVideo = createVideo(210, 210)
|
||||
$otherVideo = createVideo(0)
|
||||
editors[0].setSelectedNode($otherVideo)
|
||||
trayController.showTrayForEditor(editors[0])
|
||||
})
|
||||
|
|
|
@ -30,7 +30,19 @@ describe('RCE "Videos" Plugin > VideoOptionsTray', () => {
|
|||
props = {
|
||||
onRequestClose: jest.fn(),
|
||||
onSave: jest.fn(),
|
||||
open: true
|
||||
open: true,
|
||||
videoOptions: {
|
||||
$element: null,
|
||||
appliedHeight: 180,
|
||||
appliedWidth: 320,
|
||||
id: 'm-video-id',
|
||||
naturalHeight: 730,
|
||||
naturalWidth: 1280,
|
||||
source: {},
|
||||
titleText: '',
|
||||
type: 'video-embed',
|
||||
videoSize: 'medium'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -55,4 +67,152 @@ describe('RCE "Videos" Plugin > VideoOptionsTray', () => {
|
|||
renderComponent()
|
||||
expect(tray.label).toEqual('Video Options Tray')
|
||||
})
|
||||
|
||||
describe('Title field', () => {
|
||||
it('uses the value of titleText in the given video options', () => {
|
||||
props.videoOptions.titleText = 'A turtle in a party suit.'
|
||||
renderComponent()
|
||||
expect(tray.titleText).toEqual('A turtle in a party suit.')
|
||||
})
|
||||
|
||||
it('is blank when the given video options titleText is blank', () => {
|
||||
props.videoOptions.titleText = ''
|
||||
renderComponent()
|
||||
expect(tray.titleText).toEqual('')
|
||||
})
|
||||
|
||||
it('is disabled when displaying the image as a link', () => {
|
||||
renderComponent()
|
||||
tray.setDisplayAs('link')
|
||||
expect(tray.titleTextDisabled).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('"Display Options" field', () => {
|
||||
it('is set to "embed" by default', () => {
|
||||
renderComponent()
|
||||
expect(tray.displayAs).toEqual('embed')
|
||||
})
|
||||
|
||||
it('can be set to "Display Text Link"', () => {
|
||||
renderComponent()
|
||||
tray.setDisplayAs('link')
|
||||
expect(tray.displayAs).toEqual('link')
|
||||
})
|
||||
|
||||
it('can be reset to "Embed Image"', () => {
|
||||
renderComponent()
|
||||
tray.setDisplayAs('link')
|
||||
tray.setDisplayAs('embed')
|
||||
expect(tray.displayAs).toEqual('embed')
|
||||
})
|
||||
})
|
||||
|
||||
describe('"Size" field', () => {
|
||||
it('is set using the given image options', () => {
|
||||
renderComponent()
|
||||
expect(tray.size).toEqual('Medium')
|
||||
})
|
||||
|
||||
it('can be re-set to "Medium"', async () => {
|
||||
renderComponent()
|
||||
await tray.setSize('Large')
|
||||
await tray.setSize('Medium')
|
||||
expect(tray.size).toEqual('Medium')
|
||||
})
|
||||
|
||||
it('can be set to "Large"', async () => {
|
||||
renderComponent()
|
||||
await tray.setSize('Large')
|
||||
expect(tray.size).toEqual('Large')
|
||||
})
|
||||
|
||||
it('can be set to "Custom"', async () => {
|
||||
renderComponent()
|
||||
await tray.setSize('Custom')
|
||||
expect(tray.size).toEqual('Custom')
|
||||
})
|
||||
})
|
||||
|
||||
describe('"Done" button', () => {
|
||||
describe('when Title Text is present', () => {
|
||||
beforeEach(() => {
|
||||
renderComponent()
|
||||
tray.setTitleText('A turtle in a party suit.')
|
||||
})
|
||||
|
||||
it('is enabled', () => {
|
||||
expect(tray.doneButtonDisabled).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when Title Text is not present', () => {
|
||||
beforeEach(() => {
|
||||
renderComponent()
|
||||
tray.setTitleText('')
|
||||
})
|
||||
|
||||
it('is disabled ', () => {
|
||||
expect(tray.doneButtonDisabled).toEqual(true)
|
||||
})
|
||||
|
||||
it('is enabled when "Display Text Link" is selected', () => {
|
||||
tray.setDisplayAs('link')
|
||||
expect(tray.doneButtonDisabled).toEqual(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when clicked', () => {
|
||||
beforeEach(() => {
|
||||
renderComponent()
|
||||
tray.setTitleText('A turtle in a party suit.')
|
||||
})
|
||||
|
||||
it('prevents the default click handler', () => {
|
||||
const preventDefault = jest.fn()
|
||||
// Override preventDefault before event reaches image
|
||||
tray.$doneButton.addEventListener(
|
||||
'click',
|
||||
event => {
|
||||
Object.assign(event, {preventDefault})
|
||||
},
|
||||
true
|
||||
)
|
||||
tray.$doneButton.click()
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('calls the .onSave prop', () => {
|
||||
tray.$doneButton.click()
|
||||
expect(props.onSave).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
describe('when calling the .onSave prop', () => {
|
||||
it('includes the Title Text', () => {
|
||||
tray.setTitleText('A turtle in a party suit.')
|
||||
tray.$doneButton.click()
|
||||
const [{titleText}] = props.onSave.mock.calls[0]
|
||||
expect(titleText).toEqual('A turtle in a party suit.')
|
||||
})
|
||||
|
||||
it('includes the "Display As" setting', () => {
|
||||
tray.setDisplayAs('link')
|
||||
tray.$doneButton.click()
|
||||
const [{displayAs}] = props.onSave.mock.calls[0]
|
||||
expect(displayAs).toEqual('link')
|
||||
})
|
||||
|
||||
it('includes the size to be applied', async () => {
|
||||
await tray.setSize('Large')
|
||||
tray.$doneButton.click()
|
||||
const [{appliedHeight, appliedWidth}] = props.onSave.mock.calls[0]
|
||||
expect(appliedWidth).toEqual(400)
|
||||
const expectedHt = Math.round(
|
||||
(props.videoOptions.naturalHeight / props.videoOptions.naturalWidth) * 400
|
||||
)
|
||||
expect(appliedHeight).toEqual(expectedHt)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {getByLabelText, queryByLabelText, wait} from '@testing-library/dom'
|
||||
import {fireEvent, getAllByText, getByLabelText, queryByLabelText, wait} from '@testing-library/dom'
|
||||
|
||||
function getSizeOptions($sizeSelect) {
|
||||
const controlledId = $sizeSelect.getAttribute('aria-controls')
|
||||
|
@ -44,6 +44,14 @@ export default class VideoOptionsTrayDriver {
|
|||
return this.$element.getAttribute('aria-label')
|
||||
}
|
||||
|
||||
get $titleTextField() {
|
||||
return this.$element.querySelector('textarea')
|
||||
}
|
||||
|
||||
get $displayAsField() {
|
||||
return getAllByText(this.$element, 'Display Options')[0].closest('fieldset')
|
||||
}
|
||||
|
||||
get $sizeSelect() {
|
||||
return getByLabelText(this.$element, 'Size')
|
||||
}
|
||||
|
@ -54,6 +62,18 @@ export default class VideoOptionsTrayDriver {
|
|||
)
|
||||
}
|
||||
|
||||
get titleText() {
|
||||
return this.$titleTextField.value
|
||||
}
|
||||
|
||||
get titleTextDisabled() {
|
||||
return this.$titleTextField.disabled
|
||||
}
|
||||
|
||||
get displayAs() {
|
||||
return this.$displayAsField.querySelector('input[type="radio"]:checked').value
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this.$sizeSelect.value
|
||||
}
|
||||
|
@ -62,6 +82,15 @@ export default class VideoOptionsTrayDriver {
|
|||
return this.$doneButton.disabled
|
||||
}
|
||||
|
||||
setTitleText(titleText) {
|
||||
fireEvent.change(this.$titleTextField, {target: {value: titleText}})
|
||||
}
|
||||
|
||||
setDisplayAs(value) {
|
||||
const $input = this.$displayAsField.querySelector(`input[type="radio"][value="${value}"]`)
|
||||
$input.click()
|
||||
}
|
||||
|
||||
async setSize(sizeText) {
|
||||
this.$sizeSelect.click()
|
||||
await wait(() => getSizeOptions(this.$sizeSelect))
|
||||
|
|
|
@ -16,25 +16,124 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {bool, func, shape, string} from 'prop-types'
|
||||
import React, {useState} from 'react'
|
||||
import {bool, func, number, shape, string} from 'prop-types'
|
||||
import {ScreenReaderContent} from '@instructure/ui-a11y'
|
||||
import {Button, CloseButton} from '@instructure/ui-buttons'
|
||||
|
||||
import {Flex} from '@instructure/ui-layout'
|
||||
import {Heading} from '@instructure/ui-elements'
|
||||
import React from 'react'
|
||||
import {Tray} from '@instructure/ui-overlays'
|
||||
import {RadioInput, RadioInputGroup, Select, TextArea} from '@instructure/ui-forms'
|
||||
import {IconQuestionLine} from '@instructure/ui-icons'
|
||||
import {Flex, View} from '@instructure/ui-layout'
|
||||
import {Tooltip, Tray} from '@instructure/ui-overlays'
|
||||
|
||||
import {
|
||||
CUSTOM,
|
||||
MIN_HEIGHT,
|
||||
MIN_WIDTH,
|
||||
videoSizes,
|
||||
labelForImageSize,
|
||||
scaleToSize
|
||||
} from '../../instructure_image/ImageEmbedOptions'
|
||||
import formatMessage from '../../../../format-message'
|
||||
import DimensionsInput, {useDimensionsState} from '../../shared/DimensionsInput'
|
||||
|
||||
export default function VideoOptionsTray(props) {
|
||||
const {onRequestClose, open} = props
|
||||
const {videoOptions, onRequestClose, open} = props
|
||||
|
||||
const {naturalHeight, naturalWidth} = videoOptions
|
||||
const currentHeight = videoOptions.appliedHeight || naturalHeight
|
||||
const currentWidth = videoOptions.appliedWidth || naturalWidth
|
||||
|
||||
const [titleText, setTitleText] = useState(videoOptions.titleText)
|
||||
const [displayAs, setDisplayAs] = useState('embed')
|
||||
const [videoSize, setVideoSize] = useState(videoOptions.videoSize)
|
||||
const [videoHeight, setVideoHeight] = useState(currentHeight)
|
||||
const [videoWidth, setVideoWidth] = useState(currentWidth)
|
||||
|
||||
const dimensionsState = useDimensionsState(videoOptions, {
|
||||
minHeight: MIN_HEIGHT,
|
||||
minWidth: MIN_WIDTH
|
||||
})
|
||||
|
||||
const videoSizeOption = {label: labelForImageSize(videoSize), value: videoSize}
|
||||
|
||||
function handleTitleTextChange(event) {
|
||||
setTitleText(event.target.value)
|
||||
}
|
||||
|
||||
function handleDisplayAsChange(event) {
|
||||
setDisplayAs(event.target.value)
|
||||
}
|
||||
|
||||
function handleVideoSizeChange(event, selectedOption) {
|
||||
setVideoSize(selectedOption.value)
|
||||
if (selectedOption.value === CUSTOM) {
|
||||
setVideoHeight(currentHeight)
|
||||
setVideoWidth(currentWidth)
|
||||
} else {
|
||||
const {height, width} = scaleToSize(selectedOption.value, naturalWidth, naturalHeight)
|
||||
setVideoHeight(height)
|
||||
setVideoWidth(width)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSave(event) {
|
||||
event.preventDefault()
|
||||
|
||||
let appliedHeight = videoHeight
|
||||
let appliedWidth = videoWidth
|
||||
if (videoSize === CUSTOM) {
|
||||
appliedHeight = dimensionsState.height
|
||||
appliedWidth = dimensionsState.width
|
||||
}
|
||||
|
||||
props.onSave({
|
||||
titleText,
|
||||
appliedHeight,
|
||||
appliedWidth,
|
||||
displayAs
|
||||
})
|
||||
}
|
||||
|
||||
const tooltipText = formatMessage('Used by screen readers to describe the video')
|
||||
const textAreaLabel = (
|
||||
<Flex alignItems="center">
|
||||
<Flex.Item>{formatMessage('Title')}</Flex.Item>
|
||||
|
||||
<Flex.Item margin="0 0 0 xx-small">
|
||||
<Tooltip
|
||||
on={['hover', 'focus']}
|
||||
placement="top"
|
||||
tip={
|
||||
<View display="block" id="alt-text-label-tooltip" maxWidth="14rem">
|
||||
{tooltipText}
|
||||
</View>
|
||||
}
|
||||
>
|
||||
<Button icon={IconQuestionLine} size="small" variant="icon">
|
||||
<ScreenReaderContent>{tooltipText}</ScreenReaderContent>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
)
|
||||
|
||||
const messagesForSize = []
|
||||
if (videoSize !== CUSTOM) {
|
||||
messagesForSize.push({
|
||||
text: formatMessage('{width} x {height}px', {height: videoHeight, width: videoWidth}),
|
||||
type: 'hint'
|
||||
})
|
||||
}
|
||||
|
||||
const saveDisabled =
|
||||
displayAs === 'embed' &&
|
||||
(titleText === '' || (videoSize === CUSTOM && !dimensionsState.isValid))
|
||||
|
||||
return (
|
||||
<Tray
|
||||
data-mce-component
|
||||
label={formatMessage('Video Options Tray')}
|
||||
onDismiss={onRequestClose}
|
||||
onEntered={props.onEntered}
|
||||
|
@ -53,20 +152,84 @@ export default function VideoOptionsTray(props) {
|
|||
</Flex.Item>
|
||||
|
||||
<Flex.Item>
|
||||
<CloseButton onClick={onRequestClose}>{formatMessage('Close')}</CloseButton>
|
||||
<CloseButton placemet="static" variant="icon" onClick={onRequestClose}>
|
||||
{formatMessage('Close')}
|
||||
</CloseButton>
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
</Flex.Item>
|
||||
|
||||
<Flex.Item as="form" grow margin="none" shrink>
|
||||
<Flex justifyItems="space-between" direction="column" height="100%">
|
||||
<Flex.Item grow padding="small" shrink>
|
||||
<Flex direction="column">
|
||||
<Flex.Item padding="small">
|
||||
<TextArea
|
||||
aria-describedby="alt-text-label-tooltip"
|
||||
disabled={displayAs === 'link'}
|
||||
height="4rem"
|
||||
label={textAreaLabel}
|
||||
onChange={handleTitleTextChange}
|
||||
placeholder={formatMessage('(Describe the video)')}
|
||||
resize="vertical"
|
||||
value={titleText}
|
||||
/>
|
||||
</Flex.Item>
|
||||
|
||||
<Flex.Item margin="small none none none" padding="small">
|
||||
<RadioInputGroup
|
||||
description={formatMessage('Display Options')}
|
||||
name="display-video-as"
|
||||
onChange={handleDisplayAsChange}
|
||||
value={displayAs}
|
||||
>
|
||||
<RadioInput label={formatMessage('Embed Video')} value="embed" />
|
||||
|
||||
<RadioInput
|
||||
label={formatMessage('Display Text Link (Opens in a new tab)')}
|
||||
value="link"
|
||||
/>
|
||||
</RadioInputGroup>
|
||||
</Flex.Item>
|
||||
|
||||
<Flex.Item margin="small none xx-small none">
|
||||
<View as="div" padding="small small xx-small small">
|
||||
<Select
|
||||
disabled={displayAs !== 'embed'}
|
||||
label={formatMessage('Size')}
|
||||
messages={messagesForSize}
|
||||
onChange={handleVideoSizeChange}
|
||||
selectedOption={videoSizeOption}
|
||||
>
|
||||
{videoSizes.map(size => (
|
||||
<option key={size} value={size}>
|
||||
{labelForImageSize(size)}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</View>
|
||||
|
||||
{videoSize === CUSTOM && (
|
||||
<View as="div" padding="xx-small small">
|
||||
<DimensionsInput
|
||||
dimensionsState={dimensionsState}
|
||||
disabled={displayAs !== 'embed'}
|
||||
minHeight={MIN_HEIGHT}
|
||||
minWidth={MIN_WIDTH}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</Flex.Item>
|
||||
</Flex>
|
||||
</Flex.Item>
|
||||
|
||||
<Flex.Item
|
||||
background="light"
|
||||
borderWidth="small none none none"
|
||||
padding="small medium"
|
||||
textAlign="end"
|
||||
>
|
||||
<Button onClick={handleSave} variant="primary">
|
||||
<Button disabled={saveDisabled} onClick={handleSave} variant="primary">
|
||||
{formatMessage('Done')}
|
||||
</Button>
|
||||
</Flex.Item>
|
||||
|
@ -78,6 +241,13 @@ export default function VideoOptionsTray(props) {
|
|||
}
|
||||
|
||||
VideoOptionsTray.propTypes = {
|
||||
videoOptions: shape({
|
||||
titleText: string.isRequired,
|
||||
appliedHeight: number,
|
||||
appliedWidth: number,
|
||||
naturalHeight: number.isRequired,
|
||||
naturalWidth: number.isRequired
|
||||
}).isRequired,
|
||||
onEntered: func,
|
||||
onExited: func,
|
||||
onRequestClose: func.isRequired,
|
||||
|
|
|
@ -20,7 +20,7 @@ import clickCallback from './clickCallback'
|
|||
import bridge from '../../../bridge'
|
||||
import formatMessage from '../../../format-message'
|
||||
import TrayController from './VideoOptionsTray/TrayController'
|
||||
// import {isVideoElement} from '../shared/ContentSelection'
|
||||
import {isVideoElement} from '../shared/ContentSelection'
|
||||
|
||||
const trayController = new TrayController()
|
||||
|
||||
|
@ -86,7 +86,7 @@ tinymce.create('tinymce.plugins.InstructureRecord', {
|
|||
ed.ui.registry.addContextToolbar('instructure-video-toolbar', {
|
||||
items: 'instructure-video-options',
|
||||
position: 'node',
|
||||
predicate: false,
|
||||
predicate: isVideoElement,
|
||||
scope: 'node'
|
||||
})
|
||||
},
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {fromImageEmbed} from '../instructure_image/ImageEmbedOptions'
|
||||
import {fromImageEmbed, fromVideoEmbed} from '../instructure_image/ImageEmbedOptions'
|
||||
|
||||
const FILE_DOWNLOAD_PATH_REGEX = /^\/(courses\/\d+\/)?files\/\d+\/download$/
|
||||
|
||||
|
@ -77,23 +77,24 @@ export function asLink($element, editor) {
|
|||
}
|
||||
}
|
||||
|
||||
function asVideoElement($element) {
|
||||
if (!$element.id) {
|
||||
return null
|
||||
}
|
||||
|
||||
if ($element.childElementCount !== 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!$element.id.includes('media_object') || $element.children[0].tagName !== 'IFRAME') {
|
||||
// the video element is a bit tricky.
|
||||
// tinymce won't let me add many attributes to the iframe,
|
||||
// even though I've listed them in tinymce.config.js
|
||||
// extended_valid_elements.
|
||||
// we have to rely on the span tinymce wraps around the iframe
|
||||
// and it's attributes, even though this could change with future
|
||||
// tinymce releases.
|
||||
// see https://github.com/tinymce/tinymce/issues/5181
|
||||
export function asVideoElement($element) {
|
||||
if (!isVideoElement($element)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
...fromVideoEmbed($element),
|
||||
$element,
|
||||
type: VIDEO_EMBED_TYPE,
|
||||
id: $element.id.split('_')[2]
|
||||
id: $element.getAttribute('data-mce-p-data-media-id')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -159,5 +160,25 @@ export function isImageEmbed($element) {
|
|||
}
|
||||
|
||||
export function isVideoElement($element) {
|
||||
return !!asVideoElement($element)
|
||||
// the video is hosted in an iframe, but tinymce
|
||||
// wraps it in a span with swizzled attribute names
|
||||
if (!$element.getAttribute) {
|
||||
return false
|
||||
}
|
||||
|
||||
if ($element.firstElementChild?.tagName !== 'IFRAME') {
|
||||
return false
|
||||
}
|
||||
|
||||
const media_obj_id = $element.getAttribute('data-mce-p-data-media-id')
|
||||
if (!media_obj_id) {
|
||||
return false
|
||||
}
|
||||
|
||||
const media_type = $element.getAttribute('data-mce-p-data-media-type')
|
||||
if (media_type !== 'video') {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ function buildContentOptions(userContextType) {
|
|||
contentOptions.splice(
|
||||
1,
|
||||
0,
|
||||
<option value="course_files" icon={IconFolderLine}>
|
||||
<option key="course_files" value="course_files" icon={IconFolderLine}>
|
||||
{fileLabelFromContext('course')}
|
||||
</option>
|
||||
)
|
||||
|
@ -174,11 +174,6 @@ Filter.propTypes = {
|
|||
*/
|
||||
sortValue: string.isRequired,
|
||||
|
||||
/**
|
||||
* `contextType` is the context in which we are querying for files
|
||||
*/
|
||||
contextType: oneOf(['user', 'course']), // 'group' some day
|
||||
|
||||
/**
|
||||
* The user's context
|
||||
*/
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
*/
|
||||
|
||||
import React from 'react'
|
||||
import '@testing-library/jest-dom/extend-expect'
|
||||
import {render, fireEvent, waitForElement, cleanup} from '@testing-library/react'
|
||||
import {act} from 'react-dom/test-utils'
|
||||
import ComputerPanel from '../ComputerPanel'
|
||||
|
@ -124,6 +123,7 @@ describe('UploadFile: ComputerPanel', () => {
|
|||
hasUploadedFile
|
||||
setHasUploadedFile={handleSetHasUploadedFile}
|
||||
accept="image/*"
|
||||
label="Upload File"
|
||||
/>
|
||||
)
|
||||
expect(getByText('Generating preview...')).toBeInTheDocument()
|
||||
|
|
|
@ -98,35 +98,34 @@ describe('RCE > Plugins > Shared > Content Selection', () => {
|
|||
|
||||
describe('when the given element is a video container element', () => {
|
||||
beforeEach(() => {
|
||||
$element = $container.appendChild(document.createElement('div'))
|
||||
$element = $container.appendChild(document.createElement('span'))
|
||||
$element.setAttribute('data-mce-p-data-media-id', '1234')
|
||||
$element.setAttribute('data-mce-p-data-media-type', 'video')
|
||||
$element.appendChild(document.createElement('iframe'))
|
||||
})
|
||||
|
||||
it('returns None type if no id is present', () => {
|
||||
$element.removeAttribute('data-mce-p-data-media-id')
|
||||
expect(getContentFromElement($element).type).toEqual(NONE_TYPE)
|
||||
})
|
||||
|
||||
it('returns None type if there are no children', () => {
|
||||
$element.id = 'media_object_1234'
|
||||
expect(getContentFromElement($element).type).toEqual(NONE_TYPE)
|
||||
})
|
||||
|
||||
it('returns None type if there are more than one children', () => {
|
||||
$element.id = 'media_object_1234'
|
||||
$element.appendChild(document.createElement('iframe'))
|
||||
$element.appendChild(document.createElement('span'))
|
||||
$element.appendChild(document.createElement('span'))
|
||||
it('returns None type if there is no iframe child', () => {
|
||||
$element.innerHTML = ''
|
||||
expect(getContentFromElement($element).type).toEqual(NONE_TYPE)
|
||||
})
|
||||
|
||||
it('returns None type if children element is not an iframe', () => {
|
||||
$element.id = 'media_object_1234'
|
||||
$element.appendChild(document.createElement('span'))
|
||||
$element.replaceChild(document.createElement('span'), $element.firstElementChild)
|
||||
expect(getContentFromElement($element).type).toEqual(NONE_TYPE)
|
||||
})
|
||||
|
||||
it('returns None if no type is present', () => {
|
||||
$element.removeAttribute('data-mce-p-data-media-type')
|
||||
expect(getContentFromElement($element).type).toEqual(NONE_TYPE)
|
||||
})
|
||||
|
||||
it('returns id if iframe and div are set', () => {
|
||||
$element.id = 'media_object_1234'
|
||||
$element.appendChild(document.createElement('iframe'))
|
||||
$element.firstElementChild.setAttribute('src', 'data:text/html;charset=utf-8,<video/>')
|
||||
expect(getContentFromElement($element).id).toEqual('1234')
|
||||
})
|
||||
})
|
||||
|
@ -287,8 +286,9 @@ describe('RCE > Plugins > Shared > Content Selection', () => {
|
|||
})
|
||||
|
||||
it('detect a video element', () => {
|
||||
const $selectedNode = document.createElement('div')
|
||||
$selectedNode.id = 'foo_media_object'
|
||||
const $selectedNode = document.createElement('span')
|
||||
$selectedNode.setAttribute('data-mce-p-data-media-id', 'm-id')
|
||||
$selectedNode.setAttribute('data-mce-p-data-media-type', 'video')
|
||||
$selectedNode.innerHTML = '<iframe/>'
|
||||
editor.setSelectedNode($selectedNode)
|
||||
expect(isFileLink($selectedNode)).toBeFalsy()
|
||||
|
|
|
@ -39,7 +39,8 @@ export default class FakeEditor {
|
|||
},
|
||||
|
||||
collapse: () => (this._collapsed = true),
|
||||
isCollapsed: () => this._collapsed
|
||||
isCollapsed: () => this._collapsed,
|
||||
select: node => (this._selectedNode = node)
|
||||
}
|
||||
|
||||
this.dom = {
|
||||
|
@ -50,7 +51,17 @@ export default class FakeEditor {
|
|||
return parent
|
||||
}
|
||||
return null
|
||||
},
|
||||
setAttribs: (elem, hash) => {
|
||||
Object.keys(hash).forEach(k => {
|
||||
if (hash[k] == undefined) {
|
||||
elem.removeAttribute(k)
|
||||
} else {
|
||||
elem.setAttribute(k, hash[k])
|
||||
}
|
||||
})
|
||||
},
|
||||
setStyles: (_elem, _hash) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -249,15 +249,15 @@ class RceApiSource {
|
|||
return this.finalizeUpload(preflightProps, uploadResults)
|
||||
})
|
||||
.then(normalizeFileData)
|
||||
.catch(e => {
|
||||
.catch(_e => {
|
||||
this.alertFunc({
|
||||
text: formatMessage(
|
||||
'Something went wrong uploading, check your connection and try again.'
|
||||
),
|
||||
variant: 'error'
|
||||
})
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e)
|
||||
|
||||
// console.error(e) // eslint-disable-line no-console
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -380,21 +380,21 @@ class RceApiSource {
|
|||
.then(checkStatus)
|
||||
.then(parseResponse)
|
||||
.catch(throwConnectionError)
|
||||
.catch(e => {
|
||||
.catch(_e => {
|
||||
this.alertFunc({
|
||||
text: formatMessage(
|
||||
'Something went wrong uploading, check your connection and try again.'
|
||||
),
|
||||
variant: 'error'
|
||||
})
|
||||
console.error(e) // eslint-disable-line no-console
|
||||
// console.error(e) // eslint-disable-line no-console
|
||||
})
|
||||
}
|
||||
|
||||
// @private
|
||||
normalizeUriProtocol(uri, windowOverride) {
|
||||
const windowHandle = windowOverride || (typeof window !== 'undefined' ? window : undefined)
|
||||
if (windowHandle && windowHandle.location && windowHandle.location.protocol == 'https:') {
|
||||
if (windowHandle && windowHandle.location && windowHandle.location.protocol === 'https:') {
|
||||
return uri.replace('http://', 'https://')
|
||||
}
|
||||
return uri
|
||||
|
|
|
@ -715,9 +715,15 @@ export function initializeCollection(endpoint) {
|
|||
}
|
||||
}
|
||||
|
||||
export function initializeDocuments(props) {
|
||||
export function initializeDocuments(_props) {
|
||||
return {
|
||||
[props.contextType]: {
|
||||
course: {
|
||||
files: [],
|
||||
bookmark: 'documents1',
|
||||
isLoading: false,
|
||||
hasMore: true
|
||||
},
|
||||
user: {
|
||||
files: [],
|
||||
bookmark: 'documents1',
|
||||
isLoading: false,
|
||||
|
@ -726,9 +732,15 @@ export function initializeDocuments(props) {
|
|||
}
|
||||
}
|
||||
|
||||
export function initializeMedia(props) {
|
||||
export function initializeMedia(_props) {
|
||||
return {
|
||||
[props.contextType]: {
|
||||
course: {
|
||||
files: [],
|
||||
bookmark: 'media1',
|
||||
isLoading: false,
|
||||
hasMore: true
|
||||
},
|
||||
user: {
|
||||
files: [],
|
||||
bookmark: 'media1',
|
||||
isLoading: false,
|
||||
|
@ -808,7 +820,7 @@ export function fetchPage(uri) {
|
|||
if (PAGES[uri]) {
|
||||
resolve(PAGES[uri])
|
||||
} else {
|
||||
reject('bad page!')
|
||||
reject(new Error('bad page!'))
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
|
@ -820,7 +832,7 @@ export function searchFlickr(term) {
|
|||
if (FLICKR_RESULTS[term]) {
|
||||
resolve(FLICKR_RESULTS[term])
|
||||
} else {
|
||||
reject('No search results!')
|
||||
reject(new Error('No search results!'))
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
|
@ -832,7 +844,7 @@ export function searchUnsplash(term) {
|
|||
if (UNSPLASH_RESULTS[term]) {
|
||||
resolve(UNSPLASH_RESULTS[term])
|
||||
} else {
|
||||
reject('No search results!')
|
||||
reject(new Error('No search results!'))
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
|
|
|
@ -59,7 +59,6 @@ describe('Upload reducer', () => {
|
|||
thumbnail_url: 'http://some.url.example.com'
|
||||
}
|
||||
}
|
||||
state.uploads = []
|
||||
})
|
||||
|
||||
it('turns uploading off', () => {
|
||||
|
|
|
@ -305,7 +305,9 @@ describe('sources/api', () => {
|
|||
|
||||
it('throws an exception when an error occurs', () => {
|
||||
fetchMock.mock(uri, 500)
|
||||
assert.rejects(() => apiSource.preflightUpload(fileProps, apiProps))
|
||||
return apiSource.preflightUpload(fileProps, apiProps).catch(e => {
|
||||
assert(e)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -329,12 +331,15 @@ describe('sources/api', () => {
|
|||
|
||||
it('calls alertFunc if there is a problem', () => {
|
||||
fetchMock.once(uploadUrl, 500, {overwriteRoutes: true})
|
||||
return apiSource.uploadFRD(fileDomObject, preflightProps).then(() => {
|
||||
return apiSource
|
||||
.uploadFRD(fileDomObject, preflightProps)
|
||||
.then(() => {
|
||||
sinon.assert.calledWith(alertFuncSpy, {
|
||||
text: 'Something went wrong uploading, check your connection and try again.',
|
||||
variant: 'error'
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
})
|
||||
|
||||
describe('files', () => {
|
||||
|
|
|
@ -37,6 +37,9 @@ const consoleMessagesToIgnore = {
|
|||
// React 16.9+ generates these deprecation warnings but it doesn't do any good to hear about the ones for instUI. We can't do anything about them in this repo
|
||||
// Put any others we can't control here.
|
||||
/Please update the following components:[ (BaseTransition|Billboard|Button|Checkbox|CloseButton|Dialog|Expandable|FileDrop|Flex|FlexItem|FormFieldGroup|FormFieldLabel|FormFieldLayout|FormFieldMessage|FormFieldMessages|Grid|GridCol|GridRow|Heading|InlineSVG|Mask|ModalBody|ModalFooter|ModalHeader|NumberInput|Portal|Query|Responsive|SVGIcon|ScreenReaderContent|SelectOptionsList|SelectField|SelectMultiple|SelectOptionsList|SelectSingle|Spinner|Tab|TabList|TabPanel|Text|TextArea|TextInput|TinyMCE|ToggleDetails|ToggleFacade|Transition|TruncateText|View),?]+$/
|
||||
|
||||
// /is deprecated and will be removed/, // uncomment to remove instui deprecation messages
|
||||
// /Translation for/ // uncomment to remove missing translation messages
|
||||
]
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -14574,7 +14574,7 @@ node-releases@^1.1.29:
|
|||
dependencies:
|
||||
semver "^5.3.0"
|
||||
|
||||
node-sass@4.7.2, node-sass@^4.5.0, node-sass@^4.7.2:
|
||||
node-sass@^4.5.0, node-sass@^4.7.2:
|
||||
version "4.7.2"
|
||||
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.7.2.tgz#9366778ba1469eb01438a9e8592f4262bcb6794e"
|
||||
integrity sha512-CaV+wLqZ7//Jdom5aUFCpGNoECd7BbNhjuwdsX/LkXBrHl8eb1Wjw4HvWqcFvhr5KuNgAk8i/myf/MQ1YYeroA==
|
||||
|
|
Loading…
Reference in New Issue