2 * Modal dialog handler.
4 * @author Alexander Ebert
5 * @copyright 2001-2019 WoltLab GmbH
6 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
7 * @module Ui/Dialog (alias)
8 * @module WoltLabSuite/Core/Ui/Dialog
12 'Ajax', 'Core', 'Dictionary',
13 'Environment', 'Language', 'ObjectMap', 'Dom/ChangeListener',
14 'Dom/Traverse', 'Dom/Util', 'Ui/Confirmation', 'Ui/Screen', 'Ui/SimpleDropdown',
15 'EventHandler', 'List', 'EventKey'
18 Ajax
, Core
, Dictionary
,
19 Environment
, Language
, ObjectMap
, DomChangeListener
,
20 DomTraverse
, DomUtil
, UiConfirmation
, UiScreen
, UiSimpleDropdown
,
21 EventHandler
, List
, EventKey
26 var _activeDialog
= null;
27 var _callbackFocus
= null;
28 var _container
= null;
29 var _dialogs
= new Dictionary();
30 var _dialogFullHeight
= false;
31 var _dialogObjects
= new ObjectMap();
32 var _dialogToObject
= new Dictionary();
33 var _focusedBeforeDialog
= null;
34 var _keyupListener
= null;
35 var _staticDialogs
= elByClass('jsStaticDialog');
36 var _validCallbacks
= ['onBeforeClose', 'onClose', 'onShow'];
38 // list of supported `input[type]` values for dialog submit
39 var _validInputTypes
= ['number', 'password', 'search', 'tel', 'text', 'url'];
41 var _focusableElements
= [
42 'a[href]:not([tabindex^="-"]):not([inert])',
43 'area[href]:not([tabindex^="-"]):not([inert])',
44 'input:not([disabled]):not([inert])',
45 'select:not([disabled]):not([inert])',
46 'textarea:not([disabled]):not([inert])',
47 'button:not([disabled]):not([inert])',
48 'iframe:not([tabindex^="-"]):not([inert])',
49 'audio:not([tabindex^="-"]):not([inert])',
50 'video:not([tabindex^="-"]):not([inert])',
51 '[contenteditable]:not([tabindex^="-"]):not([inert])',
52 '[tabindex]:not([tabindex^="-"]):not([inert])'
56 * @exports WoltLabSuite/Core/Ui/Dialog
60 * Sets up global container and internal variables.
63 // Fetch Ajax, as it cannot be provided because of a circular dependency
64 if (Ajax
=== undefined) Ajax
= require('Ajax');
66 _container
= elCreate('div');
67 _container
.classList
.add('dialogOverlay');
68 elAttr(_container
, 'aria-hidden', 'true');
69 _container
.addEventListener('mousedown', this._closeOnBackdrop
.bind(this));
70 _container
.addEventListener('wheel', function (event
) {
71 if (event
.target
=== _container
) {
72 event
.preventDefault();
74 }, { passive
: false });
76 elById('content').appendChild(_container
);
78 _keyupListener
= (function(event
) {
79 if (event
.keyCode
=== 27) {
80 if (event
.target
.nodeName
!== 'INPUT' && event
.target
.nodeName
!== 'TEXTAREA') {
81 this.close(_activeDialog
);
90 UiScreen
.on('screen-xs', {
91 match: function() { _dialogFullHeight
= true; },
92 unmatch: function() { _dialogFullHeight
= false; },
93 setup: function() { _dialogFullHeight
= true; }
96 this._initStaticDialogs();
97 DomChangeListener
.add('Ui/Dialog', this._initStaticDialogs
.bind(this));
99 UiScreen
.setDialogContainer(_container
);
101 window
.addEventListener('resize', (function () {
102 _dialogs
.forEach((function (dialog
) {
103 if (!elAttrBool(dialog
.dialog
, 'aria-hidden')) {
104 this.rebuild(elData(dialog
.dialog
, 'id'));
110 _initStaticDialogs: function() {
111 var button
, container
, id
;
112 while (_staticDialogs
.length
) {
113 button
= _staticDialogs
[0];
114 button
.classList
.remove('jsStaticDialog');
116 id
= elData(button
, 'dialog-id');
117 if (id
&& (container
= elById(id
))) {
118 ((function(button
, container
) {
119 container
.classList
.remove('jsStaticDialogContent');
120 elData(container
, 'is-static-dialog', true);
122 button
.addEventListener(WCF_CLICK_EVENT
, (function(event
) {
123 event
.preventDefault();
125 this.openStatic(container
.id
, null, { title
: elData(container
, 'title') });
127 }).bind(this))(button
, container
);
133 * Opens the dialog and implicitly creates it on first usage.
135 * @param {object} callbackObject used to invoke `_dialogSetup()` on first call
136 * @param {(string|DocumentFragment=} html html content or document fragment to use for dialog content
137 * @returns {object<string, *>} dialog data
139 open: function(callbackObject
, html
) {
140 var dialogData
= _dialogObjects
.get(callbackObject
);
141 if (Core
.isPlainObject(dialogData
)) {
142 // dialog already exists
143 return this.openStatic(dialogData
.id
, html
);
146 // initialize a new dialog
147 if (typeof callbackObject
._dialogSetup
!== 'function') {
148 throw new Error("Callback object does not implement the method '_dialogSetup()'.");
151 var setupData
= callbackObject
._dialogSetup();
152 if (!Core
.isPlainObject(setupData
)) {
153 throw new Error("Expected an object literal as return value of '_dialogSetup()'.");
156 dialogData
= { id
: setupData
.id
};
158 var createOnly
= true;
159 if (setupData
.source
=== undefined) {
160 var dialogElement
= elById(setupData
.id
);
161 if (dialogElement
=== null) {
162 throw new Error("Element id '" + setupData
.id
+ "' is invalid and no source attribute was given. If you want to use the `html` argument instead, please add `source: null` to your dialog configuration.");
165 setupData
.source
= document
.createDocumentFragment();
166 setupData
.source
.appendChild(dialogElement
);
168 // remove id and `display: none` from dialog element
169 dialogElement
.removeAttribute('id');
170 elShow(dialogElement
);
172 else if (setupData
.source
=== null) {
173 // `null` means there is no static markup and `html` should be used instead
174 setupData
.source
= html
;
177 else if (typeof setupData
.source
=== 'function') {
180 else if (Core
.isPlainObject(setupData
.source
)) {
181 if (typeof html
=== 'string' && html
.trim() !== '') {
182 setupData
.source
= html
;
185 Ajax
.api(this, setupData
.source
.data
, (function (data
) {
186 if (data
.returnValues
&& typeof data
.returnValues
.template
=== 'string') {
187 this.open(callbackObject
, data
.returnValues
.template
);
189 if (typeof setupData
.source
.after
=== 'function') {
190 setupData
.source
.after(_dialogs
.get(setupData
.id
).content
, data
);
195 // deferred initialization
200 if (typeof setupData
.source
=== 'string') {
201 var dialogElement
= elCreate('div');
202 elAttr(dialogElement
, 'id', setupData
.id
);
203 DomUtil
.setInnerHtml(dialogElement
, setupData
.source
);
205 setupData
.source
= document
.createDocumentFragment();
206 setupData
.source
.appendChild(dialogElement
);
209 if (!setupData
.source
.nodeType
|| setupData
.source
.nodeType
!== Node
.DOCUMENT_FRAGMENT_NODE
) {
210 throw new Error("Expected at least a document fragment as 'source' attribute.");
216 _dialogObjects
.set(callbackObject
, dialogData
);
217 _dialogToObject
.set(setupData
.id
, callbackObject
);
219 return this.openStatic(setupData
.id
, setupData
.source
, setupData
.options
, createOnly
);
223 * Opens an dialog, if the dialog is already open the content container
224 * will be replaced by the HTML string contained in the parameter html.
226 * If id is an existing element id, html will be ignored and the referenced
227 * element will be appended to the content element instead.
229 * @param {string} id element id, if exists the html parameter is ignored in favor of the existing element
230 * @param {?(string|DocumentFragment)} html content html
231 * @param {object<string, *>} options list of options, is completely ignored if the dialog already exists
232 * @param {boolean=} createOnly create the dialog but do not open it
233 * @return {object<string, *>} dialog data
235 openStatic: function(id
, html
, options
, createOnly
) {
236 UiScreen
.pageOverlayOpen();
238 if (Environment
.platform() !== 'desktop') {
239 if (!this.isOpen(id
)) {
240 UiScreen
.scrollDisable();
244 if (_dialogs
.has(id
)) {
245 this._updateDialog(id
, html
);
248 options
= Core
.extend({
249 backdropCloseOnClick
: true,
251 closeButtonLabel
: Language
.get('wcf.global.button.close'),
252 closeConfirmMessage
: '',
253 disableContentPadding
: false,
262 if (!options
.closable
) options
.backdropCloseOnClick
= false;
263 if (options
.closeConfirmMessage
) {
264 options
.onBeforeClose
= (function(id
) {
265 UiConfirmation
.show({
266 confirm
: this.close
.bind(this, id
),
267 message
: options
.closeConfirmMessage
272 this._createDialog(id
, html
, options
);
275 var data
= _dialogs
.get(id
);
277 // iOS breaks `position: fixed` when input elements or `contenteditable`
278 // are focused, this will freeze the screen and force Safari to scroll
279 // to the input field
280 if (Environment
.platform() === 'ios') {
281 window
.setTimeout((function () {
282 var input
= elBySel('input, textarea', data
.content
);
283 if (input
!== null) {
293 * Sets the dialog title.
295 * @param {(string|object)} id element id
296 * @param {string} title dialog title
298 setTitle: function(id
, title
) {
299 id
= this._getDialogId(id
);
301 var data
= _dialogs
.get(id
);
302 if (data
=== undefined) {
303 throw new Error("Expected a valid dialog id, '" + id
+ "' does not match any active dialog.");
306 var dialogTitle
= elByClass('dialogTitle', data
.dialog
);
307 if (dialogTitle
.length
) {
308 dialogTitle
[0].textContent
= title
;
313 * Sets a callback function on runtime.
315 * @param {(string|object)} id element id
316 * @param {string} key callback identifier
317 * @param {?function} value callback function or `null`
319 setCallback: function(id
, key
, value
) {
320 if (typeof id
=== 'object') {
321 var dialogData
= _dialogObjects
.get(id
);
322 if (dialogData
!== undefined) {
327 var data
= _dialogs
.get(id
);
328 if (data
=== undefined) {
329 throw new Error("Expected a valid dialog id, '" + id
+ "' does not match any active dialog.");
332 if (_validCallbacks
.indexOf(key
) === -1) {
333 throw new Error("Invalid callback identifier, '" + key
+ "' is not recognized.");
336 if (typeof value
!== 'function' && value
!== null) {
337 throw new Error("Only functions or the 'null' value are acceptable callback values ('" + typeof value
+ "' given).");
344 * Creates the DOM for a new dialog and opens it.
346 * @param {string} id element id, if exists the html parameter is ignored in favor of the existing element
347 * @param {?(string|DocumentFragment)} html content html
348 * @param {object<string, *>} options list of options
349 * @param {boolean=} createOnly create the dialog but do not open it
351 _createDialog: function(id
, html
, options
, createOnly
) {
354 element
= elById(id
);
355 if (element
=== null) {
356 throw new Error("Expected either a HTML string or an existing element id.");
360 var dialog
= elCreate('div');
361 dialog
.classList
.add('dialogContainer');
362 elAttr(dialog
, 'aria-hidden', 'true');
363 elAttr(dialog
, 'role', 'dialog');
364 elData(dialog
, 'id', id
);
366 var header
= elCreate('header');
367 dialog
.appendChild(header
);
369 var titleId
= DomUtil
.getUniqueId();
370 elAttr(dialog
, 'aria-labelledby', titleId
);
372 var title
= elCreate('span');
373 title
.classList
.add('dialogTitle');
374 title
.textContent
= options
.title
;
375 elAttr(title
, 'id', titleId
);
376 header
.appendChild(title
);
378 if (options
.closable
) {
379 var closeButton
= elCreate('a');
380 closeButton
.className
= 'dialogCloseButton jsTooltip';
381 closeButton
.href
= '#';
382 elAttr(closeButton
, 'role', 'button');
383 elAttr(closeButton
, 'tabindex', '0');
384 elAttr(closeButton
, 'title', options
.closeButtonLabel
);
385 elAttr(closeButton
, 'aria-label', options
.closeButtonLabel
);
386 closeButton
.addEventListener(WCF_CLICK_EVENT
, this._close
.bind(this));
387 header
.appendChild(closeButton
);
389 var span
= elCreate('span');
390 span
.className
= 'icon icon24 fa-times';
391 closeButton
.appendChild(span
);
394 var contentContainer
= elCreate('div');
395 contentContainer
.classList
.add('dialogContent');
396 if (options
.disableContentPadding
) contentContainer
.classList
.add('dialogContentNoPadding');
397 dialog
.appendChild(contentContainer
);
399 contentContainer
.addEventListener('wheel', function (event
) {
400 var allowScroll
= false;
401 var element
= event
.target
, clientHeight
, scrollHeight
, scrollTop
;
403 clientHeight
= element
.clientHeight
;
404 scrollHeight
= element
.scrollHeight
;
406 if (clientHeight
< scrollHeight
) {
407 scrollTop
= element
.scrollTop
;
409 // negative value: scrolling up
410 if (event
.deltaY
< 0 && scrollTop
> 0) {
414 else if (event
.deltaY
> 0 && (scrollTop
+ clientHeight
< scrollHeight
)) {
420 if (!element
|| element
=== contentContainer
) {
424 element
= element
.parentNode
;
427 if (allowScroll
=== false) {
428 event
.preventDefault();
430 }, { passive
: false });
433 if (element
=== null) {
434 if (typeof html
=== 'string') {
435 content
= elCreate('div');
437 DomUtil
.setInnerHtml(content
, html
);
439 else if (html
instanceof DocumentFragment
) {
440 var children
= [], node
;
441 for (var i
= 0, length
= html
.childNodes
.length
; i
< length
; i
++) {
442 node
= html
.childNodes
[i
];
444 if (node
.nodeType
=== Node
.ELEMENT_NODE
) {
449 if (children
[0].nodeName
!== 'DIV' || children
.length
> 1) {
450 content
= elCreate('div');
452 content
.appendChild(html
);
455 content
= children
[0];
459 throw new TypeError("'html' must either be a string or a DocumentFragment");
466 contentContainer
.appendChild(content
);
468 if (content
.style
.getPropertyValue('display') === 'none') {
473 backdropCloseOnClick
: options
.backdropCloseOnClick
,
474 closable
: options
.closable
,
478 onBeforeClose
: options
.onBeforeClose
,
479 onClose
: options
.onClose
,
480 onShow
: options
.onShow
,
483 inputFields
: new List()
486 DomUtil
.prepend(dialog
, _container
);
488 if (typeof options
.onSetup
=== 'function') {
489 options
.onSetup(content
);
492 if (createOnly
!== true) {
493 this._updateDialog(id
, null);
498 * Updates the dialog's content element.
500 * @param {string} id element id
501 * @param {?string} html content html, prevent changes by passing null
503 _updateDialog: function(id
, html
) {
504 var data
= _dialogs
.get(id
);
505 if (data
=== undefined) {
506 throw new Error("Expected a valid dialog id, '" + id
+ "' does not match any active dialog.");
509 if (typeof html
=== 'string') {
510 DomUtil
.setInnerHtml(data
.content
, html
);
513 if (elAttr(data
.dialog
, 'aria-hidden') === 'true') {
514 // close existing dropdowns
515 UiSimpleDropdown
.closeAll();
516 window
.WCF
.Dropdown
.Interactive
.Handler
.closeAll();
518 if (_callbackFocus
=== null) {
519 _callbackFocus
= this._maintainFocus
.bind(this);
520 document
.body
.addEventListener('focus', _callbackFocus
, { capture
: true });
523 if (data
.closable
&& elAttr(_container
, 'aria-hidden') === 'true') {
524 window
.addEventListener('keyup', _keyupListener
);
527 // Move the dialog to the front to prevent it being hidden behind already open dialogs
528 // if it was previously visible.
529 data
.dialog
.parentNode
.insertBefore(data
.dialog
, data
.dialog
.parentNode
.firstChild
);
531 elAttr(data
.dialog
, 'aria-hidden', 'false');
532 elAttr(_container
, 'aria-hidden', 'false');
533 elData(_container
, 'close-on-click', (data
.backdropCloseOnClick
? 'true' : 'false'));
536 // Keep a reference to the currently focused element to be able to restore it later.
537 _focusedBeforeDialog
= document
.activeElement
;
539 // Set the focus to the first focusable child of the dialog element.
540 var closeButton
= elBySel('.dialogCloseButton', data
.header
);
541 if (closeButton
) elAttr(closeButton
, 'inert', true);
542 this._setFocusToFirstItem(data
.dialog
);
543 if (closeButton
) closeButton
.removeAttribute('inert');
545 if (typeof data
.onShow
=== 'function') {
546 data
.onShow(data
.content
);
549 if (elDataBool(data
.content
, 'is-static-dialog')) {
550 EventHandler
.fire('com.woltlab.wcf.dialog', 'openStatic', {
551 content
: data
.content
,
559 DomChangeListener
.trigger();
563 * @param {Event} event
565 _maintainFocus: function(event
) {
567 var data
= _dialogs
.get(_activeDialog
);
568 if (!data
.dialog
.contains(event
.target
) && !event
.target
.closest('.dropdownMenuContainer') && !event
.target
.closest('.datePicker')) {
569 this._setFocusToFirstItem(data
.dialog
, true);
575 * @param {Element} dialog
576 * @param {boolean} maintain
578 _setFocusToFirstItem: function(dialog
, maintain
) {
579 var focusElement
= this._getFirstFocusableChild(dialog
);
580 if (focusElement
!== null) {
582 if (focusElement
.id
=== 'username' || focusElement
.name
=== 'username') {
583 if (Environment
.browser() === 'safari' && Environment
.platform() === 'ios') {
584 // iOS Safari's username/password autofill breaks if the input field is focused
591 // Setting the focus to a select element in iOS is pretty strange, because
592 // it focuses it, but also displays the keyboard for a fraction of a second,
593 // causing it to pop out from below and immediately vanish.
595 // iOS will only show the keyboard if an input element is focused *and* the
596 // focus is an immediate result of a user interaction. This method must be
597 // assumed to be called from within a click event, but we want to set the
598 // focus without triggering the keyboard.
600 // We can break the condition by wrapping it in a setTimeout() call,
601 // effectively tricking iOS into focusing the element without showing the
603 setTimeout(function() {
604 focusElement
.focus();
611 * @param {Element} node
612 * @returns {?Element}
614 _getFirstFocusableChild: function(node
) {
615 var nodeList
= elBySelAll(_focusableElements
.join(','), node
);
616 for (var i
= 0, length
= nodeList
.length
; i
< length
; i
++) {
617 if (nodeList
[i
].offsetWidth
&& nodeList
[i
].offsetHeight
&& nodeList
[i
].getClientRects().length
) {
626 * Rebuilds dialog identified by given id.
628 * @param {string} id element id
630 rebuild: function(id
) {
631 id
= this._getDialogId(id
);
633 var data
= _dialogs
.get(id
);
634 if (data
=== undefined) {
635 throw new Error("Expected a valid dialog id, '" + id
+ "' does not match any active dialog.");
638 // ignore non-active dialogs
639 if (elAttr(data
.dialog
, 'aria-hidden') === 'true') {
643 var contentContainer
= data
.content
.parentNode
;
645 var formSubmit
= elBySel('.formSubmit', data
.content
);
646 var unavailableHeight
= 0;
647 if (formSubmit
!== null) {
648 contentContainer
.classList
.add('dialogForm');
649 formSubmit
.classList
.add('dialogFormSubmit');
651 unavailableHeight
+= DomUtil
.outerHeight(formSubmit
);
653 // Calculated height can be a fractional value and depending on the
654 // browser the results can vary. By subtracting a single pixel we're
655 // working around fractional values, without visually changing anything.
656 unavailableHeight
-= 1;
658 contentContainer
.style
.setProperty('margin-bottom', unavailableHeight
+ 'px', '');
661 contentContainer
.classList
.remove('dialogForm');
662 contentContainer
.style
.removeProperty('margin-bottom');
665 unavailableHeight
+= DomUtil
.outerHeight(data
.header
);
667 var maximumHeight
= (window
.innerHeight
* (_dialogFullHeight
? 1 : 0.8)) - unavailableHeight
;
668 contentContainer
.style
.setProperty('max-height', ~~maximumHeight
+ 'px', '');
670 // Chrome and Safari use heavy anti-aliasing when the dialog's width
671 // cannot be evenly divided, causing the whole text to become blurry
672 if (Environment
.browser() === 'chrome' || Environment
.browser() === 'safari') {
673 // The new Microsoft Edge is detected as "chrome", because effectively we're detecting
674 // Chromium rather than Chrome specifically. The workaround for fractional pixels does
675 // not work well in Edge, there seems to be a different logic for fractional positions,
676 // causing the text to be blurry.
678 // We can use `backface-visibility: hidden` to prevent the anti aliasing artifacts in
679 // WebKit/Blink, which will also prevent some weird font rendering issues when resizing.
680 data
.content
.parentNode
.classList
.add('jsWebKitFractionalPixelFix');
683 var callbackObject
= _dialogToObject
.get(id
);
684 //noinspection JSUnresolvedVariable
685 if (callbackObject
!== undefined && typeof callbackObject
._dialogSubmit
=== 'function') {
686 var inputFields
= elBySelAll('input[data-dialog-submit-on-enter="true"]', data
.content
);
688 var submitButton
= elBySel('.formSubmit > input[type="submit"], .formSubmit > button[data-type="submit"]', data
.content
);
689 if (submitButton
=== null) {
690 // check if there is at least one input field with submit handling,
691 // otherwise we'll assume the dialog has not been populated yet
692 if (inputFields
.length
=== 0) {
693 console
.warn("Broken dialog, expected a submit button.", data
.content
);
699 if (data
.submitButton
!== submitButton
) {
700 data
.submitButton
= submitButton
;
702 submitButton
.addEventListener(WCF_CLICK_EVENT
, (function (event
) {
703 event
.preventDefault();
709 var inputField
, _callbackKeydown
= null;
710 for (var i
= 0, length
= inputFields
.length
; i
< length
; i
++) {
711 inputField
= inputFields
[i
];
713 if (data
.inputFields
.has(inputField
)) continue;
715 if (_validInputTypes
.indexOf(inputField
.type
) === -1) {
716 console
.warn("Unsupported input type.", inputField
);
720 data
.inputFields
.add(inputField
);
722 if (_callbackKeydown
=== null) {
723 _callbackKeydown
= (function (event
) {
724 if (EventKey
.Enter(event
)) {
725 event
.preventDefault();
731 inputField
.addEventListener('keydown', _callbackKeydown
);
738 * Submits the dialog.
740 * @param {string} id dialog id
743 _submit: function (id
) {
744 var data
= _dialogs
.get(id
);
747 data
.inputFields
.forEach(function (inputField
) {
748 if (inputField
.required
) {
749 if (inputField
.value
.trim() === '') {
750 elInnerError(inputField
, Language
.get('wcf.global.form.error.empty'));
755 elInnerError(inputField
, false);
761 //noinspection JSUnresolvedFunction
762 _dialogToObject
.get(id
)._dialogSubmit();
767 * Handles clicks on the close button or the backdrop if enabled.
769 * @param {object} event click event
770 * @return {boolean} false if the event should be cancelled
772 _close: function(event
) {
773 event
.preventDefault();
775 var data
= _dialogs
.get(_activeDialog
);
776 if (typeof data
.onBeforeClose
=== 'function') {
777 data
.onBeforeClose(_activeDialog
);
782 this.close(_activeDialog
);
786 * Closes the current active dialog by clicks on the backdrop.
788 * @param {object} event event object
790 _closeOnBackdrop: function(event
) {
791 if (event
.target
!== _container
) {
795 if (elData(_container
, 'close-on-click') === 'true') {
799 event
.preventDefault();
804 * Closes a dialog identified by given id.
806 * @param {(string|object)} id element id or callback object
808 close: function(id
) {
809 id
= this._getDialogId(id
);
811 var data
= _dialogs
.get(id
);
812 if (data
=== undefined) {
813 throw new Error("Expected a valid dialog id, '" + id
+ "' does not match any active dialog.");
816 elAttr(data
.dialog
, 'aria-hidden', 'true');
818 // avoid keyboard focus on a now hidden element
819 if (document
.activeElement
.closest('.dialogContainer') === data
.dialog
) {
820 document
.activeElement
.blur();
823 if (typeof data
.onClose
=== 'function') {
827 // get next active dialog
828 _activeDialog
= null;
829 for (var i
= 0; i
< _container
.childElementCount
; i
++) {
830 var child
= _container
.children
[i
];
831 if (elAttr(child
, 'aria-hidden') === 'false') {
832 _activeDialog
= elData(child
, 'id');
837 UiScreen
.pageOverlayClose();
839 if (_activeDialog
=== null) {
840 elAttr(_container
, 'aria-hidden', 'true');
841 elData(_container
, 'close-on-click', 'false');
844 window
.removeEventListener('keyup', _keyupListener
);
848 data
= _dialogs
.get(_activeDialog
);
849 elData(_container
, 'close-on-click', (data
.backdropCloseOnClick
? 'true' : 'false'));
852 if (Environment
.platform() !== 'desktop') {
853 UiScreen
.scrollEnable();
858 * Returns the dialog data for given element id.
860 * @param {(string|object)} id element id or callback object
861 * @return {(object|undefined)} dialog data or undefined if element id is unknown
863 getDialog: function(id
) {
864 return _dialogs
.get(this._getDialogId(id
));
868 * Returns true for open dialogs.
870 * @param {(string|object)} id element id or callback object
873 isOpen: function(id
) {
874 var data
= this.getDialog(id
);
875 return (data
!== undefined && elAttr(data
.dialog
, 'aria-hidden') === 'false');
879 * Destroys a dialog instance.
881 * @param {Object} callbackObject the same object that was used to invoke `_dialogSetup()` on first call
883 destroy: function(callbackObject
) {
884 if (typeof callbackObject
!== 'object' || callbackObject
instanceof String
) {
885 throw new TypeError("Expected the callback object as parameter.");
888 if (_dialogObjects
.has(callbackObject
)) {
889 var id
= _dialogObjects
.get(callbackObject
).id
;
890 if (this.isOpen(id
)) {
894 // If the dialog is destroyed in the close callback, this method is
895 // called twice resulting in `_dialogs.get(id)` being undefined for
897 if (_dialogs
.has(id
)) {
898 elRemove(_dialogs
.get(id
).dialog
);
901 _dialogObjects
.delete(callbackObject
);
906 * Returns a dialog's id.
908 * @param {(string|object)} id element id or callback object
912 _getDialogId: function(id
) {
913 if (typeof id
=== 'object') {
914 var dialogData
= _dialogObjects
.get(id
);
915 if (dialogData
!== undefined) {
916 return dialogData
.id
;
920 return id
.toString();
923 _ajaxSetup: function() {