1/* eslint-disable no-param-reassign */ 2H5P.ConfirmationDialog = (function (EventDispatcher) { 3 /** 4 * Create a confirmation dialog 5 * 6 * @param [options] Options for confirmation dialog 7 * @param [options.instance] Instance that uses confirmation dialog 8 * @param [options.headerText] Header text 9 * @param [options.dialogText] Dialog text 10 * @param [options.cancelText] Cancel dialog button text 11 * @param [options.confirmText] Confirm dialog button text 12 * @param [options.hideCancel] Hide cancel button 13 * @param [options.hideExit] Hide exit button 14 * @param [options.skipRestoreFocus] Skip restoring focus when hiding the dialog 15 * @param [options.classes] Extra classes for popup 16 * @param [options.theme] Whether to use the new theme (true) or the old design (false) 17 * @constructor 18 */ 19 function ConfirmationDialog(options) { 20 EventDispatcher.call(this); 21 const self = this; 22 23 // Make sure confirmation dialogs have unique id 24 H5P.ConfirmationDialog.uniqueId += 1; 25 const { uniqueId } = H5P.ConfirmationDialog; 26 27 // Default options 28 options = options || {}; 29 options.headerText = options.headerText || H5P.t('confirmDialogHeader'); 30 options.dialogText = options.dialogText || H5P.t('confirmDialogBody'); 31 options.cancelText = options.cancelText || H5P.t('cancelLabel'); 32 options.closeText = options.closeText || H5P.t('close'); 33 options.confirmText = options.confirmText || H5P.t('confirmLabel'); 34 35 /** 36 * Handle confirming event 37 * @param {Event} e 38 */ 39 function dialogConfirmed(e) { 40 self.hide(); 41 self.trigger('confirmed'); 42 e.preventDefault(); 43 } 44 45 /** 46 * Handle dialog canceled 47 * @param {Event} e Event. 48 * @param {object} [options] Options. 49 * @param {boolean} [options.wasExplicitChoice] 50 * True if user chose cancel explicitly, otherwise (close, esc) false. 51 */ 52 function dialogCanceled(e, options = {}) { 53 self.hide(); 54 self.trigger('canceled', { wasExplicitChoice: options.wasExplicitChoice ?? false }); 55 e.preventDefault(); 56 } 57 58 /** 59 * Handle tabbing through buttons with focus trapping. 60 * @param {KeyboardEvent} event Keyboard event. 61 */ 62 function handleTabbing(event) { 63 if (event.key !== 'Tab') { 64 return; 65 } 66 67 event.preventDefault(); 68 69 const currentIndex = focusableButtons.indexOf(event.target); 70 const offset = event.shiftKey ? -1 : 1; 71 const nextIndex = (currentIndex + offset + focusableButtons.length) % focusableButtons.length; 72 73 focusableButtons[nextIndex].focus(); 74 } 75 76 // Offset of exit button 77 const exitButtonOffset = 2 * 16; 78 const shadowOffset = 8; 79 80 // Determine if we are too large for our container and must resize 81 let resizeIFrame = false; 82 83 // Create background 84 const popupBackground = document.createElement('div'); 85 popupBackground.classList 86 .add('h5p-confirmation-dialog-background', 'hidden', 'hiding'); 87 88 if (options.theme) { 89 popupBackground.classList.add('h5p-theme'); 90 } 91 92 if (window.H5PEditor) { 93 popupBackground.classList.add('h5peditor'); 94 95 if (H5PIntegration.theme?.density) { 96 popupBackground.classList.add(`h5p-${H5PIntegration.theme.density}`); 97 } 98 } 99 100 // Create outer popup 101 const popup = document.createElement('div'); 102 popup.classList.add('h5p-confirmation-dialog-popup', 'hidden'); 103 if (options.classes) { 104 options.classes.forEach((popupClass) => { 105 popup.classList.add(popupClass); 106 }); 107 } 108 109 popup.setAttribute('role', 'alertdialog'); 110 popup.setAttribute('aria-modal', 'true'); 111 popup.setAttribute('aria-labelledby', `h5p-confirmation-dialog-header-text-${uniqueId}`); 112 popup.setAttribute('aria-describedby', `h5p-confirmation-dialog-text-${uniqueId}`); 113 popupBackground.appendChild(popup); 114 popup.addEventListener('keydown', (e) => { 115 if (e.key === 'Escape') { // Esc key 116 // Exit dialog 117 dialogCanceled(e); 118 } 119 }); 120 popup.addEventListener('keydown', (event) => { 121 handleTabbing(event); 122 }); 123 124 // Popup header 125 const header = document.createElement('div'); 126 header.classList.add('h5p-confirmation-dialog-header'); 127 popup.appendChild(header); 128 129 // Header text 130 const headerText = document.createElement('div'); 131 headerText.classList.add('h5p-confirmation-dialog-header-text'); 132 headerText.id = `h5p-confirmation-dialog-dialog-header-text-${uniqueId}`; 133 headerText.innerHTML = options.headerText; 134 header.appendChild(headerText); 135 136 // Popup body 137 const body = document.createElement('div'); 138 body.classList.add('h5p-confirmation-dialog-body'); 139 popup.appendChild(body); 140 141 // Popup text 142 const text = document.createElement('div'); 143 text.classList.add('h5p-confirmation-dialog-text'); 144 text.innerHTML = options.dialogText; 145 text.id = `h5p-confirmation-dialog-dialog-text-${uniqueId}`; 146 body.appendChild(text); 147 148 // Popup buttons 149 const buttons = document.createElement('div'); 150 buttons.classList.add('h5p-confirmation-dialog-buttons'); 151 body.appendChild(buttons); 152 153 // Cancel button 154 if (!options.hideCancel) { 155 const cancelButton = document.createElement('button'); 156 if (!options.theme) { 157 cancelButton.classList.add('h5p-core-cancel-button'); 158 } 159 else { 160 cancelButton.classList.add('h5p-theme-button', 'h5p-theme-secondary-cta'); 161 cancelButton.classList.add('h5p-theme-cancel'); 162 } 163 const cancelText = document.createElement('span'); 164 cancelText.textContent = options.cancelText; 165 cancelButton.appendChild(cancelText); 166 cancelButton.addEventListener('click', (event) => { 167 dialogCanceled(event, { wasExplicitChoice: true }); 168 }); 169 buttons.appendChild(cancelButton); 170 } 171 else { 172 // Center remaining buttons 173 buttons.classList.add('center'); 174 } 175 176 // Confirm button 177 const confirmButton = document.createElement('button'); 178 if (!options.theme) { 179 confirmButton.classList.add('h5p-core-button'); 180 } 181 // confirmButton.classList.add('h5p-confirmation-dialog-confirm-button'); 182 confirmButton.setAttribute('aria-label', options.confirmText); 183 184 if (options.theme) { 185 confirmButton.classList.add('h5p-theme-button', 'h5p-theme-primary-cta'); 186 confirmButton.classList.add('h5p-theme-check'); 187 } 188 189 confirmButton.addEventListener('click', dialogConfirmed); 190 const confirmText = document.createElement('span'); 191 confirmText.textContent = options.confirmText; 192 confirmButton.appendChild(confirmText); 193 buttons.appendChild(confirmButton); 194 195 let focusableButtons = [...buttons.childNodes]; 196 197 // Exit button 198 if (!options.hideExit) { 199 const exitButton = document.createElement('button'); 200 exitButton.classList.add('h5p-confirmation-dialog-exit'); 201 exitButton.setAttribute('aria-label', options.closeText); 202 exitButton.addEventListener('click', dialogCanceled); 203 if (options.theme) { 204 header.appendChild(exitButton); 205 } 206 else { 207 popup.appendChild(exitButton); 208 } 209 focusableButtons.push(exitButton); 210 } 211 212 // Wrapper element 213 let wrapperElement; 214 215 // Maintains hidden state of elements 216 let wrapperSiblingsHidden = []; 217 let popupSiblingsHidden = []; 218 219 // Element with focus before dialog 220 let previouslyFocused; 221 222 /** 223 * Set parent of confirmation dialog 224 * @param {HTMLElement} wrapper 225 * @returns {H5P.ConfirmationDialog} 226 */ 227 this.appendTo = function (wrapper) { 228 wrapperElement = wrapper; 229 return this; 230 }; 231 232 /** 233 * Hide siblings of element from assistive technology 234 * 235 * @param {HTMLElement} element 236 * @returns {Array} The previous hidden state of all siblings 237 */ 238 const hideSiblings = function (element) { 239 const hiddenSiblings = []; 240 const siblings = element.parentNode.children; 241 let i; 242 for (i = 0; i < siblings.length; i += 1) { 243 // Preserve hidden state 244 hiddenSiblings[i] = !!siblings[i].getAttribute('aria-hidden'); 245 246 if (siblings[i] !== element) { 247 if (siblings[i].getAttribute('aria-live')) { 248 siblings[i].setAttribute('aria-busy', true); 249 } 250 else { 251 siblings[i].setAttribute('aria-hidden', true); 252 } 253 } 254 } 255 return hiddenSiblings; 256 }; 257 258 /** 259 * Restores assistive technology state of element's siblings 260 * 261 * @param {HTMLElement} element 262 * @param {Array} hiddenSiblings Hidden state of all siblings 263 */ 264 const restoreSiblings = function (element, hiddenSiblings) { 265 const siblings = element.parentNode.children; 266 let i; 267 for (i = 0; i < siblings.length; i += 1) { 268 if (siblings[i] !== element && !hiddenSiblings[i]) { 269 if (siblings[i].getAttribute('aria-live')) { 270 siblings[i].setAttribute('aria-busy', false); 271 } 272 else { 273 siblings[i].removeAttribute('aria-hidden'); 274 } 275 } 276 } 277 }; 278 279 /** 280 * Hide siblings in underlay from assistive technologies 281 */ 282 const disableUnderlay = function () { 283 wrapperSiblingsHidden = hideSiblings(wrapperElement);
284 popupSiblingsHidden = hideSiblings(popupBackground); 285 }; 286 287 /** 288 * Restore state of underlay for assistive technologies 289 */ 290 const restoreUnderlay = function () { 291 restoreSiblings(wrapperElement, wrapperSiblingsHidden); 292 restoreSiblings(popupBackground, popupSiblingsHidden); 293 }; 294 295 /** 296 * Fit popup to container. Makes sure it doesn't overflow. 297 * @params {number} [offsetTop] Offset of popup 298 */ 299 const fitToContainer = function (offsetTop) { 300 let popupOffsetTop = parseInt(popup.style.top, 10); 301 if (offsetTop !== undefined) { 302 popupOffsetTop = offsetTop; 303 } 304 305 if (!popupOffsetTop) { 306 popupOffsetTop = 0; 307 } 308 309 // Overflows height 310 if (popupOffsetTop + popup.offsetHeight > wrapperElement.offsetHeight) { 311 popupOffsetTop = wrapperElement.offsetHeight - popup.offsetHeight - shadowOffset; 312 } 313 314 if (popupOffsetTop - exitButtonOffset <= 0) { 315 popupOffsetTop = exitButtonOffset + shadowOffset; 316 317 // We are too big and must resize 318 resizeIFrame = true; 319 } 320 popup.style.top = `${popupOffsetTop}px`; 321 }; 322 323 /** 324 * Show confirmation dialog 325 * @params {number} offsetTop Offset top 326 * @returns {H5P.ConfirmationDialog} 327 */ 328 this.show = function (offsetTop) { 329 // Capture focused item 330 previouslyFocused = document.activeElement; 331 wrapperElement.appendChild(popupBackground); 332 popupBackground.classList.remove('hidden'); 333 fitToContainer(offsetTop); 334 popup.classList.remove('hidden'); 335 popupBackground.addEventListener('transitionend', () => { 336 buttons.firstChild.focus(); 337 }, { once: true }); 338 popupBackground.classList.remove('hiding'); 339 disableUnderlay(); 340 341 // Resize iFrame if necessary 342 if (resizeIFrame && options.instance) { 343 const minHeight = parseInt(popup.offsetHeight, 10) 344 + exitButtonOffset + (2 * shadowOffset); 345 self.setViewPortMinimumHeight(minHeight); 346 options.instance.trigger('resize'); 347 resizeIFrame = false; 348 } 349 350 // Detect if the user prefers reduced motion, because in that case 351 // we cannot rely on transitionend triggering and we need to manually 352 // focus the buttons. It should also be checked for each show, since a 353 // user may change this setting at any time. 354 const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); 355 if (prefersReducedMotion.matches) { 356 buttons.firstChild.focus(); 357 } 358 359 return this; 360 }; 361 362 /** 363 * Hide confirmation dialog 364 * @returns {H5P.ConfirmationDialog} 365 */ 366 this.hide = function () { 367 restoreUnderlay(); 368 popupBackground.classList.add('hiding'); 369 popup.classList.add('hidden'); 370 371 // Restore focus 372 if (!options.skipRestoreFocus) { 373 previouslyFocused.focus(); 374 } 375 popupBackground.classList.add('hidden'); 376 wrapperElement.removeChild(popupBackground); 377 self.setViewPortMinimumHeight(null); 378 379 return this; 380 }; 381 382 /** 383 * Retrieve element 384 * 385 * @return {HTMLElement} 386 */ 387 this.getElement = function () { 388 return popup; 389 }; 390 391 /** 392 * Get previously focused element 393 * @return {HTMLElement} 394 */ 395 this.getPreviouslyFocused = function () { 396 return previouslyFocused; 397 }; 398 399 /** 400 * Sets the minimum height of the view port 401 * 402 * @param {number|null} minHeight 403 */ 404 this.setViewPortMinimumHeight = function (minHeight) { 405 const container = document.querySelector('.h5p-container') || document.body; 406 container.style.minHeight = (typeof minHeight === 'number') ? (`${minHeight}px`) : minHeight; 407 }; 408 } 409 410 ConfirmationDialog.prototype = Object.create(EventDispatcher.prototype); 411 ConfirmationDialog.prototype.constructor = ConfirmationDialog; 412 413 return ConfirmationDialog; 414}(H5P.EventDispatcher)); 415 416H5P.ConfirmationDialog.uniqueId = -1;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.