chore: increment v_js_includes (#7029)
[openemr.git] / library / dialog.js
blob64db046ac18d50c77d6d42795a22da8e2bc7b8ab
1 // Copyright (C) 2005 Rod Roark <rod@sunsetsystems.com>
2 // Copyright (C) 2018-2021 Jerry Padgett <sjpadgett@gmail.com>
3 //
4 // This program is free software; you can redistribute it and/or
5 // modify it under the terms of the GNU General Public License
6 // as published by the Free Software Foundation; either version 2
7 // of the License, or (at your option) any later version.
9 (function (define) {
10     define(['jquery'], function ($, root) {
11         let opts_default = {};
12         root = root || {};
13         root.alert = alert;
14         root.ajax = ajax;
15         root.confirm = confirm;
16         root.closeAjax = closeAjax;
17         root.close = close;
18         root.popUp = popUp;
19         return root;
21         function ajax(data) {
22             let opts = {
23                 buttons: data.buttons,
24                 allowDrag: data.allowDrag,
25                 allowResize: data.allowResize,
26                 sizeHeight: data.sizeHeight,
27                 type: data.type,
28                 resolvePromiseOn: data.resolvePromiseOn,
29                 data: data.data,
30                 url: data.url,
31                 dataType: data.dataType // xml/json/text etc.
32             };
34             let title = data.title;
36             return dlgopen('', '', data.size, 0, '', title, opts);
37         }
39         function alert(data, title) {
40             title = title ? title : 'Alert';
41             let alertTitle = '<span class="text-danger bg-light"><i class="fa fa-exclamation-triangle"></i>&nbsp;' + title + '</span>';
42             return dlgopen('', '', 675, 0, '', alertTitle, {
43                 buttons: [
44                     {text: 'OK', close: true, style: 'primary'}
45                 ],
46                 type: 'Alert',
47                 sizeHeight: 'auto',
48                 resolvePromiseOn: 'close',
49                 html: '<p class="text-center">' + data + '</p>'
50             });
51         }
53         function confirm(data, title) {
54             title = title ? title : 'Confirm';
55             let alertTitle = '<span class="text-info bg-light"><i class="fa fa-exclamation-triangle"></i>&nbsp;' + title + '</span>';
56             return dlgopen('', '', "modal-md", 0, '', alertTitle, {
57                 buttons: [
58                     {text: 'Yes', close: true, id: 'confirmYes', style: 'primary'},
59                     {text: '<i class="fa fa-thumbs-down mr-1"></i>No', close: true, id: 'confirmNo', style: 'primary'},
60                     {text: 'Nevermind', close: true, style: 'secondary'}
61                 ],
62                 type: 'Confirm',
63                 resolvePromiseOn: 'confirm',
64                 sizeHeight: 'auto',
65                 html: '<p class="text-center">' + data + '</p>'
66             });
67         }
69         /* popUp
70         * Borrowed from a CKEditor Source plugin and modified to suit my purpose.
71         * Licensed under the GPL 2 or greater.
72         * @license Copyright (c) 2003-2021, CKSource - Frederico Knabben. All rights reserved.
73         * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
74         */
75         function popUp( url, data, name, width, height) {
76             width = width || '80%'; height = height || '85%';
78             if ( typeof width == 'string' && width.length > 1 && width.substr( width.length - 1, 1 ) === '%') {
79                 width = parseInt(window.screen.width * parseInt(width, 10) / 100, 10);
80             }
81             if ( typeof height == 'string' && height.length > 1 && height.substr( height.length - 1, 1 ) === '%') {
82                 height = parseInt(window.screen.height * parseInt(height, 10) / 100, 10);
83             }
84             if ( width < 640 ) {
85                 width = 640;
86             }
87             if ( height < 420 ) {
88                 height = 420;
89             }
90             let top = parseInt(( window.screen.height - height ) / 2, 10);
91             let left = parseInt(( window.screen.width - width ) / 2, 10);
93             let options = ('location=no,menubar=no,toolbar=no,dependent=yes,minimizable=no,modal=yes,alwaysRaised=yes,resizable=yes,scrollbars=yes') +
94                 ',width=' + width +
95                 ',height=' + height +
96                 ',top=' + top +
97                 ',left=' + left;
99             let modalWindow = window.open('', name, options, true);
100             if ( !modalWindow ) {
101                 return false;
102             }
103             try {
104                 modalWindow.focus();
105                 if (data) {
106                     modalWindow.document.body.innerHTML = data;
107                 } else {
108                     modalWindow.location.href = url;
109                 }
110             } catch ( e ) {
111                 window.open(url, null, options, true);
112             }
114             return true;
115         }
117         function closeAjax() {
118             dlgCloseAjax();
119         }
121         function close() {
122             dlgclose();
123         }
124     });
126     if (typeof includeScript !== 'function') {
127         // Obviously utility.js has not been included for an unknown reason!
128         // Will include below.
129         /* eslint-disable-next-line no-inner-declarations */
130         function includeScript(srcUrl, type) {
131             return new Promise(function (resolve, reject) {
132                 if (type == 'script') {
133                     let newScriptElement = document.createElement('script');
134                     newScriptElement.src = srcUrl;
135                     newScriptElement.onload = () => resolve(newScriptElement);
136                     newScriptElement.onerror = () => reject(new Error(`Script load error for ${srcUrl}`));
138                     document.head.append(newScriptElement);
139                     console.log('Needed to load:[' + srcUrl + '] For: [' + location + ']');
140                 }
141                 if (type === "link") {
142                     let newScriptElement = document.createElement("link")
143                     newScriptElement.type = "text/css";
144                     newScriptElement.rel = "stylesheet";
145                     newScriptElement.href = srcUrl;
146                     newScriptElement.onload = () => resolve(newScriptElement);
147                     newScriptElement.onerror = () => reject(new Error(`Link load error for ${srcUrl}`));
149                     document.head.append(newScriptElement);
150                     console.log('Needed to load:[' + srcUrl + '] For: [' + location + ']');
151                 }
152             });
153         }
154     }
155     if (typeof window.xl !== 'function') {
156         (async (utilfn) => {
157             await includeScript(utilfn, 'script');
158         })(top.webroot_url + '/library/js/utility.js')
159     }
160 }(typeof define == 'function' && define.amd ?
161     define :
162     function (args, mName) {
163         this.dialog = typeof module != 'undefined' && module.exports ?
164             mName(require(args[0], {}), module.exports) :
165             mName(window.$);
166     }));
169 // open a new cascaded window
170 function cascwin(url, winname, width, height, options) {
171     var mywin = window.parent ? window.parent : window;
172     var newx = 25, newy = 25;
173     if (!isNaN(mywin.screenX)) {
174         newx += mywin.screenX;
175         newy += mywin.screenY;
176     } else if (!isNaN(mywin.screenLeft)) {
177         newx += mywin.screenLeft;
178         newy += mywin.screenTop;
179     }
180     if ((newx + width) > screen.width || (newy + height) > screen.height) {
181         newx = 0;
182         newy = 0;
183     }
184     if (typeof top.restoreSession === 'function') {
185         top.restoreSession();
186     }
188     // MS IE version detection taken from
189     // http://msdn2.microsoft.com/en-us/library/ms537509.aspx
190     // to adjust the height of this box for IE only -- JRM
191     if (navigator.appName == 'Microsoft Internet Explorer') {
192         var ua = navigator.userAgent;
193         var re = new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})");
194         if (re.exec(ua) != null)
195             rv = parseFloat(RegExp.$1); // this holds the version number
196         height = height + 28;
197     }
199     retval = window.open(url, winname, options +
200         ",width=" + width + ",height=" + height +
201         ",left=" + newx + ",top=" + newy +
202         ",screenX=" + newx + ",screenY=" + newy);
204     return retval;
207 // recursive window focus-event grabber
208 function grabfocus(w) {
209     for (var i = 0; i < w.frames.length; ++i) grabfocus(w.frames[i]);
210     w.onfocus = top.imfocused;
213 // Call this when a "modal" windowed dialog is desired.
214 // Note that the below function is free standing for either
215 // ui's.Use dlgopen() for responsive b.s modal dialogs.
216 // Can now use anywhere to cascade natives...12/1/17 sjp
218 function dlgOpenWindow(url, winname, width, height) {
219     if (top.modaldialog && !top.modaldialog.closed) {
220         if (window.focus) top.modaldialog.focus();
221         if (top.modaldialog.confirm(top.oemr_dialog_close_msg)) {
222             top.modaldialog.close();
223             top.modaldialog = null;
224         } else {
225             return false;
226         }
227     }
228     top.modaldialog = cascwin(url, winname, width, height,
229         "resizable=1,scrollbars=1,location=0,toolbar=0");
231     return false;
234 // This is called from del_related() which in turn is invoked by find_code_dynamic.php.
235 // Deletes the specified codetype:code from the indicated input text element.
236 function my_del_related(s, elem, usetitle) {
237     if (!s) {
238         // Deleting everything.
239         elem.value = '';
240         if (usetitle) {
241             elem.title = '';
242         }
243         return;
244     }
245     // Convert the codes and their descriptions to arrays for easy manipulation.
246     var acodes = elem.value.split(';');
247     var i = acodes.indexOf(s);
248     if (i < 0) {
249         return; // not found, should not happen
250     }
251     // Delete the indicated code and description and convert back to strings.
252     acodes.splice(i, 1);
253     elem.value = acodes.join(';');
254     if (usetitle) {
255         var atitles = elem.title.split(';');
256         atitles.splice(i, 1);
257         elem.title = atitles.join(';');
258     }
261 function dialogID() {
262     function s4() {
263         return Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1);
264     }
266     return s4() + s4() + s4() + s4() + s4() + +s4() + s4() + s4();
269 // test for and/or remove dependency.
270 function inDom(dependency, type, remove) {
271     let el = type;
272     let attr = type === 'script' ? 'src' : type === 'link' ? 'href' : 'none';
273     let all = document.getElementsByTagName(el);
274     for (let i = all.length; i > -1; i--) {
275         if (all[i] && all[i].getAttribute(attr) !== null && all[i].getAttribute(attr).indexOf(dependency) !== -1) {
276             if (remove) {
277                 all[i].parentNode.removeChild(all[i]);
278                 console.log("Removed from DOM: " + dependency);
279                 return true;
280             } else {
281                 return true;
282             }
283         }
284     }
285     return false;
288 // test to see if bootstrap theming is loaded (via standard or custom bootstrap library)
289 //  Will check for the badge-secondary class
290 //   - if exist, then assume bootstrap loaded
291 //   - if not exist, then assume bootstrap not loaded
292 function isBootstrapCss() {
293     for (let i = 0; i < document.styleSheets.length; i++) {
294         let rules = document.styleSheets[i].rules || document.styleSheets[i].cssRules;
295         for (let x in rules) {
296             if (rules[x].selectorText == '.badge-secondary') {
297                 return true;
298             }
299         }
300     }
301     return false;
304 // These functions may be called from scripts that may be out of scope with top so...
305 // if opener is tab then we need to be in tabs UI scope and while we're at it, let's bring webroot along...
307 if (typeof top.webroot_url === "undefined" && opener) {
308     if (typeof opener.top.webroot_url !== "undefined") {
309         top.webroot_url = opener.top.webroot_url;
310     }
312 // We'll need these if out of scope
314 if (typeof top.set_opener !== "function") {
315     var opener_list = [];
316     /* eslint-disable-next-line no-inner-declarations */
317     function set_opener(window, opener) {
318         top.opener_list[window] = opener;
319     }
320     /* eslint-disable-next-line no-inner-declarations */
321     function get_opener(window) {
322         return top.opener_list[window];
323     }
326 // universal alert popup message
327 if (typeof alertMsg !== "function") {
328     /* eslint-disable-next-line no-inner-declarations */
329     function alertMsg(message, timer = 5000, type = 'danger', size = '', persist = '') {
330         // this xl() is just so cool.
331         let gotIt = xl("Got It");
332         let title = xl("Alert");
333         let dismiss = xl("Dismiss");
334         $('#alert_box').remove();
335         let oHidden = '';
336         oHidden = !persist ? "hidden" : '';
337         let oSize = (size == 'lg') ? 'left:10%;width:80%;' : 'left:25%;width:50%;';
338         let style = "position:fixed;top:25%;" + oSize + " bottom:0;z-index:9999;";
339         $("body").prepend("<div class='container text-center' id='alert_box' style='" + style + "'></div>");
340         let mHtml = '<div id="alertmsg" class="alert alert-' + type + ' alert-dismissable">' +
341             '<button type="button" class="btn btn-link ' + oHidden + '" id="dontShowAgain" data-dismiss="alert">' +
342             gotIt + '&nbsp;<i class="fa fa-thumbs-up"></i></button>' +
343             '<h4 class="alert-heading text-center">' + title + '!</h4><hr>' + '<p class="bg-light text-dark">' + message + '</p>' +
344             '<button type="button" id="alertDismissButton" class="pull-right btn btn-link" data-dismiss="alert">' + dismiss + '</button><br /></div>';
345         $('#alert_box').append(mHtml);
346         $('#alertmsg').on('closed.bs.alert', function () {
347             clearTimeout(AlertMsg);
348             $('#alert_box').remove();
349             return false;
350         });
351         $('#dontShowAgain').on('click', function (e) {
352             clearTimeout(AlertMsg);
353             $('#alert_box').remove();
354             persistUserOption(persist, 1);
355         });
356         $('#alertDismissButton').on('click', function (e) {
357             clearTimeout(AlertMsg);
358             $('#alert_box').remove();
359         });
360         let AlertMsg = setTimeout(function () {
361             $('#alertmsg').fadeOut(800, function () {
362                 $('#alert_box').remove();
363             });
364         }, timer);
365     }
367     const persistUserOption = function (option, value) {
368         return $.ajax({
369             url: top.webroot_url + "/library/ajax/user_settings.php",
370             type: 'post',
371             contentType: 'application/x-www-form-urlencoded',
372             data: {
373                 csrf_token_form: top.csrf_token_js,
374                 target: option,
375                 setting: value
376             },
377             beforeSend: function () {
378                 if (typeof top.restoreSession === 'function') {
379                     top.restoreSession();
380                 }
381             },
382             error: function (jqxhr, status, errorThrown) {
383                 console.log(errorThrown);
384             }
385         });
386     };
390 // Test if supporting dialog callbacks and close dependencies are in scope.
391 // This is useful when opening and closing the dialog is in the same scope. Still use include_opener.js
392 // in script that will close a dialog that is not in the same scope dlgopen was used
393 // or use parent.dlgclose() if known decedent.
394 // dlgopen() will always have a name whether assigned by dev or created by function.
395 // Callback, onClosed and button clicks are still available either way.
396 // For a callback on close use: dlgclose(functionName, farg1, farg2 ...) which becomes: functionName(farg1,farg2, etc)
398 if (typeof dlgclose !== "function") {
399     if (!opener) {
400         /* eslint-disable-next-line no-global-assign */
401         opener = window;
402     }
403     /* eslint-disable-next-line no-inner-declarations */
404     function dlgclose(call, args) {
405         var frameName = window.name;
406         var wframe = top;
407         if (frameName === "") {
408             // try to find dialog. dialogModal is embedded dialog class
409             // It has to be here somewhere.
410             frameName = $(".dialogModal").attr("id");
411             if (!frameName) {
412                 frameName = parent.$(".dialogModal").attr("id");
413                 if (!frameName) {
414                     console.log("Unable to find dialog.");
415                     return false;
416                 }
417             }
418         }
419         var dialogModal = top.$("div#" + frameName);
421         var removeFrame = dialogModal.find("iframe[name='" + frameName + "']");
422         if (removeFrame.length > 0) {
423             removeFrame.remove();
424         }
426         if (dialogModal.length > 0) {
427             if (call) {
428                 wframe.setCallBack(call, args); // sets/creates callback function in dialogs scope.
429             }
430             dialogModal.modal("hide");
431         } else {
432             // no opener not iframe must be in here
433             $(this.document).find(".dialogModal").modal("hide");
434         }
435     }
439 * function dlgopen(url, winname, width, height, forceNewWindow, title, opts)
441 * @summary Stackable, resizable and draggable responsive ajax/iframe dialog modal.
443 * @param {url} string Content location.
444 * @param {String} winname If set becomes modal id and/or iframes name. Or, one is created/assigned(iframes).
445 * @param {Number| String} width|modalSize(modal-xl) For sizing: an number will be converted to a percentage of view port width.
446 * @param {Number} height Initial minimum height. For iframe auto resize starts at this height.
447 * @param {boolean} forceNewWindow Force using a native window.
448 * @param {String} title If exist then header with title is created otherwise no header and content only.
449 * @param {Object} opts Dialogs options.
450 * @returns {Object} dialog object reference.
451 * */
452 function dlgopen(url, winname, width, height, forceNewWindow, title, opts) {
453     // First things first...
454     if (typeof top.restoreSession === 'function') {
455         top.restoreSession();
456     }
458     // A matter of Legacy
459     if (forceNewWindow) {
460         return dlgOpenWindow(url, winname, width, height);
461     }
463     // wait for DOM then check dependencies needed to run this feature.
464     // dependency duration is while 'this' is in scope, temporary...
465     // seldom will this get used as more of U.I is moved to Bootstrap
466     // but better to continue than stop because of a dependency...
467     //
468     let jqurl = top.webroot_url + '/public/assets/jquery/dist/jquery.min.js';
469     if (typeof jQuery === 'undefined') {
470         (async (utilfn) => {
471             await includeScript(utilfn, 'script');
472         })(jqurl);
473     }
474     jQuery(function () {
475         // Check for dependencies we will need.
476         // webroot_url is a global defined in main_screen.php or main.php.
477         let bscss = top.webroot_url + '/public/assets/bootstrap/dist/css/bootstrap.min.css';
478         let bscssRtl = top.webroot_url + '/public/assets/bootstrap-v4-rtl/dist/css/bootstrap-rtl.min.css';
479         let bsurl = top.webroot_url + '/public/assets/bootstrap/dist/js/bootstrap.bundle.min.js';
481         let version = jQuery.fn.jquery.split(' ')[0].split('.');
482         if ((version[0] < 2 && version[1] < 9) || (version[0] === 1 && version[1] === 9 && version[2] < 1)) {
483             inDom('jquery-min', 'script', true);
484             (async (utilfn) => {
485                 await includeScript(utilfn, 'script');
486             })(jqurl).then(() => {
487                 console.log('Replacing jQuery version:[ ' + version + ' ]');
488             });
489         }
490         if (!isBootstrapCss()) {
491             (async (utilfn) => {
492                 await includeScript(utilfn, 'link');
493             })(bscss);
494             if (top.jsLanguageDirection == 'rtl') {
495                 (async (utilfn) => {
496                     await includeScript(utilfn, 'link');
497                 })(bscssRtl);
498             }
499         }
500         if (typeof jQuery.fn.modal === 'undefined') {
501             if (!inDom('bootstrap.bundle.min.js', 'script', false)) {
502                 (async (utilfn) => {
503                     await includeScript(utilfn, 'script');
504                 })(bsurl);
505             }
506         }
507     });
509     // onward
510     var opts_defaults = {
511         type: 'iframe', // POST, GET (ajax) or iframe
512         async: true,
513         frameContent: "", // for iframe embedded content
514         html: "", // content for alerts, comfirm etc ajax
515         allowDrag: false,
516         allowResize: true,
517         sizeHeight: 'auto', // 'full' will use as much height as allowed
518         // use is onClosed: fnName ... args not supported however, onClosed: 'reload' is auto defined and requires no function to be created.
519         onClosed: false,
520         allowExternal: false, // allow a dialog window to a URL that is external to the current url
521         callBack: false, // use {call: 'functionName, args: args, args} if known or use dlgclose.
522         resolvePromiseOn: '' // this may be useful. values are init, shown, show, confirm, alert and close which coincide with dialog events.
523     };
525     if (!opts) {
526         opts = {};
527     }
528     opts = jQuery.extend({}, opts_defaults, opts);
529     opts.type = opts.type ? opts.type.toLowerCase() : '';
530     opts.resolvePromiseOn = opts.resolvePromiseOn ?? 'init';
531     var mHeight, mWidth, mSize, msSize, dlgContainer, fullURL, where; // a growing list...
533     where = (opts.type === 'iframe') ? top : window;
535     // get url straight...
536     fullURL = "";
537     if (opts.url) {
538         url = opts.url;
539     }
540     if (url) {
541         if (url[0] === "/") {
542             fullURL = url
543         } else if (opts.allowExternal === true) {
544             var checkUrl = new URL(url);
545             // we only allow http & https protocols to be launched
546             if (checkUrl.protocol === "http:" || checkUrl.protocol == "https:") {
547                 fullURL = url;
548             }
549         } else {
550             fullURL = window.location.href.substr(0, window.location.href.lastIndexOf("/") + 1) + url;
551         }
552     }
554     // what's a window without a name. important for stacking and opener.
555     winname = (winname === "_blank" || !winname) ? dialogID() : winname;
557     // for small screens or request width is larger than viewport.
558     if (where.innerWidth <= 1080) {
559         width = "modal-full";
560     }
561     // Convert dialog size to percentages and/or css class.
562     var sizeChoices = ['modal-sm', 'modal-md', 'modal-mlg', 'modal-lg', 'modal-xl', 'modal-full'];
563     if (Math.abs(width) > 0) {
564         width = Math.abs(width);
565         mWidth = (width / where.innerWidth * 100).toFixed(1) + '%';
566         msSize = '<style>.modal-custom-' + winname + ' {max-width:' + mWidth + ' !important;}</style>';
567         mSize = 'modal-custom' + winname;
568     } else if (jQuery.inArray(width, sizeChoices) !== -1) {
569         mSize = width; // is a modal class
570     } else {
571         msSize = '<style>.modal-custom-' + winname + ' {max-width:35% !important;}</style>'; // standard B.S. modal default (modal-md)
572     }
573     // leave below for legacy
574     if (mSize === 'modal-sm') {
575         msSize = '<style>.modal-custom-' + winname + ' {max-width:25% !important;}</style>';
576     } else if (mSize === 'modal-md') {
577         msSize = '<style>.modal-custom-' + winname + ' {max-width:40% !important;}</style>';
578     } else if (mSize === 'modal-mlg') {
579         msSize = '<style>.modal-custom-' + winname + ' {max-width:55% !important;}</style>';
580     } else if (mSize === 'modal-lg') {
581         msSize = '<style>.modal-custom-' + winname + ' {max-width:75% !important;}</style>';
582     } else if (mSize === 'modal-xl') {
583         msSize = '<style>.modal-custom-' + winname + ' {max-width:92% !important;}</style>';
584     } else if (mSize === 'modal-full') {
585         msSize = '<style>.modal-custom-' + winname + ' {max-width:97% !important;}</style>';
586     }
587     mSize = 'modal-custom-' + winname;
589     // Initial responsive height.
590     let vpht = where.innerHeight;
591     if (height <= 300 && opts.type === 'iframe') {
592         height = 300;
593     }
594     mHeight = height > 0 ? (height / vpht * 100).toFixed(1) + 'vh' : '';
596     // Build modal template. For now !title = !header and modal full height.
597     var mTitle = title > "" ? '<h5 class=modal-title>' + title + '</h5>' : '';
599     var waitHtml =
600         '<div class="loadProgress text-center">' +
601         '<span class="fa fa-circle-notch fa-spin fa-3x text-primary"></span>' +
602         '</div>';
604     var headerhtml =
605         ('<div class="modal-header">%title%<button type="button" class="close" data-dismiss="modal">' +
606             '&times;</button></div>').replace('%title%', mTitle);
608     var frameHtml =
609         ('<iframe id="modalframe" class="modalIframe w-100 h-100 border-0" name="%winname%" %url%></iframe>').replace('%winname%', winname).replace('%url%', fullURL ? 'src=' + fullURL : '');
611     var contentStyles = ('style="height:%initHeight%; max-height: 94vh"').replace('%initHeight%', opts.sizeHeight !== 'full' ? mHeight : '90vh');
613     var altClose = '<div class="closeDlgIframe" data-dismiss="modal" ></div>';
615     var mhtml =
616         ('<div id="%id%" class="modal fade dialogModal" tabindex="-1" role="dialog">%sizeStyle%' +
617             '<style>.drag-resize {touch-action:none;user-select:none;}</style>' +
618             '<div %dialogId% class="modal-dialog %drag-action% %sizeClass%" role="dialog">' +
619             '<div class="modal-content %resize-action%" %contentStyles%>' + '%head%' + '%altclose%' + '%wait%' +
620             '<div class="modal-body px-1 h-100">' + '%body%' + '</div></div></div></div>').replace('%id%', winname).replace('%sizeStyle%', msSize ? msSize : '').replace('%dialogId%', opts.dialogId ? ('id=' + opts.dialogId + '"') : '').replace('%sizeClass%', mSize ? mSize : '').replace('%head%', mTitle !== '' ? headerhtml : '').replace('%altclose%', mTitle === '' ? altClose : '').replace('%drag-action%', (opts.allowDrag) ? 'drag-action' : '').replace('%resize-action%', (opts.allowResize) ? 'resize-action' : '').replace('%wait%', '').replace('%contentStyles%', contentStyles).replace('%body%', opts.type === 'iframe' ? frameHtml : '');
622     // Write modal template.
623     dlgContainer = where.jQuery(mhtml);
624     dlgContainer.attr("name", winname);
626     // No url and just iframe content
627     if (opts.frameContent && opts.type === 'iframe') {
628         var ipath = 'data:text/html,' + encodeURIComponent(opts.frameContent);
629         dlgContainer.find("iframe[name='" + winname + "']").attr("src", ipath);
630     }
632     if (opts.buttons) {
633         dlgContainer.find('.modal-content').append(buildFooter());
634     }
635     // Ajax setup
636     if (opts.type === 'alert') {
637         dlgContainer.find('.modal-body').html(opts.html);
638     }
639     if (opts.type === 'confirm') {
640         dlgContainer.find('.modal-body').html(opts.html);
641     }
642     if (opts.type !== 'iframe' && opts.type !== 'alert' && opts.type !== 'confirm') {
643         var params = {
644             async: opts.async,
645             method: opts.type || '', // if empty and has data object, then post else get.
646             content: opts.data || opts.html, // ajax loads fetched content.
647             url: opts.url || fullURL,
648             dataType: opts.dataType || '' // xml/json/text etc.
649         };
651         dialogAjax(params, dlgContainer, opts);
652     }
654     // let opener array know about us.
655     top.set_opener(winname, window);
657     // Write the completed template to calling document or 'where' window.
658     where.jQuery("body").append(dlgContainer);
660     // We promised
661     return new Promise((resolve, reject) => {
662         jQuery(function () {
663             // DOM Ready. Handle events and cleanup.
664             if (opts.type === 'iframe') {
665                 var modalwin = where.jQuery('body').find("[name='" + winname + "']");
666                 jQuery('div.modal-dialog', modalwin).css({'margin': "0.75rem auto auto"});
667                 modalwin.on('load', function (e) {
668                     setTimeout(function () {
669                         if (opts.sizeHeight === 'auto' && opts.type === 'iframe') {
670                             SizeModaliFrame(e, height);
671                         } else if (opts.sizeHeight === 'fixed') {
672                             sizing(e, height);
673                         } else {
674                             sizing(e, height); // must be full height of container
675                         }
676                     }, 800);
677                 });
678             } else {
679                 var modalwin = where.jQuery('body').find("[name='" + winname + "']");
680                 jQuery('div.modal-dialog', modalwin).css({'margin': '15px auto auto'});
681                 modalwin.on('show.bs.modal', function (e) {
682                     setTimeout(function () {
683                         sizing(e, height);
684                     }, 800);
685                 });
686             }
687             if (opts.resolvePromiseOn === 'confirm') {
688                 jQuery("#confirmYes").on('click', function (e) {
689                     resolve(true);
690                 });
691                 jQuery("#confirmNo").on('click', function (e) {
692                     resolve(false);
693                 });
694             }
695             // events chain.
696             dlgContainer.on('show.bs.modal', function () {
697                 if (opts.allowResize || opts.allowDrag) {
698                     initDragResize(where.document, where.document);
699                 }
701                 if (opts.resolvePromiseOn === 'show') {
702                     resolve(dlgContainer);
703                 }
704             }).on('shown.bs.modal', function () {
705                 // Remove waitHtml spinner/loader etc.
706                 jQuery(this).parent().find('div.loadProgress').fadeOut(function () {
707                     jQuery(this).remove();
708                 });
709                 dlgContainer.modal('handleUpdate'); // allow for scroll bar
711                 if (opts.resolvePromiseOn === 'shown') {
712                     resolve(dlgContainer);
713                 }
714             }).on('hidden.bs.modal', function (e) {
715                 // clear cursor
716                 e.target.style.cursor = "pointer";
717                 // remove our dialog
718                 jQuery(this).remove();
719                 // now we can run functions in our window.
720                 if (opts.onClosed) {
721                     console.log('Doing onClosed:[' + opts.onClosed + ']');
722                     if (opts.onClosed === 'reload') {
723                         window.location.reload();
724                     } else {
725                         window[opts.onClosed]();
726                     }
727                 }
728                 if (opts.callBack.call) {
729                     console.log('Doing callBack:[' + opts.callBack.call + '|' + opts.callBack.args + ']');
730                     if (opts.callBack.call === 'reload') {
731                         window.location.reload();
732                     } else if (typeof opts.callBack.call == 'string') {
733                         window[opts.callBack.call](opts.callBack.args);
734                     } else {
735                         opts.callBack.call(opts.callBack.args);
736                     }
737                 }
739                 if (opts.resolvePromiseOn == 'close') {
740                     resolve(dlgContainer);
741                 }
742             });
744             // define local dialog close() function. openers scope
745             window.dlgCloseAjax = function (calling, args) {
746                 if (calling) {
747                     opts.callBack = {call: calling, args: args};
748                 }
749                 dlgContainer.modal('hide'); // important to clean up in only one place, hide event....
750                 return false;
751             };
753             // define local callback function. Set with opener or from opener, will exe on hide.
754             window.dlgSetCallBack = function (calling, args) {
755                 opts.callBack = {call: calling, args: args};
756                 return false;
757             };
759             // in residents dialog scope
760             where.setCallBack = function (calling, args) {
761                 opts.callBack = {call: calling, args: args};
762                 return true;
763             };
765             where.getOpener = function () {
766                 return where;
767             };
768             // dialog is completely built and events set
769             // this is default returning our dialog container reference.
770             if (opts.resolvePromiseOn == 'init') {
771                 resolve(dlgContainer);
772             }
773             // Finally Show Dialog after DOM settles
774             dlgContainer.modal({backdrop: 'static', keyboard: true}, 'show');
775         }); // end events
776     }); /* Returning Promise */
778     // Ajax call with promise via dialog
779     function dialogAjax(data, $dialog, opts) {
780         var params = {
781             async: data.async,
782             method: data.method || '',
783             data: data.content,
784             url: data.url,
785             dataType: data.dataType || 'html'
786         };
788         if (data.url) {
789             jQuery.extend(params, data);
790         }
792         jQuery.ajax(params).done(aOkay).fail(oops);
794         return true;
796         function aOkay(html) {
797             opts.ajax = true;
798             $dialog.find('.modal-body').html(data.success ? data.success(html) : html);
800             return true;
801         }
803         function oops(r, s) {
804             var msg = data.error ?
805                 data.error(r, s, params) :
806                 '<div class="alert alert-danger">' +
807                 '<strong>XHR Failed:</strong> [ ' + params.url + '].' + '</div>';
809             $dialog.find('.modal-body').html(msg);
811             return false;
812         }
813     }
815     function buildFooter() {
816         if (opts.buttons === false) {
817             return '';
818         }
819         var oFoot = jQuery('<div>').addClass('modal-footer').prop('id', 'oefooter');
820         if (opts.buttons) {
821             for (var i = 0, k = opts.buttons.length; i < k; i++) {
822                 var btnOp = opts.buttons[i];
823                 if (typeof btnOp.class !== 'undefined') {
824                     btnOp.class = btnOp.class.replace(/default/gi, 'secondary');
825                     var btn = jQuery('<button>').addClass('btn ' + (btnOp.class || 'btn-primary'));
826                 } else { // legacy
827                     btnOp.style = btnOp.style.replace(/default/gi, 'secondary');
828                     var btn = jQuery('<button>').addClass('btn btn-' + (btnOp.style || 'primary'));
829                     btnOp.style = "";
830                 }
831                 for (var index in btnOp) {
832                     if (Object.prototype.hasOwnProperty.call(btnOp, index)) {
833                         switch (index) {
834                             case "close":
835                                 //add close event
836                                 if (btnOp[index]) {
837                                     btn.attr("data-dismiss", "modal");
838                                 }
839                                 break;
840                             case "click":
841                                 //binds button to click event of fn defined in calling document/form
842                                 var fn = btnOp.click.bind(
843                                     dlgContainer.find(".modal-content")
844                                 );
845                                 btn.click(fn);
846                                 break;
847                             case "text":
848                                 btn.html(btnOp[index]);
849                                 break;
850                             case "class":
851                                 break;
852                             default:
853                                 //all other possible HTML attributes to button element
854                                 // name, id etc
855                                 btn.attr(index, btnOp[index]);
856                         }
857                     }
858                 }
860                 oFoot.append(btn);
861             }
862         }
863         return oFoot; // jquery object of modal footer.
864     }
866     // dynamic sizing - special case for full height
867     function sizing(e, height) {
868         let viewPortHt = 0;
869         if (opts.sizeHeight === 'auto') {
870             dlgContainer.find('div.modal-body').css({'overflow-y': 'auto'});
871             // let BS determine height for alerts etc
872             return;
873         }
874         let $idoc = jQuery(e.currentTarget);
875         viewPortHt = Math.max(window.document.documentElement.clientHeight, window.innerHeight || 0);
876         let frameContentHt = opts.sizeHeight === 'full' ? viewPortHt : height;
877         frameContentHt = frameContentHt >= viewPortHt ? viewPortHt : frameContentHt;
878         size = (frameContentHt / viewPortHt * 100).toFixed(2);
879         size = size + 'vh';
880         dlgContainer.find('div.modal-content').css({'height': size});
881         if (opts.type === 'iframe') {
882             dlgContainer.find('div.modal-body').css({'overflow-y': 'hidden'});
883         } else {
884             dlgContainer.find('div.modal-body').css({'overflow-y': 'auto'});
885         }
888         return size;
889     }
891     // sizing for modals with iframes
892     function SizeModaliFrame(e, minSize) {
893         let viewPortHt = where.window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
894         let frameContentHt = 0;
895         let idoc = null;
896         try {
897             idoc = e.currentTarget.contentDocument ? e.currentTarget.contentDocument : e.currentTarget.contentWindow.document;
898             jQuery(e.currentTarget).parents('div.modal-content').css({'height': 0});
899             frameContentHt = Math.max(jQuery(idoc).height(), idoc.body.offsetHeight) + 60;
900         } catch (err) {
901             frameContentHt = minSize + 60;
902         }
903         frameContentHt = frameContentHt <= minSize ? minSize : frameContentHt;
904         frameContentHt = frameContentHt >= viewPortHt ? viewPortHt : frameContentHt;
905         size = (frameContentHt / viewPortHt * 100).toFixed(1);
906         size = size + 'vh';
907         jQuery(e.currentTarget).parents('div.modal-content').css({'height': size});
909         return size;
910     }