mirror of
https://github.com/videojs/video.js.git
synced 2025-02-12 12:16:27 +02:00
@misteroneill added a modal dialog. closes #2668
This commit is contained in:
parent
ab88bcdde3
commit
51f1863adc
@ -8,6 +8,7 @@ CHANGELOG
|
||||
* @forbesjo removed android/ios tests to increase build stability ([view](https://github.com/videojs/video.js/pull/2739))
|
||||
* @nickygerritsen added canPlayType method to player ([view](https://github.com/videojs/video.js/pull/2709))
|
||||
* @gkatsev fixes track tests and ignored empty properties in tracks converter ([view](https://github.com/videojs/video.js/pull/2744))
|
||||
* @misteroneill added a modal dialog ([view](https://github.com/videojs/video.js/pull/2668))
|
||||
|
||||
--------------------
|
||||
|
||||
|
@ -28,7 +28,7 @@
|
||||
"object.assign": "^4.0.1",
|
||||
"safe-json-parse": "^4.0.0",
|
||||
"tsml": "1.0.1",
|
||||
"videojs-font": "1.3.0",
|
||||
"videojs-font": "1.4.0",
|
||||
"videojs-ie8": "1.1.0",
|
||||
"videojs-swf": "5.0.0-rc1",
|
||||
"vtt.js": "git+https://github.com/gkatsev/vtt.js.git#vjs-v0.12.1",
|
||||
|
@ -1,3 +1,5 @@
|
||||
@import "utilities/linear-gradient";
|
||||
|
||||
@mixin background-color-with-alpha($color, $alpha) {
|
||||
background-color: $color;
|
||||
background-color: rgba($color, $alpha);
|
||||
@ -94,11 +96,15 @@
|
||||
order: $value;
|
||||
}
|
||||
|
||||
%icon-default {
|
||||
%fill-parent {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
%icon-default {
|
||||
@extend %fill-parent;
|
||||
text-align: center;
|
||||
}
|
||||
|
9
src/css/components/_close-button.scss
Normal file
9
src/css/components/_close-button.scss
Normal file
@ -0,0 +1,9 @@
|
||||
.video-js .vjs-control.vjs-close-button {
|
||||
@extend .vjs-icon-cancel;
|
||||
cursor: pointer;
|
||||
height: 3em;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0.5em;
|
||||
z-index: 2;
|
||||
}
|
@ -1,48 +1,23 @@
|
||||
.vjs-error-display {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vjs-error .vjs-error-display {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.vjs-error .vjs-error-display .vjs-modal-dialog-content {
|
||||
font-size: 1.4em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vjs-error .vjs-error-display:before {
|
||||
color: #fff;
|
||||
content: 'X';
|
||||
font-family: $text-font-family;
|
||||
font-size: 4em;
|
||||
color: #fff;
|
||||
/* In order to center the play icon vertically we need to set the line height
|
||||
to the same as the button height */
|
||||
line-height: 1;
|
||||
text-shadow: 0.05em 0.05em 0.1em #000;
|
||||
text-align: center /* Needed for IE8 */;
|
||||
vertical-align: middle;
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
|
||||
// In order to center the play icon vertically we need to set the line height
|
||||
// to the same as the button height
|
||||
line-height: 1;
|
||||
margin-top: -0.5em;
|
||||
position: absolute;
|
||||
text-shadow: 0.05em 0.05em 0.1em #000;
|
||||
text-align: center; // Needed for IE8
|
||||
top: 50%;
|
||||
vertical-align: middle;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vjs-error-display div {
|
||||
position: absolute;
|
||||
bottom: 1em;
|
||||
right: 0;
|
||||
left: 0;
|
||||
font-size: 1.4em;
|
||||
text-align: center;
|
||||
padding: 3px;
|
||||
|
||||
@include background-color-with-alpha(#000, 0.5);
|
||||
}
|
||||
|
||||
.vjs-error-display a,
|
||||
.vjs-error-display a:visited {
|
||||
color: #66A8CC;
|
||||
}
|
||||
|
@ -125,6 +125,15 @@ body.vjs-full-window {
|
||||
/* Hide disabled or unsupported controls. */
|
||||
.vjs-hidden { display: none !important; }
|
||||
|
||||
// Visually hidden offscreen, but accessible to screen readers.
|
||||
.video-js .vjs-offscreen {
|
||||
height: 1px;
|
||||
left: -9999px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.vjs-lock-showing {
|
||||
display: block !important;
|
||||
opacity: 1;
|
||||
|
13
src/css/components/_modal-dialog.scss
Normal file
13
src/css/components/_modal-dialog.scss
Normal file
@ -0,0 +1,13 @@
|
||||
.video-js .vjs-modal-dialog {
|
||||
@extend %fill-parent;
|
||||
@include linear-gradient(180deg, rgba(0, 0, 0, 0.8), rgba(255, 255, 255, 0));
|
||||
}
|
||||
|
||||
.vjs-modal-dialog .vjs-modal-dialog-content {
|
||||
@extend %fill-parent;
|
||||
|
||||
font-size: 1.2em; // 12px
|
||||
line-height: 1.5; // 18px
|
||||
padding: 30px;
|
||||
z-index: 1;
|
||||
}
|
94
src/css/utilities/_linear-gradient.scss
Normal file
94
src/css/utilities/_linear-gradient.scss
Normal file
@ -0,0 +1,94 @@
|
||||
// These functions and mixins taken from:
|
||||
//
|
||||
// "Building a linear-gradient Mixin in Sass" by Hugo Giraudel
|
||||
// http://www.sitepoint.com/building-linear-gradient-mixin-sass/
|
||||
// http://sassmeister.com/gist/b58f6e2cc3160007c880
|
||||
//
|
||||
|
||||
/// Convert angle
|
||||
/// @author Chris Eppstein
|
||||
/// @param {Number} $value - Value to convert
|
||||
/// @param {String} $unit - Unit to convert to
|
||||
/// @return {Number} Converted angle
|
||||
@function convert-angle($value, $unit) {
|
||||
$convertable-units: deg grad turn rad;
|
||||
$conversion-factors: 1 (10grad/9deg) (1turn/360deg) (3.1415926rad/180deg);
|
||||
@if index($convertable-units, unit($value)) and index($convertable-units, $unit) {
|
||||
@return $value
|
||||
/ nth($conversion-factors, index($convertable-units, unit($value)))
|
||||
* nth($conversion-factors, index($convertable-units, $unit));
|
||||
}
|
||||
|
||||
@warn "Cannot convert `#{unit($value)}` to `#{$unit}`.";
|
||||
}
|
||||
|
||||
/// Test if `$value` is an angle
|
||||
/// @param {*} $value - Value to test
|
||||
/// @return {Bool}
|
||||
@function is-direction($value) {
|
||||
$is-direction: index((
|
||||
'to top',
|
||||
'to top right',
|
||||
'to right top',
|
||||
'to right',
|
||||
'to bottom right',
|
||||
'to right bottom',
|
||||
'to bottom',
|
||||
'to bottom left',
|
||||
'to left bottom',
|
||||
'to left',
|
||||
'to left top',
|
||||
'to top left'
|
||||
), $value);
|
||||
$is-angle: type-of($value) == 'number' and index('deg' 'grad' 'turn' 'rad', unit($value));
|
||||
|
||||
@return $is-direction or $is-angle;
|
||||
}
|
||||
|
||||
/// Convert a direction to legacy syntax
|
||||
/// @param {Keyword | Angle} $value - Value to convert
|
||||
/// @require {function} is-direction
|
||||
/// @require {function} convert-angle
|
||||
@function legacy-direction($value) {
|
||||
@if is-direction($value) == false {
|
||||
@warn "Cannot convert `#{$value}` to legacy syntax because it doesn't seem to be an angle or a direction";
|
||||
}
|
||||
|
||||
$conversion-map: (
|
||||
'to top' : 'bottom',
|
||||
'to top right' : 'bottom left',
|
||||
'to right top' : 'left bottom',
|
||||
'to right' : 'left',
|
||||
'to bottom right' : 'top left',
|
||||
'to right bottom' : 'left top',
|
||||
'to bottom' : 'top',
|
||||
'to bottom left' : 'top right',
|
||||
'to left bottom' : 'right top',
|
||||
'to left' : 'right',
|
||||
'to left top' : 'right bottom',
|
||||
'to top left' : 'bottom right'
|
||||
);
|
||||
|
||||
@if map-has-key($conversion-map, $value) {
|
||||
@return map-get($conversion-map, $value);
|
||||
}
|
||||
|
||||
@return 90deg - convert-angle($value, 'deg');
|
||||
}
|
||||
|
||||
/// Mixin printing a linear-gradient
|
||||
/// as well as a plain color fallback
|
||||
/// and the `-webkit-` prefixed declaration
|
||||
/// @access public
|
||||
/// @param {String | List | Angle} $direction - Linear gradient direction
|
||||
/// @param {Arglist} $color-stops - List of color-stops composing the gradient
|
||||
@mixin linear-gradient($direction, $color-stops...) {
|
||||
@if is-direction($direction) == false {
|
||||
$color-stops: ($direction, $color-stops);
|
||||
$direction: 180deg;
|
||||
}
|
||||
|
||||
background: nth(nth($color-stops, 1), 1);
|
||||
background: -webkit-linear-gradient(legacy-direction($direction), $color-stops);
|
||||
background: linear-gradient($direction, $color-stops);
|
||||
}
|
@ -7,6 +7,7 @@
|
||||
@import "components/layout";
|
||||
@import "components/big-play";
|
||||
@import "components/button";
|
||||
@import "components/close-button";
|
||||
|
||||
@import "components/menu/menu";
|
||||
@import "components/menu/menu-popup";
|
||||
@ -35,3 +36,4 @@
|
||||
@import "components/subtitles";
|
||||
@import "components/adaptive";
|
||||
@import "components/captions-settings";
|
||||
@import "components/modal-dialog";
|
||||
|
28
src/js/close-button.js
Normal file
28
src/js/close-button.js
Normal file
@ -0,0 +1,28 @@
|
||||
import Button from './button';
|
||||
import Component from './component';
|
||||
|
||||
/**
|
||||
* The `CloseButton` component is a button which fires a "close" event
|
||||
* when it is activated.
|
||||
*
|
||||
* @extends Button
|
||||
* @class CloseButton
|
||||
*/
|
||||
class CloseButton extends Button {
|
||||
|
||||
constructor(player, options) {
|
||||
super(player, options);
|
||||
this.controlText(options && options.controlText || this.localize('Close'));
|
||||
}
|
||||
|
||||
buildCSSClass() {
|
||||
return `vjs-close-button ${super.buildCSSClass()}`;
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
this.trigger({type: 'close', bubbles: false});
|
||||
}
|
||||
}
|
||||
|
||||
Component.registerComponent('CloseButton', CloseButton);
|
||||
export default CloseButton;
|
@ -2,53 +2,58 @@
|
||||
* @file error-display.js
|
||||
*/
|
||||
import Component from './component';
|
||||
import * as Dom from './utils/dom.js';
|
||||
import ModalDialog from './modal-dialog';
|
||||
|
||||
import * as Dom from './utils/dom';
|
||||
import mergeOptions from './utils/merge-options';
|
||||
|
||||
/**
|
||||
* Display that an error has occurred making the video unplayable
|
||||
* Display that an error has occurred making the video unplayable.
|
||||
*
|
||||
* @param {Object} player Main Player
|
||||
* @param {Object=} options Object of option names and values
|
||||
* @extends Component
|
||||
* @extends ModalDialog
|
||||
* @class ErrorDisplay
|
||||
*/
|
||||
class ErrorDisplay extends Component {
|
||||
class ErrorDisplay extends ModalDialog {
|
||||
|
||||
/**
|
||||
* Constructor for error display modal.
|
||||
*
|
||||
* @param {Player} player
|
||||
* @param {Object} [options]
|
||||
*/
|
||||
constructor(player, options) {
|
||||
super(player, options);
|
||||
|
||||
this.update();
|
||||
this.on(player, 'error', this.update);
|
||||
this.on(player, 'error', this.open);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the component's DOM element
|
||||
* Include the old class for backward-compatibility.
|
||||
*
|
||||
* @return {Element}
|
||||
* @method createEl
|
||||
* This can be removed in 6.0.
|
||||
*
|
||||
* @method buildCSSClass
|
||||
* @deprecated
|
||||
* @return {String}
|
||||
*/
|
||||
createEl() {
|
||||
var el = super.createEl('div', {
|
||||
className: 'vjs-error-display'
|
||||
});
|
||||
|
||||
this.contentEl_ = Dom.createEl('div');
|
||||
el.appendChild(this.contentEl_);
|
||||
|
||||
return el;
|
||||
buildCSSClass() {
|
||||
return `vjs-error-display ${super.buildCSSClass()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the error message in localized language
|
||||
* Generates the modal content based on the player error.
|
||||
*
|
||||
* @method update
|
||||
* @return {String|Null}
|
||||
*/
|
||||
update() {
|
||||
if (this.player().error()) {
|
||||
this.contentEl_.innerHTML = this.localize(this.player().error().message);
|
||||
}
|
||||
content() {
|
||||
let error = this.player().error();
|
||||
return error ? this.localize(error.message) : '';
|
||||
}
|
||||
}
|
||||
|
||||
ErrorDisplay.prototype.options_ = mergeOptions(ModalDialog.prototype.options_, {
|
||||
fillAlways: true,
|
||||
uncloseable: true
|
||||
});
|
||||
|
||||
Component.registerComponent('ErrorDisplay', ErrorDisplay);
|
||||
export default ErrorDisplay;
|
||||
|
372
src/js/modal-dialog.js
Normal file
372
src/js/modal-dialog.js
Normal file
@ -0,0 +1,372 @@
|
||||
/**
|
||||
* @file modal-dialog.js
|
||||
*/
|
||||
import document from 'global/document';
|
||||
|
||||
import * as Dom from './utils/dom';
|
||||
import * as Fn from './utils/fn';
|
||||
import log from './utils/log';
|
||||
|
||||
import Component from './component';
|
||||
import CloseButton from './close-button';
|
||||
|
||||
const MODAL_CLASS_NAME = 'vjs-modal-dialog';
|
||||
const ESC = 27;
|
||||
|
||||
/**
|
||||
* The `ModalDialog` displays over the video and its controls, which blocks
|
||||
* interaction with the player until it is closed.
|
||||
*
|
||||
* Modal dialogs include a "Close" button and will close when that button
|
||||
* is activated - or when ESC is pressed anywhere.
|
||||
*
|
||||
* @extends Component
|
||||
* @class ModalDialog
|
||||
*/
|
||||
class ModalDialog extends Component {
|
||||
|
||||
/**
|
||||
* Constructor for modals.
|
||||
*
|
||||
* @param {Player} player
|
||||
* @param {Object} [options]
|
||||
* @param {Mixed} [options.content=undefined]
|
||||
* Provide customized content for this modal.
|
||||
*
|
||||
* @param {String} [options.description]
|
||||
* A text description for the modal, primarily for accessibility.
|
||||
*
|
||||
* @param {Boolean} [options.fillAlways=false]
|
||||
* Normally, modals are automatically filled only the first time
|
||||
* they open. This tells the modal to refresh its content
|
||||
* every time it opens.
|
||||
*
|
||||
* @param {String} [options.label]
|
||||
* A text label for the modal, primarily for accessibility.
|
||||
*
|
||||
* @param {Boolean} [options.temporary=true]
|
||||
* If `true`, the modal can only be opened once; it will be
|
||||
* disposed as soon as it's closed.
|
||||
*
|
||||
* @param {Boolean} [options.uncloseable=false]
|
||||
* If `true`, the user will not be able to close the modal
|
||||
* through the UI in the normal ways. Programmatic closing is
|
||||
* still possible.
|
||||
*
|
||||
*/
|
||||
constructor(player, options) {
|
||||
super(player, options);
|
||||
this.opened_ = this.hasBeenOpened_ = this.hasBeenFilled_ = false;
|
||||
|
||||
this.closeable(!this.options_.uncloseable);
|
||||
this.content(this.options_.content);
|
||||
|
||||
// Make sure the contentEl is defined AFTER any children are initialized
|
||||
// because we only want the contents of the modal in the contentEl
|
||||
// (not the UI elements like the close button).
|
||||
this.contentEl_ = Dom.createEl('div', {
|
||||
className: `${MODAL_CLASS_NAME}-content`
|
||||
}, {
|
||||
role: 'document'
|
||||
});
|
||||
|
||||
this.descEl_ = Dom.createEl('p', {
|
||||
className: `${MODAL_CLASS_NAME}-description vjs-offscreen`,
|
||||
id: this.el().getAttribute('aria-describedby')
|
||||
});
|
||||
|
||||
Dom.textContent(this.descEl_, this.description());
|
||||
this.el_.appendChild(this.descEl_);
|
||||
this.el_.appendChild(this.contentEl_);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the modal's DOM element
|
||||
*
|
||||
* @method createEl
|
||||
* @return {Element}
|
||||
*/
|
||||
createEl() {
|
||||
return super.createEl('div', {
|
||||
className: this.buildCSSClass(),
|
||||
tabIndex: -1
|
||||
}, {
|
||||
'aria-describedby': `${this.id()}_description`,
|
||||
'aria-hidden': 'true',
|
||||
'aria-label': this.label(),
|
||||
role: 'dialog'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the modal's CSS class.
|
||||
*
|
||||
* @method buildCSSClass
|
||||
* @return {String}
|
||||
*/
|
||||
buildCSSClass() {
|
||||
return `${MODAL_CLASS_NAME} vjs-hidden ${super.buildCSSClass()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles key presses on the document, looking for ESC, which closes
|
||||
* the modal.
|
||||
*
|
||||
* @method handleKeyPress
|
||||
* @param {Event} e
|
||||
*/
|
||||
handleKeyPress(e) {
|
||||
if (e.which === ESC && this.closeable()) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the label string for this modal. Primarily used for accessibility.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
label() {
|
||||
return this.options_.label || this.localize('Modal Window');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the description string for this modal. Primarily used for
|
||||
* accessibility.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
description() {
|
||||
let desc = this.options_.description || this.localize('This is a modal window.');
|
||||
|
||||
// Append a universal closeability message if the modal is closeable.
|
||||
if (this.closeable()) {
|
||||
desc += ' ' + this.localize('This modal can be closed by pressing the Escape key or activating the close button.');
|
||||
}
|
||||
|
||||
return desc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the modal.
|
||||
*
|
||||
* @method open
|
||||
* @return {ModalDialog}
|
||||
*/
|
||||
open() {
|
||||
if (!this.opened_) {
|
||||
let player = this.player();
|
||||
|
||||
this.trigger('beforemodalopen');
|
||||
this.opened_ = true;
|
||||
|
||||
// Fill content if the modal has never opened before and
|
||||
// never been filled.
|
||||
if (this.options_.fillAlways || !this.hasBeenOpened_ && !this.hasBeenFilled_) {
|
||||
this.fill();
|
||||
}
|
||||
|
||||
// If the player was playing, pause it and take note of its previously
|
||||
// playing state.
|
||||
this.wasPlaying_ = !player.paused();
|
||||
|
||||
if (this.wasPlaying_) {
|
||||
player.pause();
|
||||
}
|
||||
|
||||
if (this.closeable()) {
|
||||
this.on(document, 'keydown', Fn.bind(this, this.handleKeyPress));
|
||||
}
|
||||
|
||||
player.controls(false);
|
||||
this.show();
|
||||
this.el().setAttribute('aria-hidden', 'false');
|
||||
this.trigger('modalopen');
|
||||
this.hasBeenOpened_ = true;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the modal is opened currently.
|
||||
*
|
||||
* @method opened
|
||||
* @param {Boolean} [value]
|
||||
* If given, it will open (`true`) or close (`false`) the modal.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
opened(value) {
|
||||
if (typeof value === 'boolean') {
|
||||
this[value ? 'open' : 'close']();
|
||||
}
|
||||
return this.opened_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the modal.
|
||||
*
|
||||
* @method close
|
||||
* @return {ModalDialog}
|
||||
*/
|
||||
close() {
|
||||
if (this.opened_) {
|
||||
let player = this.player();
|
||||
|
||||
this.trigger('beforemodalclose');
|
||||
this.opened_ = false;
|
||||
|
||||
if (this.wasPlaying_) {
|
||||
player.play();
|
||||
}
|
||||
|
||||
if (this.closeable()) {
|
||||
this.off(document, 'keydown', Fn.bind(this, this.handleKeyPress));
|
||||
}
|
||||
|
||||
player.controls(true);
|
||||
this.hide();
|
||||
this.el().setAttribute('aria-hidden', 'true');
|
||||
this.trigger('modalclose');
|
||||
|
||||
if (this.options_.temporary) {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the modal is closeable via the UI.
|
||||
*
|
||||
* @method closeable
|
||||
* @param {Boolean} [value]
|
||||
* If given as a Boolean, it will set the `closeable` option.
|
||||
*
|
||||
* @return {Boolean}
|
||||
*/
|
||||
closeable(value) {
|
||||
if (typeof value === 'boolean') {
|
||||
let closeable = this.closeable_ = !!value;
|
||||
let close = this.getChild('closeButton');
|
||||
|
||||
// If this is being made closeable and has no close button, add one.
|
||||
if (closeable && !close) {
|
||||
|
||||
// The close button should be a child of the modal - not its
|
||||
// content element, so temporarily change the content element.
|
||||
let temp = this.contentEl_;
|
||||
this.contentEl_ = this.el_;
|
||||
close = this.addChild('closeButton');
|
||||
this.contentEl_ = temp;
|
||||
this.on(close, 'close', this.close);
|
||||
}
|
||||
|
||||
// If this is being made uncloseable and has a close button, remove it.
|
||||
if (!closeable && close) {
|
||||
this.off(close, 'close', this.close);
|
||||
this.removeChild(close);
|
||||
close.dispose();
|
||||
}
|
||||
}
|
||||
return this.closeable_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the modal's content element with the modal's "content" option.
|
||||
*
|
||||
* The content element will be emptied before this change takes place.
|
||||
*
|
||||
* @method fill
|
||||
* @return {ModalDialog}
|
||||
*/
|
||||
fill() {
|
||||
return this.fillWith(this.content());
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the modal's content element with arbitrary content.
|
||||
*
|
||||
* The content element will be emptied before this change takes place.
|
||||
*
|
||||
* @method fillWith
|
||||
* @param {Mixed} [content]
|
||||
* The same rules apply to this as apply to the `content` option.
|
||||
*
|
||||
* @return {ModalDialog}
|
||||
*/
|
||||
fillWith(content) {
|
||||
let contentEl = this.contentEl();
|
||||
let parentEl = contentEl.parentNode;
|
||||
let nextSiblingEl = contentEl.nextSibling;
|
||||
|
||||
this.trigger('beforemodalfill');
|
||||
this.hasBeenFilled_ = true;
|
||||
|
||||
// Detach the content element from the DOM before performing
|
||||
// manipulation to avoid modifying the live DOM multiple times.
|
||||
parentEl.removeChild(contentEl);
|
||||
this.empty();
|
||||
Dom.insertContent(contentEl, content);
|
||||
this.trigger('modalfill');
|
||||
|
||||
// Re-inject the re-filled content element.
|
||||
if (nextSiblingEl) {
|
||||
parentEl.insertBefore(contentEl, nextSiblingEl);
|
||||
} else {
|
||||
parentEl.appendChild(contentEl);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Empties the content element.
|
||||
*
|
||||
* This happens automatically anytime the modal is filled.
|
||||
*
|
||||
* @method empty
|
||||
* @return {ModalDialog}
|
||||
*/
|
||||
empty() {
|
||||
this.trigger('beforemodalempty');
|
||||
Dom.emptyEl(this.contentEl());
|
||||
this.trigger('modalempty');
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets or sets the modal content, which gets normalized before being
|
||||
* rendered into the DOM.
|
||||
*
|
||||
* This does not update the DOM or fill the modal, but it is called during
|
||||
* that process.
|
||||
*
|
||||
* @method content
|
||||
* @param {Mixed} [value]
|
||||
* If defined, sets the internal content value to be used on the
|
||||
* next call(s) to `fill`. This value is normalized before being
|
||||
* inserted. To "clear" the internal content value, pass `null`.
|
||||
*
|
||||
* @return {Mixed}
|
||||
*/
|
||||
content(value) {
|
||||
if (typeof value !== 'undefined') {
|
||||
this.content_ = value;
|
||||
}
|
||||
return this.content_;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Modal dialog default options.
|
||||
*
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
ModalDialog.prototype.options_ = {
|
||||
temporary: true
|
||||
};
|
||||
|
||||
Component.registerComponent('ModalDialog', ModalDialog);
|
||||
export default ModalDialog;
|
@ -32,6 +32,7 @@ import BigPlayButton from './big-play-button.js';
|
||||
import ControlBar from './control-bar/control-bar.js';
|
||||
import ErrorDisplay from './error-display.js';
|
||||
import TextTrackSettings from './tracks/text-track-settings.js';
|
||||
import ModalDialog from './modal-dialog';
|
||||
|
||||
// Require html5 tech, at least for disposing the original video tag
|
||||
import Html5 from './tech/html5.js';
|
||||
@ -2513,6 +2514,38 @@ class Player extends Component {
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a simple modal dialog (an instance of the `ModalDialog`
|
||||
* component) that immediately overlays the player with arbitrary
|
||||
* content and removes itself when closed.
|
||||
*
|
||||
* @param {String|Function|Element|Array|Null} content
|
||||
* Same as `ModalDialog#content`'s param of the same name.
|
||||
*
|
||||
* The most straight-forward usage is to provide a string or DOM
|
||||
* element.
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* Extra options which will be passed on to the `ModalDialog`.
|
||||
*
|
||||
* @return {ModalDialog}
|
||||
*/
|
||||
createModal(content, options) {
|
||||
let player = this;
|
||||
|
||||
options = options || {};
|
||||
options.content = content || '';
|
||||
|
||||
let modal = new ModalDialog(player, options);
|
||||
|
||||
player.addChild(modal);
|
||||
modal.on('dispose', function() {
|
||||
player.removeChild(modal);
|
||||
});
|
||||
|
||||
return modal.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets tag settings
|
||||
*
|
||||
|
@ -58,6 +58,22 @@ export function createEl(tagName='div', properties={}, attributes={}){
|
||||
return el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects text into an element, replacing any existing contents entirely.
|
||||
*
|
||||
* @param {Element} el
|
||||
* @param {String} text
|
||||
* @return {Element}
|
||||
* @function textContent
|
||||
*/
|
||||
export function textContent(el, text) {
|
||||
if (typeof el.textContent === 'undefined') {
|
||||
el.innerText = text;
|
||||
} else {
|
||||
el.textContent = text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert an element as the first child node of another
|
||||
*
|
||||
@ -369,3 +385,112 @@ export function getPointerPosition(el, event) {
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines, via duck typing, whether or not a value is a DOM element.
|
||||
*
|
||||
* @param {Mixed} value
|
||||
* @return {Boolean}
|
||||
*/
|
||||
export function isEl(value) {
|
||||
return !!value && typeof value === 'object' && value.nodeType === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines, via duck typing, whether or not a value is a text node.
|
||||
*
|
||||
* @param {Mixed} value
|
||||
* @return {Boolean}
|
||||
*/
|
||||
export function isTextNode(value) {
|
||||
return !!value && typeof value === 'object' && value.nodeType === 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Empties the contents of an element.
|
||||
*
|
||||
* @function emptyEl
|
||||
* @param {Element} el
|
||||
* @return {Element}
|
||||
*/
|
||||
export function emptyEl(el) {
|
||||
while (el.firstChild) {
|
||||
el.removeChild(el.firstChild);
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes content for eventual insertion into the DOM.
|
||||
*
|
||||
* This allows a wide range of content definition methods, but protects
|
||||
* from falling into the trap of simply writing to `innerHTML`, which is
|
||||
* an XSS concern.
|
||||
*
|
||||
* The content for an element can be passed in multiple types, whose
|
||||
* behavior is as follows:
|
||||
*
|
||||
* - String: Normalized into a text node.
|
||||
* - Node: An Element or TextNode is passed through.
|
||||
* - Array: A one-dimensional array of strings, nodes, or functions (which
|
||||
* return single strings or nodes).
|
||||
* - Function: If the sole argument, is expected to produce a string, node,
|
||||
* or array.
|
||||
*
|
||||
* @function normalizeContent
|
||||
* @param {String|Element|Array|Function} content
|
||||
* @return {Array}
|
||||
*/
|
||||
export function normalizeContent(content) {
|
||||
|
||||
// First, invoke content if it is a function. If it produces an array,
|
||||
// that needs to happen before normalization.
|
||||
if (typeof content === 'function') {
|
||||
content = content();
|
||||
}
|
||||
|
||||
// Next up, normalize to an array, so one or many items can be normalized,
|
||||
// filtered, and returned.
|
||||
return (Array.isArray(content) ? content : [content]).map(value => {
|
||||
|
||||
// First, invoke value if it is a function to produce a new value,
|
||||
// which will be subsequently normalized to a Node of some kind.
|
||||
if (typeof value === 'function') {
|
||||
value = value();
|
||||
}
|
||||
|
||||
if (isEl(value) || isTextNode(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && /\S/.test(value)) {
|
||||
return document.createTextNode(value);
|
||||
}
|
||||
}).filter(value => value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes and appends content to an element.
|
||||
*
|
||||
* @function appendContent
|
||||
* @param {Element} el
|
||||
* @param {String|Element|Array|Function} content
|
||||
* @return {Element}
|
||||
*/
|
||||
export function appendContent(el, content) {
|
||||
normalizeContent(content).forEach(node => el.appendChild(node));
|
||||
return el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes and inserts content into an element; this is identical to
|
||||
* `appendContent()`, except it empties the element first.
|
||||
*
|
||||
* @function insertContent
|
||||
* @param {Element} el
|
||||
* @param {String|Element|Array|Function} content
|
||||
* @return {Element}
|
||||
*/
|
||||
export function insertContent(el, content) {
|
||||
return appendContent(emptyEl(el), content);
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
<div id="qunit"></div>
|
||||
<script src="../node_modules/qunitjs/qunit/qunit.js"></script>
|
||||
|
||||
<script src="../build/temp/ie8/videojs-ie8.min.js"></script>
|
||||
<script src="../build/temp/ie8/videojs-ie8.js"></script>
|
||||
|
||||
<!-- Execute the bundled tests first -->
|
||||
<script src="../build/temp/tests.js"></script>
|
||||
|
47
test/unit/close-button.test.js
Normal file
47
test/unit/close-button.test.js
Normal file
@ -0,0 +1,47 @@
|
||||
import CloseButton from '../../src/js/close-button';
|
||||
import TestHelpers from './test-helpers';
|
||||
|
||||
q.module('CloseButton', {
|
||||
|
||||
beforeEach: function() {
|
||||
this.player = TestHelpers.makePlayer();
|
||||
this.btn = new CloseButton(this.player);
|
||||
},
|
||||
|
||||
afterEach: function() {
|
||||
this.player.dispose();
|
||||
this.btn.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
q.test('should create the expected element', function(assert) {
|
||||
let elAssertions = TestHelpers.assertEl(assert, this.btn.el(), {
|
||||
tagName: 'button',
|
||||
classes: [
|
||||
'vjs-button',
|
||||
'vjs-close-button',
|
||||
'vjs-control'
|
||||
]
|
||||
});
|
||||
|
||||
assert.expect(elAssertions.count + 1);
|
||||
elAssertions();
|
||||
assert.strictEqual(this.btn.el().querySelector('.vjs-control-text').innerHTML, 'Close');
|
||||
});
|
||||
|
||||
q.test('should allow setting the controlText_ property as an option', function(assert) {
|
||||
var text = 'OK!';
|
||||
var btn = new CloseButton(this.player, {controlText: text});
|
||||
|
||||
assert.expect(1);
|
||||
assert.strictEqual(btn.controlText_, text, 'set the controlText_ property');
|
||||
});
|
||||
|
||||
q.test('should trigger an event on activation', function(assert) {
|
||||
var spy = sinon.spy();
|
||||
|
||||
this.btn.on('close', spy);
|
||||
this.btn.trigger('click');
|
||||
assert.expect(1);
|
||||
assert.strictEqual(spy.callCount, 1, 'the "close" event was triggered');
|
||||
});
|
398
test/unit/modal-dialog.test.js
Normal file
398
test/unit/modal-dialog.test.js
Normal file
@ -0,0 +1,398 @@
|
||||
import CloseButton from '../../src/js/close-button';
|
||||
import ModalDialog from '../../src/js/modal-dialog';
|
||||
import * as Dom from '../../src/js/utils/dom';
|
||||
import * as Fn from '../../src/js/utils/fn';
|
||||
import TestHelpers from './test-helpers';
|
||||
|
||||
var ESC = 27;
|
||||
|
||||
q.module('ModalDialog', {
|
||||
|
||||
beforeEach: function() {
|
||||
this.player = TestHelpers.makePlayer();
|
||||
this.modal = new ModalDialog(this.player, {temporary: false});
|
||||
this.el = this.modal.el();
|
||||
},
|
||||
|
||||
afterEach: function() {
|
||||
this.player.dispose();
|
||||
this.modal.dispose();
|
||||
this.el = null;
|
||||
}
|
||||
});
|
||||
|
||||
q.test('should create the expected element', function(assert) {
|
||||
let elAssertions = TestHelpers.assertEl(assert, this.el, {
|
||||
tagName: 'div',
|
||||
classes: [
|
||||
'vjs-modal-dialog',
|
||||
'vjs-hidden'
|
||||
],
|
||||
attrs: {
|
||||
'aria-describedby': this.modal.descEl_.id,
|
||||
'aria-hidden': 'true',
|
||||
'aria-label': this.modal.label(),
|
||||
'role': 'dialog'
|
||||
},
|
||||
props: {
|
||||
tabIndex: -1
|
||||
}
|
||||
});
|
||||
|
||||
assert.expect(elAssertions.count);
|
||||
elAssertions();
|
||||
});
|
||||
|
||||
q.test('should create the expected description element', function(assert) {
|
||||
let elAssertions = TestHelpers.assertEl(assert, this.modal.descEl_, {
|
||||
tagName: 'p',
|
||||
innerHTML: this.modal.description(),
|
||||
classes: [
|
||||
'vjs-modal-dialog-description',
|
||||
'vjs-offscreen'
|
||||
],
|
||||
attrs: {
|
||||
id: this.el.getAttribute('aria-describedby')
|
||||
}
|
||||
});
|
||||
|
||||
assert.expect(elAssertions.count);
|
||||
elAssertions();
|
||||
});
|
||||
|
||||
q.test('should create the expected contentEl', function(assert) {
|
||||
let elAssertions = TestHelpers.assertEl(assert, this.modal.contentEl(), {
|
||||
tagName: 'div',
|
||||
classes: [
|
||||
'vjs-modal-dialog-content'
|
||||
],
|
||||
props: {
|
||||
parentNode: this.el
|
||||
}
|
||||
});
|
||||
|
||||
assert.expect(elAssertions.count);
|
||||
elAssertions();
|
||||
});
|
||||
|
||||
q.test('should create a close button by default', function(assert) {
|
||||
var btn = this.modal.getChild('closeButton');
|
||||
|
||||
// We only check the aspects of the button that relate to the modal. Other
|
||||
// aspects of the button (classes, etc) are tested in their appropriate test
|
||||
// module.
|
||||
assert.expect(2);
|
||||
assert.ok(btn instanceof CloseButton, 'close button is a CloseButton');
|
||||
assert.strictEqual(btn.el().parentNode, this.el, 'close button is a child of el');
|
||||
});
|
||||
|
||||
q.test('returns `this` for expected methods', function(assert) {
|
||||
var methods = ['close', 'empty', 'fill', 'fillWith', 'open'];
|
||||
|
||||
assert.expect(methods.length);
|
||||
methods.forEach(function(method) {
|
||||
assert.strictEqual(this[method](), this, '`' + method + '()` returns `this`');
|
||||
}, this.modal);
|
||||
});
|
||||
|
||||
q.test('open() triggers events', function(assert) {
|
||||
var modal = this.modal;
|
||||
var beforeModalOpenSpy = sinon.spy(function() {
|
||||
assert.notOk(modal.opened(), 'modal is not opened before opening event');
|
||||
});
|
||||
|
||||
var modalOpenSpy = sinon.spy(function() {
|
||||
assert.ok(modal.opened(), 'modal is opened on opening event');
|
||||
});
|
||||
|
||||
assert.expect(4);
|
||||
|
||||
modal.
|
||||
on('beforemodalopen', beforeModalOpenSpy).
|
||||
on('modalopen', modalOpenSpy).
|
||||
open();
|
||||
|
||||
assert.strictEqual(beforeModalOpenSpy.callCount, 1, 'beforemodalopen spy was called');
|
||||
assert.strictEqual(modalOpenSpy.callCount, 1, 'modalopen spy was called');
|
||||
});
|
||||
|
||||
q.test('open() removes "vjs-hidden" class', function(assert) {
|
||||
assert.expect(2);
|
||||
assert.ok(this.modal.hasClass('vjs-hidden'), 'modal starts hidden');
|
||||
this.modal.open();
|
||||
assert.notOk(this.modal.hasClass('vjs-hidden'), 'modal is not hidden after opening');
|
||||
});
|
||||
|
||||
q.test('open() cannot be called on an opened modal', function(assert) {
|
||||
var spy = sinon.spy();
|
||||
|
||||
this.modal.on('modalopen', spy).open().open();
|
||||
|
||||
assert.expect(1);
|
||||
assert.strictEqual(spy.callCount, 1, 'modal was only opened once');
|
||||
});
|
||||
|
||||
q.test('close() triggers events', function(assert) {
|
||||
var modal = this.modal;
|
||||
var beforeModalCloseSpy = sinon.spy(function() {
|
||||
assert.ok(modal.opened(), 'modal is not closed before closing event');
|
||||
});
|
||||
|
||||
var modalCloseSpy = sinon.spy(function() {
|
||||
assert.notOk(modal.opened(), 'modal is closed on closing event');
|
||||
});
|
||||
|
||||
assert.expect(4);
|
||||
|
||||
modal.
|
||||
on('beforemodalclose', beforeModalCloseSpy).
|
||||
on('modalclose', modalCloseSpy).
|
||||
open().
|
||||
close();
|
||||
|
||||
assert.strictEqual(beforeModalCloseSpy.callCount, 1, 'beforemodalclose spy was called');
|
||||
assert.strictEqual(modalCloseSpy.callCount, 1, 'modalclose spy was called');
|
||||
});
|
||||
|
||||
q.test('close() adds the "vjs-hidden" class', function(assert) {
|
||||
assert.expect(1);
|
||||
this.modal.open().close();
|
||||
assert.ok(this.modal.hasClass('vjs-hidden'), 'modal is hidden upon close');
|
||||
});
|
||||
|
||||
q.test('pressing ESC triggers close(), but only when the modal is opened', function(assert) {
|
||||
var spy = sinon.spy();
|
||||
|
||||
this.modal.on('modalclose', spy).handleKeyPress({which: ESC});
|
||||
assert.expect(2);
|
||||
assert.strictEqual(spy.callCount, 0, 'ESC did not close the closed modal');
|
||||
|
||||
this.modal.open().handleKeyPress({which: ESC});
|
||||
assert.strictEqual(spy.callCount, 1, 'ESC closed the now-opened modal');
|
||||
});
|
||||
|
||||
q.test('close() cannot be called on a closed modal', function(assert) {
|
||||
var spy = sinon.spy();
|
||||
|
||||
this.modal.on('modalclose', spy);
|
||||
this.modal.open().close().close();
|
||||
|
||||
assert.expect(1);
|
||||
assert.strictEqual(spy.callCount, 1, 'modal was only closed once');
|
||||
});
|
||||
|
||||
q.test('open() pauses playback, close() resumes', function(assert) {
|
||||
var playSpy = sinon.spy();
|
||||
var pauseSpy = sinon.spy();
|
||||
|
||||
// Quick and dirty; make it looks like the player is playing.
|
||||
this.player.paused = function() {
|
||||
return false;
|
||||
};
|
||||
|
||||
this.player.play = function() {
|
||||
playSpy();
|
||||
};
|
||||
|
||||
this.player.pause = function() {
|
||||
pauseSpy();
|
||||
};
|
||||
|
||||
this.modal.open();
|
||||
|
||||
assert.expect(2);
|
||||
assert.strictEqual(pauseSpy.callCount, 1, 'player is paused when the modal opens');
|
||||
|
||||
this.modal.close();
|
||||
assert.strictEqual(playSpy.callCount, 1, 'player is resumed when the modal closes');
|
||||
});
|
||||
|
||||
q.test('open() hides controls, close() shows controls', function(assert) {
|
||||
this.modal.open();
|
||||
|
||||
assert.expect(2);
|
||||
assert.notOk(this.player.controls_, 'controls are hidden');
|
||||
|
||||
this.modal.close();
|
||||
assert.ok(this.player.controls_, 'controls are no longer hidden');
|
||||
});
|
||||
|
||||
q.test('opened()', function(assert) {
|
||||
var openSpy = sinon.spy();
|
||||
var closeSpy = sinon.spy();
|
||||
|
||||
assert.expect(4);
|
||||
assert.strictEqual(this.modal.opened(), false, 'the modal is closed');
|
||||
this.modal.open();
|
||||
assert.strictEqual(this.modal.opened(), true, 'the modal is open');
|
||||
|
||||
this.modal.
|
||||
close().
|
||||
on('modalopen', openSpy).
|
||||
on('modalclose', closeSpy).
|
||||
opened(true);
|
||||
|
||||
this.modal.opened(true);
|
||||
this.modal.opened(false);
|
||||
assert.strictEqual(openSpy.callCount, 1, 'modal was opened only once');
|
||||
assert.strictEqual(closeSpy.callCount, 1, 'modal was closed only once');
|
||||
});
|
||||
|
||||
q.test('content()', function(assert) {
|
||||
var content;
|
||||
|
||||
assert.expect(3);
|
||||
assert.strictEqual(typeof this.modal.content(), 'undefined', 'no content by default');
|
||||
|
||||
content = this.modal.content(Dom.createEl());
|
||||
assert.ok(Dom.isEl(content), 'content was set from a single DOM element');
|
||||
|
||||
assert.strictEqual(this.modal.content(null), null, 'content was nullified');
|
||||
});
|
||||
|
||||
q.test('fillWith()', function(assert) {
|
||||
var contentEl = this.modal.contentEl();
|
||||
var children = [Dom.createEl(), Dom.createEl(), Dom.createEl()];
|
||||
var beforeFillSpy = sinon.spy();
|
||||
var fillSpy = sinon.spy();
|
||||
|
||||
children.forEach(function(el) {
|
||||
contentEl.appendChild(el);
|
||||
});
|
||||
|
||||
this.modal.
|
||||
on('beforemodalfill', beforeFillSpy).
|
||||
on('modalfill', fillSpy).
|
||||
fillWith(children);
|
||||
|
||||
assert.expect(5 + children.length);
|
||||
assert.strictEqual(contentEl.children.length, children.length, 'has the right number of children');
|
||||
|
||||
children.forEach(function(el) {
|
||||
assert.strictEqual(el.parentNode, contentEl, 'new child appended');
|
||||
});
|
||||
|
||||
assert.strictEqual(beforeFillSpy.callCount, 1, 'the "beforemodalfill" callback was called');
|
||||
assert.strictEqual(beforeFillSpy.getCall(0).thisValue, this.modal, 'the value of "this" is the modal');
|
||||
assert.strictEqual(fillSpy.callCount, 1, 'the "modalfill" callback was called');
|
||||
assert.strictEqual(fillSpy.getCall(0).thisValue, this.modal, 'the value of "this" is the modal');
|
||||
});
|
||||
|
||||
q.test('empty()', function(assert) {
|
||||
var beforeEmptySpy = sinon.spy();
|
||||
var emptySpy = sinon.spy();
|
||||
|
||||
this.modal.
|
||||
fillWith([Dom.createEl(), Dom.createEl()]).
|
||||
on('beforemodalempty', beforeEmptySpy).
|
||||
on('modalempty', emptySpy).
|
||||
empty();
|
||||
|
||||
assert.expect(5);
|
||||
assert.strictEqual(this.modal.contentEl().children.length, 0, 'removed all `contentEl()` children');
|
||||
assert.strictEqual(beforeEmptySpy.callCount, 1, 'the "beforemodalempty" callback was called');
|
||||
assert.strictEqual(beforeEmptySpy.getCall(0).thisValue, this.modal, 'the value of "this" is the modal');
|
||||
assert.strictEqual(emptySpy.callCount, 1, 'the "modalempty" callback was called');
|
||||
assert.strictEqual(emptySpy.getCall(0).thisValue, this.modal, 'the value of "this" is the modal');
|
||||
});
|
||||
|
||||
q.test('closeable()', function(assert) {
|
||||
let initialCloseButton = this.modal.getChild('closeButton');
|
||||
|
||||
assert.expect(8);
|
||||
assert.strictEqual(this.modal.closeable(), true, 'the modal is closed');
|
||||
|
||||
this.modal.open().closeable(false);
|
||||
assert.notOk(this.modal.getChild('closeButton'), 'the close button is no longer a child of the modal');
|
||||
assert.notOk(initialCloseButton.el(), 'the initial close button was disposed');
|
||||
|
||||
this.modal.handleKeyPress({which: ESC});
|
||||
assert.ok(this.modal.opened(), 'the modal was not closed by the ESC key');
|
||||
|
||||
this.modal.close();
|
||||
assert.notOk(this.modal.opened(), 'the modal was closed programmatically');
|
||||
|
||||
this.modal.open().closeable(true);
|
||||
assert.ok(this.modal.getChild('closeButton'), 'a new close button was created');
|
||||
|
||||
this.modal.getChild('closeButton').trigger('click');
|
||||
assert.notOk(this.modal.opened(), 'the modal was closed by the new close button');
|
||||
|
||||
this.modal.open().handleKeyPress({which: ESC});
|
||||
assert.notOk(this.modal.opened(), 'the modal was closed by the ESC key');
|
||||
});
|
||||
|
||||
q.test('"content" option (fills on first open() invocation)', function(assert) {
|
||||
var modal = new ModalDialog(this.player, {
|
||||
content: Dom.createEl(),
|
||||
temporary: false
|
||||
});
|
||||
|
||||
var spy = sinon.spy();
|
||||
|
||||
modal.on('modalfill', spy);
|
||||
modal.open().close().open();
|
||||
|
||||
assert.expect(3);
|
||||
assert.strictEqual(modal.content(), modal.options_.content, 'has the expected content');
|
||||
assert.strictEqual(spy.callCount, 1, 'auto-fills only once');
|
||||
assert.strictEqual(modal.contentEl().firstChild, modal.options_.content, 'has the expected content in the DOM');
|
||||
});
|
||||
|
||||
q.test('"temporary" option', function(assert) {
|
||||
var temp = new ModalDialog(this.player, {temporary: true});
|
||||
var tempSpy = sinon.spy();
|
||||
var perm = new ModalDialog(this.player, {temporary: false});
|
||||
var permSpy = sinon.spy();
|
||||
|
||||
temp.on('dispose', tempSpy);
|
||||
perm.on('dispose', permSpy);
|
||||
temp.open().close();
|
||||
perm.open().close();
|
||||
|
||||
assert.expect(2);
|
||||
assert.strictEqual(tempSpy.callCount, 1, 'temporary modals are disposed');
|
||||
assert.strictEqual(permSpy.callCount, 0, 'permanent modals are not disposed');
|
||||
});
|
||||
|
||||
q.test('"fillAlways" option', function(assert) {
|
||||
var modal = new ModalDialog(this.player, {
|
||||
content: 'foo',
|
||||
fillAlways: true,
|
||||
temporary: false
|
||||
});
|
||||
|
||||
var spy = sinon.spy();
|
||||
|
||||
modal.on('modalfill', spy);
|
||||
modal.open().close().open();
|
||||
|
||||
assert.expect(1);
|
||||
assert.strictEqual(spy.callCount, 2, 'the modal was filled on each open call');
|
||||
});
|
||||
|
||||
q.test('"label" option', function(assert) {
|
||||
var label = 'foo';
|
||||
var modal = new ModalDialog(this.player, {label: label});
|
||||
|
||||
assert.expect(1);
|
||||
assert.strictEqual(modal.el().getAttribute('aria-label'), label, 'uses the label as the aria-label');
|
||||
});
|
||||
|
||||
q.test('"uncloseable" option', function(assert) {
|
||||
var modal = new ModalDialog(this.player, {
|
||||
temporary: false,
|
||||
uncloseable: true
|
||||
});
|
||||
|
||||
var spy = sinon.spy();
|
||||
|
||||
modal.on('modalclose', spy);
|
||||
|
||||
assert.expect(3);
|
||||
assert.strictEqual(modal.closeable(), false, 'the modal is uncloseable');
|
||||
assert.notOk(modal.getChild('closeButton'), 'the close button is not present');
|
||||
|
||||
modal.open().handleKeyPress({which: ESC});
|
||||
assert.strictEqual(spy.callCount, 0, 'ESC did not close the modal');
|
||||
});
|
@ -833,3 +833,29 @@ test('should return correct values for canPlayType', function(){
|
||||
|
||||
player.dispose();
|
||||
});
|
||||
|
||||
test('createModal()', function() {
|
||||
var player = TestHelpers.makePlayer();
|
||||
var modal = player.createModal('foo');
|
||||
var spy = sinon.spy();
|
||||
|
||||
modal.on('dispose', spy);
|
||||
|
||||
expect(5);
|
||||
strictEqual(modal.el().parentNode, player.el(), 'the modal is injected into the player');
|
||||
strictEqual(modal.content(), 'foo', 'content is set properly');
|
||||
ok(modal.opened(), 'modal is opened by default');
|
||||
modal.close();
|
||||
ok(spy.called, 'modal was disposed when closed');
|
||||
strictEqual(player.children().indexOf(modal), -1, 'modal was removed from player\'s children');
|
||||
});
|
||||
|
||||
test('createModal() options object', function() {
|
||||
var player = TestHelpers.makePlayer();
|
||||
var modal = player.createModal('foo', {content: 'bar', label: 'boo'});
|
||||
|
||||
expect(2);
|
||||
strictEqual(modal.content(), 'foo', 'content argument takes precedence');
|
||||
strictEqual(modal.options_.label, 'boo', 'modal options are set properly');
|
||||
modal.close();
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
import * as Dom from '../../src/js/utils/dom';
|
||||
import Player from '../../src/js/player.js';
|
||||
import TechFaker from './tech/tech-faker.js';
|
||||
import window from 'global/window';
|
||||
@ -40,6 +41,90 @@ var TestHelpers = {
|
||||
return el.currentStyle[rule];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Runs a range of assertions on a DOM element.
|
||||
*
|
||||
* @param {QUnit.Assert} assert
|
||||
* @param {Element} el
|
||||
* @param {Object} spec
|
||||
* An object from which assertions are generated.
|
||||
*
|
||||
* @param {Object} [spec.attrs]
|
||||
* An object mapping attribute names (keys) to strict values.
|
||||
*
|
||||
* @param {Array} [spec.classes]
|
||||
* An array of classes that are expected on the element.
|
||||
*
|
||||
* @param {String} [spec.innerHTML]
|
||||
* A string of text/html that is expected as the content of element.
|
||||
* Both values will be trimmed, but remains case-sensitive.
|
||||
*
|
||||
* @param {Object} [spec.props]
|
||||
* An object mapping property names (keys) to strict values.
|
||||
*
|
||||
* @param {String} [spec.tagName]
|
||||
* A string (case-insensitive) representing that element's tagName.
|
||||
*
|
||||
* @return {Function}
|
||||
* Invoke the returned function to run the assertions. This
|
||||
* function has a `count` property which can be used to
|
||||
* reference how many assertions will be run (e.g. for use
|
||||
* with `assert.expect()`).
|
||||
*/
|
||||
assertEl: function(assert, el, spec) {
|
||||
let attrs = spec.attrs ? Object.keys(spec.attrs) : [];
|
||||
let classes = spec.classes || [];
|
||||
let innerHTML = spec.innerHTML ? spec.innerHTML.trim() : '';
|
||||
let props = spec.props ? Object.keys(spec.props) : [];
|
||||
let tagName = spec.tagName ? spec.tagName.toLowerCase() : '';
|
||||
|
||||
// Return value is a function, which runs through all the combined
|
||||
// assertions. This is done so that the count can be attached dynamically
|
||||
// and run whenever desired.
|
||||
let run = () => {
|
||||
if (tagName) {
|
||||
let elTagName = el.tagName.toLowerCase();
|
||||
let msg = `el should have been a <${tagName}> and was a <${elTagName}>`;
|
||||
assert.strictEqual(elTagName, tagName, msg);
|
||||
}
|
||||
|
||||
if (innerHTML) {
|
||||
let elInnerHTML = el.innerHTML.trim();
|
||||
let msg = `el should have expected HTML content`;
|
||||
assert.strictEqual(elInnerHTML, innerHTML, msg);
|
||||
}
|
||||
|
||||
attrs.forEach(a => {
|
||||
let actual = el.getAttribute(a);
|
||||
let expected = spec.attrs[a];
|
||||
let msg = `el should have the "${a}" attribute with the value "${expected}" and it was "${actual}"`;
|
||||
assert.strictEqual(actual, expected, msg);
|
||||
});
|
||||
|
||||
classes.forEach(c => {
|
||||
let msg = `el should have the "${c}" class in its className, which is "${el.className}"`;
|
||||
assert.ok(Dom.hasElClass(el, c), msg);
|
||||
});
|
||||
|
||||
props.forEach(p => {
|
||||
let actual = el[p];
|
||||
let expected = spec.props[p];
|
||||
let msg = `el should have the "${p}" property with the value "${expected}" and it was "${actual}"`;
|
||||
assert.strictEqual(actual, expected, msg);
|
||||
});
|
||||
};
|
||||
|
||||
// Include the number of assertions to run, so it can be used to set
|
||||
// expectations (via `assert.expect()`).
|
||||
run.count = Number(!!tagName) +
|
||||
Number(!!innerHTML) +
|
||||
classes.length +
|
||||
attrs.length +
|
||||
props.length;
|
||||
|
||||
return run;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -160,3 +160,137 @@ test('Dom.findElPosition should find top and left position', function() {
|
||||
position = Dom.findElPosition(d);
|
||||
deepEqual(position, {left: 0, top: 0}, 'If there is no gBCR, we should get zeros');
|
||||
});
|
||||
|
||||
test('Dom.isEl', function(assert) {
|
||||
assert.expect(7);
|
||||
assert.notOk(Dom.isEl(), 'undefined is not an element');
|
||||
assert.notOk(Dom.isEl(true), 'booleans are not elements');
|
||||
assert.notOk(Dom.isEl({}), 'objects are not elements');
|
||||
assert.notOk(Dom.isEl([]), 'arrays are not elements');
|
||||
assert.notOk(Dom.isEl('<h1></h1>'), 'strings are not elements');
|
||||
assert.ok(Dom.isEl(document.createElement('div')), 'elements are elements');
|
||||
assert.ok(Dom.isEl({nodeType: 1}), 'duck typing is imperfect');
|
||||
});
|
||||
|
||||
test('Dom.isTextNode', function(assert) {
|
||||
assert.expect(7);
|
||||
assert.notOk(Dom.isTextNode(), 'undefined is not a text node');
|
||||
assert.notOk(Dom.isTextNode(true), 'booleans are not text nodes');
|
||||
assert.notOk(Dom.isTextNode({}), 'objects are not text nodes');
|
||||
assert.notOk(Dom.isTextNode([]), 'arrays are not text nodes');
|
||||
assert.notOk(Dom.isTextNode('hola mundo'), 'strings are not text nodes');
|
||||
assert.ok(Dom.isTextNode(document.createTextNode('hello, world!')), 'text nodes are text nodes');
|
||||
assert.ok(Dom.isTextNode({nodeType: 3}), 'duck typing is imperfect');
|
||||
});
|
||||
|
||||
test('Dom.emptyEl', function(assert) {
|
||||
let el = Dom.createEl();
|
||||
|
||||
el.appendChild(Dom.createEl('span'));
|
||||
el.appendChild(Dom.createEl('span'));
|
||||
el.appendChild(document.createTextNode('hola mundo'));
|
||||
el.appendChild(Dom.createEl('span'));
|
||||
|
||||
Dom.emptyEl(el);
|
||||
|
||||
assert.expect(1);
|
||||
assert.notOk(el.firstChild, 'the element was emptied');
|
||||
});
|
||||
|
||||
test('Dom.normalizeContent: strings and elements/nodes', function(assert) {
|
||||
assert.expect(8);
|
||||
|
||||
let str = Dom.normalizeContent('hello');
|
||||
assert.strictEqual(str[0].data, 'hello', 'single string becomes a text node');
|
||||
|
||||
let elem = Dom.normalizeContent(Dom.createEl());
|
||||
assert.ok(Dom.isEl(elem[0]), 'an element is passed through');
|
||||
|
||||
let node = Dom.normalizeContent(document.createTextNode('goodbye'));
|
||||
assert.strictEqual(node[0].data, 'goodbye', 'a text node is passed through');
|
||||
|
||||
assert.strictEqual(Dom.normalizeContent(null).length, 0, 'falsy values are ignored');
|
||||
assert.strictEqual(Dom.normalizeContent(false).length, 0, 'falsy values are ignored');
|
||||
assert.strictEqual(Dom.normalizeContent().length, 0, 'falsy values are ignored');
|
||||
assert.strictEqual(Dom.normalizeContent(123).length, 0, 'numbers are ignored');
|
||||
assert.strictEqual(Dom.normalizeContent({}).length, 0, 'objects are ignored');
|
||||
});
|
||||
|
||||
test('Dom.normalizeContent: functions returning strings and elements/nodes', function(assert) {
|
||||
assert.expect(9);
|
||||
|
||||
let str = Dom.normalizeContent(() => 'hello');
|
||||
assert.strictEqual(str[0].data, 'hello', 'a function can return a string, which becomes a text node');
|
||||
|
||||
let elem = Dom.normalizeContent(() => Dom.createEl());
|
||||
assert.ok(Dom.isEl(elem[0]), 'a function can return an element');
|
||||
|
||||
let node = Dom.normalizeContent(() => document.createTextNode('goodbye'));
|
||||
assert.strictEqual(node[0].data, 'goodbye', 'a function can return a text node');
|
||||
|
||||
assert.strictEqual(Dom.normalizeContent(() => null).length, 0, 'a function CANNOT return falsy values');
|
||||
assert.strictEqual(Dom.normalizeContent(() => false).length, 0, 'a function CANNOT return falsy values');
|
||||
assert.strictEqual(Dom.normalizeContent(() => undefined).length, 0, 'a function CANNOT return falsy values');
|
||||
assert.strictEqual(Dom.normalizeContent(() => 123).length, 0, 'a function CANNOT return numbers');
|
||||
assert.strictEqual(Dom.normalizeContent(() => {}).length, 0, 'a function CANNOT return objects');
|
||||
assert.strictEqual(Dom.normalizeContent(() => (() => null)).length, 0, 'a function CANNOT return a function');
|
||||
});
|
||||
|
||||
test('Dom.normalizeContent: arrays of strings and objects', function(assert) {
|
||||
assert.expect(7);
|
||||
|
||||
let source = [
|
||||
'hello',
|
||||
{},
|
||||
Dom.createEl(),
|
||||
['oops'],
|
||||
null,
|
||||
document.createTextNode('goodbye'),
|
||||
() => 'it works'
|
||||
];
|
||||
|
||||
let result = Dom.normalizeContent(source);
|
||||
|
||||
assert.strictEqual(result[0].data, 'hello', 'an array can include a string normalized to a text node');
|
||||
assert.ok(Dom.isEl(result[1]), 'an array can include an element');
|
||||
assert.strictEqual(result[2].data, 'goodbye', 'an array can include a text node');
|
||||
assert.strictEqual(result[3].data, 'it works', 'an array can include a function, which is invoked');
|
||||
assert.strictEqual(result.indexOf(source[1]), -1, 'an array CANNOT include an object');
|
||||
assert.strictEqual(result.indexOf(source[3]), -1, 'an array CANNOT include an array');
|
||||
assert.strictEqual(result.indexOf(source[4]), -1, 'an array CANNOT include falsy values');
|
||||
});
|
||||
|
||||
test('Dom.normalizeContent: functions returning arrays', function(assert) {
|
||||
assert.expect(3);
|
||||
|
||||
let arr = [];
|
||||
let result = Dom.normalizeContent(() => ['hello', Function.prototype, arr]);
|
||||
|
||||
assert.strictEqual(result[0].data, 'hello', 'a function can return an array');
|
||||
assert.strictEqual(result.indexOf(Function.prototype), -1, 'a function can return an array, but it CANNOT include a function');
|
||||
assert.strictEqual(result.indexOf(arr), -1, 'a function can return an array, but it CANNOT include an array');
|
||||
});
|
||||
|
||||
test('Dom.insertContent', function(assert) {
|
||||
let p = Dom.createEl('p');
|
||||
let text = document.createTextNode('hello');
|
||||
let el = Dom.insertContent(Dom.createEl(), [p, text]);
|
||||
|
||||
assert.expect(2);
|
||||
assert.strictEqual(el.firstChild, p, 'the paragraph was inserted first');
|
||||
assert.strictEqual(el.firstChild.nextSibling, text, 'the text node was inserted last');
|
||||
});
|
||||
|
||||
test('Dom.appendContent', function(assert) {
|
||||
let p1 = Dom.createEl('p');
|
||||
let p2 = Dom.createEl('p');
|
||||
let el = Dom.insertContent(Dom.createEl(), [p1]);
|
||||
|
||||
Dom.appendContent(el, p2);
|
||||
|
||||
assert.expect(2);
|
||||
assert.strictEqual(el.firstChild, p1, 'the first paragraph is the first child');
|
||||
assert.strictEqual(el.firstChild.nextSibling, p2, 'the second paragraph was appended');
|
||||
});
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user