/**
* @file modal-dialog.js
*/
import * as Dom from './utils/dom' ;
import Component from './component' ;
import window from 'global/window' ;
import document from 'global/document' ;
import keycode from 'keycode' ;
const MODAL_CLASS_NAME = 'vjs-modal-dialog' ;
/**
* Le `ModalDialog` s'affiche au-dessus de la vidéo et de ses contrôles, ce qui bloque l'accès à la vidéo
* l'interaction avec le lecteur jusqu'à ce qu'il soit fermé.
*
* Les boîtes de dialogue modales comportent un bouton "Fermer" et se ferment lorsque ce bouton est activé
* est activée - ou lorsque l'on appuie sur ESC n'importe où.
*
* @extends Component
*/
class ModalDialog extends Component {
/**
* Créer une instance de cette classe.
*
* @param {Player} player
* Le `Player` auquel cette classe doit être attachée.
*
* @param {Objet} [options]
* La mémoire clé/valeur des options du lecteur.
*
* @param {Mixed} [options.content=undefined]
* Fournir un contenu personnalisé pour ce modal.
*
* @param {string} [options.description]
* Description textuelle de la fenêtre modale, principalement pour des raisons d'accessibilité.
*
* @param {boolean} [options.fillAlways=false]
* Normalement, les modales ne sont remplies automatiquement que la première fois
* ils ouvrent. Cela indique à la fenêtre modale de rafraîchir son contenu
* chaque fois qu'il s'ouvre.
*
* @param {string} [options.label]
* Une étiquette de texte pour la fenêtre modale, principalement pour des raisons d'accessibilité.
*
* @param {boolean} [options.pauseOnOpen=true]
* Si `true`, la lecture sera interrompue si la lecture est interrompue lorsque
* la modale s'ouvre et reprend lorsqu'elle se ferme.
*
* @param {boolean} [options.temporary=true]
* Si `true`, la fenêtre modale ne peut être ouverte qu'une seule fois ; elle sera
* a disposé dès qu'il a été fermé.
*
* @param {boolean} [options.uncloseable=false]
* Si `true`, l'utilisateur ne pourra pas fermer la fenêtre modale
* par l'intermédiaire de l'interface utilisateur selon les modalités habituelles. La clôture programmatique est
* toujours possible.
*/
constructor(player, options) {
super(player, options) ;
this.handleKeyDown_ = (e) => this.handleKeyDown(e) ;
this.close_ = (e) => this.close(e) ;
this.opened_ = this.hasBeenOpened_ = this.hasBeenFilled_ = false ;
this.closeable(!this.options_.uncloseable) ;
this.content(this.options_.content) ;
// Assurez-vous que le contentEl est défini APRÈS l'initialisation des enfants
// parce que nous ne voulons que le contenu de la modale dans le contentEl
// (pas les éléments de l'interface utilisateur comme le bouton de fermeture).
this.contentEl_ = Dom.createEl('div', {
className : `${MODAL_CLASS_NAME}-content`
}, {
rôle : "document
}) ;
this.descEl_ = Dom.createEl('p', {
className : `${MODAL_CLASS_NAME}-description vjs-control-text`,
id : this.el().getAttribute('aria-describedby')
}) ;
Dom.textContent(this.descEl_, this.description()) ;
this.el_.appendChild(this.descEl_) ;
this.el_.appendChild(this.contentEl_) ;
}
/**
* Créer l'élément DOM du `ModalDialog`
*
* @return {Element}
* L'élément DOM qui est créé.
*/
createEl() {
return super.createEl('div', {
className : this.buildCSSClass(),
tabIndex : -1
}, {
'aria-describedby' : `${this.id()}_description`,
aria-hidden" : "true",
'aria-label' : this.label(),
'rôle' : 'dialogue'
}) ;
}
dispose() {
this.contentEl_ = null ;
this.descEl_ = null ;
this.previouslyActiveEl_ = null ;
super.dispose() ;
}
/**
* Construit le DOM par défaut `className`.
*
* @return {string}
* Le `nom de classe` du DOM pour cet objet.
*/
buildCSSClass() {
return `${MODAL_CLASS_NAME} vjs-hidden ${super.buildCSSClass()}` ;
}
/**
* Renvoie la chaîne de l'étiquette de cette fenêtre modale. Principalement utilisé pour l'accessibilité.
*
* @return {string}
* l'étiquette localisée ou brute de cette modale.
*/
label() {
return this.localize(this.options_.label || 'Fenêtre modale') ;
}
/**
* Renvoie la chaîne de description de cette fenêtre modale. Principalement utilisé pour
* l'accessibilité.
*
* @return {string}
* La description localisée ou brute de cette modalité.
*/
description() {
let desc = this.options_.description || this.localize('Ceci est une fenêtre modale.') ;
// Ajouter un message universel de fermeture si la modale est fermable.
if (this.closeable()) {
desc += ' ' + this.localize('Cette fenêtre modale peut être fermée en appuyant sur la touche Echap ou en activant le bouton de fermeture.') ;
}
retour desc ;
}
/**
* Ouvre la fenêtre modale.
*
* @fires ModalDialog#beforemodalopen
* @fires ModalDialog#modalopen
*/
open() {
if (!this.opened_) {
const player = this.player() ;
/**
* Déclenché juste avant l'ouverture d'un `ModalDialog`.
*
* @event ModalDialog#beforemodalopen
* @type {EventTarget~Event}
*/
this.trigger('beforemodalopen') ;
this.opened_ = true ;
// Remplir le contenu si la fenêtre modale n'a jamais été ouverte auparavant et
// n'a jamais été remplie.
if (this.options_.fillAlways || !this.hasBeenOpened_ && !this.hasBeenFilled_) {
this.fill() ;
}
// Si le lecteur est en cours de lecture, le mettre en pause et prendre note de son état antérieur
// état de jeu.
this.wasPlaying_ = !player.paused() ;
if (this.options_.pauseOnOpen && this.wasPlaying_) {
player.pause() ;
}
this.on('keydown', this.handleKeyDown_) ;
// Masquer les contrôles et noter s'ils ont été activés.
this.hadControls_ = player.controls() ;
player.controls(false) ;
this.show() ;
this.conditionalFocus_() ;
this.el().setAttribute('aria-hidden', 'false') ;
/**
* Déclenché juste après l'ouverture d'un `ModalDialog`.
*
* @event ModalDialog#modalopen
* @type {EventTarget~Event}
*/
this.trigger('modalopen') ;
this.hasBeenOpened_ = true ;
}
}
/**
* Si le `ModalDialog` est actuellement ouvert ou fermé.
*
* @param {boolean} [valeur]
* Si elle est donnée, elle ouvrira (`true`) ou fermera (`false`) la fenêtre modale.
*
* @return {boolean}
* l'état d'ouverture actuel de la boîte de dialogue modale
*/
opened(value) {
if (typeof value === 'boolean') {
this[value ? 'open' : 'close']() ;
}
return this.opened_ ;
}
/**
* Ferme la fenêtre modale, ne fait rien si le `ModalDialog` est
* n'est pas ouverte.
*
* @fires ModalDialog#beforemodalclose
* @fires ModalDialog#modalclose
*/
close() {
if (!this.opened_) {
retour ;
}
const player = this.player() ;
/**
* Déclenché juste avant la fermeture d'un `ModalDialog`.
*
* @event ModalDialog#beforemodalclose
* @type {EventTarget~Event}
*/
this.trigger('beforemodalclose') ;
this.opened_ = false ;
if (this.wasPlaying_ && this.options_.pauseOnOpen) {
player.play() ;
}
this.off('keydown', this.handleKeyDown_) ;
if (this.hadControls_) {
player.controls(true) ;
}
this.hide() ;
this.el().setAttribute('aria-hidden', 'true') ;
/**
* Déclenché juste après la fermeture d'un `ModalDialog`.
*
* @event ModalDialog#modalclose
* @type {EventTarget~Event}
*/
this.trigger('modalclose') ;
this.conditionalBlur_() ;
if (this.options_.temporary) {
this.dispose() ;
}
}
/**
* Vérifie si le `ModalDialog` peut être fermé par l'interface utilisateur.
*
* @param {boolean} [valeur]
* Si elle est donnée sous forme de booléen, l'option `closeable` sera activée.
*
* @return {boolean}
* Renvoie la valeur finale de l'option fermable.
*/
closeable(value) {
if (typeof value === 'boolean') {
const closeable = this.closeable_ = !!value ;
let close = this.getChild('closeButton') ;
// Si cette page est rendue refermable et qu'elle n'a pas de bouton de fermeture, ajoutez-en un.
if (closeable && !close) {
// Le bouton de fermeture doit être un enfant de la fenêtre modale - et non son
// l'élément de contenu, donc changer temporairement l'élément de contenu.
const temp = this.contentEl_ ;
this.contentEl_ = this.el_ ;
close = this.addChild('closeButton', {controlText : 'Close Modal Dialog'}) ;
this.contentEl_ = temp ;
this.on(close, 'close', this.close_) ;
}
// Si cet élément est rendu impossible à fermer et qu'il dispose d'un bouton de fermeture, supprimez-le.
if (!closeable && close) {
this.off(close, 'close', this.close_) ;
this.removeChild(close) ;
close.dispose() ;
}
}
return this.closeable_ ;
}
/**
* Remplir l'élément de contenu de la modale avec l'option "content" de la modale.
* L'élément de contenu sera vidé avant que ce changement n'ait lieu.
*/
fill() {
this.fillWith(this.content()) ;
}
/**
* Remplir l'élément de contenu de la modale avec un contenu arbitraire.
* L'élément de contenu sera vidé avant que ce changement n'ait lieu.
*
* @fires ModalDialog#beforemodalfill
* @fires ModalDialog#modalfill
*
* @param {Mixed} [content]
* Les mêmes règles s'appliquent à cette option qu'à l'option `content`.
*/
fillWith(content) {
const contentEl = this.contentEl() ;
const parentEl = contentEl.parentNode ;
const nextSiblingEl = contentEl.nextSibling ;
/**
* Déclenché juste avant qu'un `ModalDialog` ne soit rempli de contenu.
*
* @event ModalDialog#beforemodalfill
* @type {EventTarget~Event}
*/
this.trigger('beforemodalfill') ;
this.hasBeenFilled_ = true ;
// Détachement de l'élément de contenu du DOM avant d'effectuer
// manipulation pour éviter de modifier le DOM en direct plusieurs fois.
parentEl.removeChild(contentEl) ;
this.empty() ;
Dom.insertContent(contentEl, content) ;
/**
* Déclenché juste après qu'un `ModalDialog` ait été rempli avec du contenu.
*
* @event ModalDialog#modalfill
* @type {EventTarget~Event}
*/
this.trigger('modalfill') ;
// Réinjecter l'élément de contenu rempli à nouveau.
if (nextSiblingEl) {
parentEl.insertBefore(contentEl, nextSiblingEl) ;
} else {
parentEl.appendChild(contentEl) ;
}
// s'assurer que le bouton de fermeture est le dernier dans le DOM du dialogue
const closeButton = this.getChild('closeButton') ;
if (closeButton) {
parentEl.appendChild(closeButton.el_) ;
}
}
/**
* Vide l'élément de contenu. Cela se produit chaque fois que le modal est rempli.
*
* @fires ModalDialog#beforemodalempty
* @fires ModalDialog#modalempty
*/
empty() {
/**
* Déclenché juste avant qu'un `ModalDialog` ne soit vidé.
*
* @event ModalDialog#beforemodalempty
* @type {EventTarget~Event}
*/
this.trigger('beforemodalempty') ;
Dom.emptyEl(this.contentEl()) ;
/**
* Déclenché juste après qu'un `ModalDialog` ait été vidé.
*
* @event ModalDialog#modalempty
* @type {EventTarget~Event}
*/
this.trigger('modalempty') ;
}
/**
* Obtient ou définit le contenu de la fenêtre modale, qui est normalisé avant d'être affiché
* rendu dans le DOM.
*
* Cette opération ne met pas à jour le DOM et ne remplit pas la fenêtre modale, mais elle est appelée au cours de la phase d'exécution du projet
* ce processus.
*
* @param {Mixed} [valeur]
* S'il est défini, définit la valeur de contenu interne à utiliser sur le formulaire
* le(s) prochain(s) appel(s) à `fill`. Cette valeur est normalisée avant d'être
* inséré. Pour "effacer" la valeur interne du contenu, passez `null`.
*
* @return {Mixed}
* Le contenu actuel du dialogue modal
*/
content(value) {
if (typeof value !== 'undefined') {
this.content_ = valeur ;
}
return this.content_ ;
}
/**
* activer conditionnellement la boîte de dialogue modale si l'attention était précédemment portée sur le lecteur.
*
* @private
*/
conditionalFocus_() {
const activeEl = document.activeElement ;
const playerEl = this.player_.el_ ;
this.previouslyActiveEl_ = null ;
if (playerEl.contains(activeEl) || playerEl === activeEl) {
this.previouslyActiveEl_ = activeEl ;
this.focus() ;
}
}
/**
* flouter conditionnellement l'élément et recentrer le dernier élément mis au point
*
* @private
*/
conditionalBlur_() {
if (this.previouslyActiveEl_) {
this.previouslyActiveEl_.focus() ;
this.previouslyActiveEl_ = null ;
}
}
/**
* Gestionnaire de la descente de clé. Attaché lorsque le modal est focalisé.
*
* @listens keydown
*/
handleKeyDown(event) {
// Ne pas permettre aux touches de sortir de la boîte de dialogue modale.
event.stopPropagation() ;
if (keycode.isEventKey(event, 'Escape') && this.closeable()) {
event.preventDefault() ;
this.close() ;
retour ;
}
// quitter prématurément s'il ne s'agit pas d'une touche de tabulation
if (!keycode.isEventKey(event, 'Tab')) {
retour ;
}
const focusableEls = this.focusableEls_() ;
const activeEl = this.el_.querySelector(':focus') ;
let focusIndex ;
for (let i = 0 ; i < focusableEls.length ; i++) {
if (activeEl === focusableEls[i]) {
focusIndex = i ;
pause ;
}
}
if (document.activeElement === this.el_) {
focusIndex = 0 ;
}
if (event.shiftKey && focusIndex === 0) {
focusableEls[focusableEls.length - 1].focus() ;
event.preventDefault() ;
} else if (!event.shiftKey && focusIndex === focusableEls.length - 1) {
focusableEls[0].focus() ;
event.preventDefault() ;
}
}
/**
* obtenir tous les éléments focalisables
*
* @private
*/
focusableEls_() {
const allChildren = this.el_.querySelectorAll('*') ;
return Array.prototype.filter.call(allChildren, (child) => {
return ((child instanceof window.HTMLAnchorElement ||
child instanceof window.HTMLAreaElement) && child.hasAttribute('href')) ||
((child instanceof window.HTMLInputElement ||
child instanceof window.HTMLSelectElement ||
child instanceof window.HTMLTextAreaElement ||
child instanceof window.HTMLButtonElement) && !child.hasAttribute('disabled')) ||
(child instanceof window.HTMLIFrameElement ||
child instanceof window.HTMLObjectElement ||
child instanceof window.HTMLEmbedElement) ||
(child.hasAttribute('tabindex') && child.getAttribute('tabindex') !== -1) ||
(child.hasAttribute('contenteditable')) ;
}) ;
}
}
/**
* Options par défaut pour les options par défaut de `ModalDialog`.
*
* @type {Objet}
* @private
*/
ModalDialog.prototype.options_ = {
pauseOnOpen : true,
temporaire : vrai
};
Component.registerComponent('ModalDialog', ModalDialog) ;
export default ModalDialog ;