PageSourceSearch

https://heuristref.net/heurist/hclient/core/utils_msg.js

js heuristref.net collected 2026-10-04 16:23:52 UTC 101,357 bytes, 2,158 lines download raw bytes

1/**
2 * @file utils_msg.js
3 * @brief Provides a comprehensive suite of functions for displaying messages, dialogs, popups, and notifications.
4 * @fileOverview This file defines the `HEURIST4.msg` namespace, which includes a variety of utilities for user interaction.
5 * These utilities cover:
6 * - Standard error message dialogs (`showMsgErr`, `showMsgErrJson`).
7 * - General purpose dialogs with customizable buttons and titles (`showMsgDlg`, `showMsg`).
8 * - Dialogs for loading external URL content (`showMsgDlgUrl`, `showDialog`).
9 * - Prompt dialogs for user input (`showPrompt`).
10 * - Flash messages and tooltips for temporary notifications (`showMsgFlash`, `showTooltipFlash`).
11 * - Input validation helpers that display messages (`checkLength`, `checkLength2`).
12 * - Progress indicators (`showProgress`, `hideProgress`).
13 * - Specialized dialogs like exit warnings (`showMsgOnExit`) and messages for parent record operations (`prepareParentRecordMsg`).
14 * The functions often leverage jQuery UI Dialog for their implementation and provide options for styling and behavior.
15 * @project     Heurist academic knowledge management system
16 *
17 * @link https://HeuristNetwork.org
18 * @copyright (C) 2005-2023 University of Sydney, (C) 2024 onwards Heurist Network
19 * @license https://www.gnu.org/licenses/gpl-3.0.txt GNU License 3.0
20 * @author Artem Osmakov <[email protected]>
21 * @author Ian Johnson <[email protected]>
22 * @since 4.0
23 */
24
25if (!window.hWin.HEURIST4){
26    window.hWin.HEURIST4 = {};
27}
28
29/**
30 * @namespace HEURIST4.msg
31 * @memberof HEURIST4
32 * @description Provides a comprehensive set of functions for displaying various types of messages,
33 * dialogs, popups, tooltips, and progress indicators within the Heurist application.
34 * This includes error handling, informational messages, user prompts, modal dialogs for
35 * iframe content, and temporary flash notifications.
36 */
37if (! window.hWin.HEURIST4.msg) window.hWin.HEURIST4.msg = {
38
39    /**
40     * Displays an error message, specifically handling JSON-related error responses.
41     * If the response is a string (implying a JSON parsing error), it shows a generic JSON parse error.
42     * Otherwise, it passes the response object to `showMsgErr`.
43     *
44     * @param {Object|string} response - The error response object, or a string message if JSON parsing failed.
45     *                                   If an object, it's passed to `showMsgErr`.
46     * @memberof HEURIST4.msg
47     * @returns {void}
48     */
49    showMsgErrJson: function(response){
50        if(typeof response === "string"){
51            window.hWin.HEURIST4.msg.showMsgErr(null); // TODO: Review if passing null is intended or should be a specific message
52        }else{
53            // Assuming response is an error object from a failed JSON.parse, not the unparsed string.
54            // If response IS the unparsed string that failed, this message might be confusing.
55            // For now, documenting as is.
56            window.hWin.HEURIST4.msg.showMsgErr(window.hWin.HR('Error_Json_Parse')+': '+String(response).slice(0,255)+'...');
57        }
58    },
59
60    /**
61     * Displays a detailed error message dialog based on a response object or string.
62     * It handles different error statuses (system fatal, invalid request, access denied, DB error, etc.),
63     * formats the message, sets an appropriate title, and may include system messages or a prompt to login.
64     * Uses `showMsgDlg` for the actual display.
65     *
66     * @memberof HEURIST4.msg
67     * @param {Object|string} response - The error response object or a simple error message string.
68     *                                   If an object, it can have properties like `message`, `status`,
69     *                                   `sysmsg`, `error_title`, `request_code`.
70     * @param {boolean} [needlogin=false] - If true and the error indicates an access denied due to not being logged in,
71     *                                   it may trigger a login prompt after closing the dialog.
72     * @param {Object} [ext_options] - Additional options passed to `showMsgDlg` for customizing the dialog appearance.
73     *                                 Typically includes `default_palette_class: 'ui-heurist-error'`.
74     * @returns {string} The formatted error message string that was displayed.
75     */
76    showMsgErr: function(response, needlogin, ext_options){
77        let msg = '';
78        let dlg_title = null;
79        let show_login_dlg = false;
80        
81        window.hWin.HEURIST4.msg.sendCoverallToBack(true);
82        window.hWin.HEURIST4.msg.closeMsgFlash();
83        
84        if(typeof response === "string"){
85            msg = response;
86        }else{
87            let request_code = null;
88            
89            if($.isPlainObject(response.message)){
90                    response = response.message;
91            }
92            
93            if(window.hWin.HEURIST4.util.isempty(response.message) || response.message.trim().toLowerCase() == 'error'){
94
95                msg = 'Error_Empty_Message';
96                if(response){
97                    if(response.status==window.hWin.ResponseStatus.REQUEST_DENIED ){
98                        msg = '';
99                    }else if (!window.hWin.HEURIST4.util.isempty(response.request_code)){
100                        request_code = response.request_code;    
101                    }
102                }
103            }else{
104                msg = response.message;
105            }
106            msg = window.hWin.HR(msg);
107
108            dlg_title = response.error_title?response.error_title:'';
109
110            if(response.sysmsg && response.status!=window.hWin.ResponseStatus.REQUEST_DENIED){
111                //sysmsg for REQUEST_DENIED is current user id - it allows to check if session is expired
112                msg = msg + '<br><br>System error:<br>';
113                if(typeof response.sysmsg['join'] === "function"){
114                    msg = msg + response.sysmsg.join('<br>');
115                }else{
116                    msg = msg + response.sysmsg;
117                }
118
119            }
120            
121            let error_report_team = '';
122            if(window.hWin?.HAPI4 && window.hWin.HAPI4?.sysinfo){
123                let admin_email =  window.hWin.HAPI4.sysinfo.sysadmin_email;
124                error_report_team = '<br><br>'+window.hWin.HR('Error_Report_Team').replace('#sysadmin_email#', admin_email)
125            }
126            
127            if(response.status==window.hWin.ResponseStatus.SYSTEM_FATAL
128            || response.status==window.hWin.ResponseStatus.SYSTEM_CONFIG
129            || response.status==window.hWin.ResponseStatus.NETWORK_ERROR){
130
131                let def_title = window.hWin.ResponseStatus.SYSTEM_FATAL ? 'Fatal error' :'System misconfiguration';
132                dlg_title = window.hWin.HEURIST4.util.isempty(dlg_title) ? def_title : dlg_title;
133                msg = msg + '<br><br>'+window.hWin.HR('Error_System_Config');
134
135            }else if(response.status==window.hWin.ResponseStatus.INVALID_REQUEST){
136
137                dlg_title = window.hWin.HEURIST4.util.isempty(dlg_title) ? 'Invalid request made' : dlg_title;
138
139                msg = msg + '<br><br>' + window.hWin.HR('Error_Wrong_Request') 
140                    +'<br><br>' + error_report_team;
141
142            }else if(response.status==window.hWin.ResponseStatus.REQUEST_DENIED){
143                
144                if(msg!='') msg = msg + '<br><br>';
145                
146                if(window.hWin && window.hWin.HAPI4){
147                    dlg_title = 'Login required to access '+window.hWin.HAPI4.database;  
148                    response.sysmsg = (window.hWin.HAPI4.currentUser['ugr_ID']==0)?0:1;
149                }else{
150                    dlg_title = 'Access denied';
151                }
152
153                if(msg=='' || (needlogin && response.sysmsg==0)){
154                    msg = msg + window.hWin.HR('Session expired');
155                    show_login_dlg = true;
156                }else if(response.sysmsg==0){
157                    msg = msg + 'You must be logged in';  
158                }else{ 
159                    dlg_title = 'Access denied';
160                    msg = msg + 'This action is not allowed for your current permissions';    
161                } 
162                
163            }else if(response.status==window.hWin.ResponseStatus.DB_ERROR){
164                dlg_title = window.hWin.HEURIST4.util.isempty(dlg_title) ? 'Database error' : dlg_title;
165                msg = msg + error_report_team;
166            }else  if(response.status==window.hWin.ResponseStatus.ACTION_BLOCKED){
167                // No enough rights or action is blocked by constraints
168                dlg_title = window.hWin.HEURIST4.util.isempty(dlg_title) ? 'Action blocked' : dlg_title;
169            }else  if(response.status==window.hWin.ResponseStatus.NOT_FOUND){
170                // The requested object not found.
171                dlg_title = window.hWin.HEURIST4.util.isempty(dlg_title) ? 'Request not found' : dlg_title;
172            }else if(response.status==window.hWin.ResponseStatus.UNKNOWN_ERROR){
173                // An unknown/un-handled error
174                dlg_title = window.hWin.HEURIST4.util.isempty(dlg_title) ? 'An unknown error has occurred' : dlg_title;
175                msg += error_report_team;
176            }
177            
178            if(request_code!=null){
179                msg = msg + '<br>'+window.hWin.HR('Error_Report_Code')+': "'
180                    +(request_code.script+' '
181                    +(window.hWin.HEURIST4.util.isempty(request_code.action)?'':request_code.action)).trim()+'"';
182            }
183        }
184        
185        if(window.hWin.HEURIST4.util.isempty(msg) || msg.trim().toLowerCase() == 'error'){
186            msg = window.hWin.HR('Error_Empty_Message');
187        }
188        if(window.hWin.HEURIST4.util.isempty(dlg_title)){
189            dlg_title = 'Error_Title';
190        }
191        dlg_title = window.hWin.HEURIST4.util.isFunction(window.hWin.HR)?window.hWin.HR(dlg_title):'Heurist';
192
193        let buttons = {};
194        buttons[window.hWin.HR('OK')]  = function() {
195                    let $dlg = window.hWin.HEURIST4.msg.getMsgDlg();            
196                    $dlg.dialog( "close" );
197                    if(show_login_dlg){
198                            if(window.hWin?.HAPI4){
199                                window.hWin.HAPI4.setCurrentUser(null);
200                                $(window.hWin.document).trigger(window.hWin.HAPI4.Event.ON_CREDENTIALS);
201                            }
202                    }
203                }; 
204                
205        if(!ext_options) ext_options = {};
206        //else if(!ext_options['default_palette_class']) 
207        ext_options['default_palette_class'] = 'ui-heurist-error';
208                
209        window.hWin.HEURIST4.msg.showMsgDlg(msg, buttons, dlg_title, ext_options);
210       
211        return msg; 
212    },
213
214    /**
215     * Loads content from a specified URL into a dialog and displays it.
216     * The dialog is created using `getPopupDlg` or `getMsgDlg` based on options.
217     * After loading, it calls `showMsgDlg` to configure and show the dialog.
218     *
219     * @memberof HEURIST4.msg
220     * @param {string} url - The URL from which to load content.
221     * @param {Object<string, function>|function} [buttons] - Buttons to display on the dialog, in the format expected by jQuery UI Dialog,
222     *                                                        or a callback function for default Yes/No buttons. See `showMsgDlg`.
223     * @param {string|Object<string, string>} [title] - The title for the dialog or an object with localized title/button labels. See `showMsgDlg`.
224     * @param {Object} [options] - Additional options for the dialog.
225     * @param {boolean} [options.isPopupDlg=false] - If true, uses `getPopupDlg`; otherwise, `getMsgDlg`.
226     * @param {string} [options.container] - If provided with `isPopupDlg`, specifies the container for `getPopupDlg`.
227     * @param {boolean} [options.use_doc_title=false] - If true, sets the dialog title from the `<title>` tag of the loaded document.
228     *                                                Other options are passed to `showMsgDlg`.
229     * @returns {jQuery|undefined} The jQuery object representing the dialog element, or undefined if URL is not provided.
230     */
231    showMsgDlgUrl: function(url, buttons, title, options){
232
233        let $dlg;
234        if(url){
235            let isPopupDlg = (options && (options.isPopupDlg || options.container));
236            $dlg = isPopupDlg
237                            ?window.hWin.HEURIST4.msg.getPopupDlg( options.container )
238                            :window.hWin.HEURIST4.msg.getMsgDlg();
239            $dlg.load(url, function(){
240                window.hWin.HEURIST4.msg.showMsgDlg(null, buttons, title, options);
241
242                if(options && options.use_doc_title){
243                    $dlg.dialog('option', 'title', $dlg.find('title').text());
244                }
245            });
246        }
247        return $dlg;
248    },
249    
250    /**
251     * Displays a standardized error message when a dynamic script loading fails.
252     * The message informs the user about a program error and advises them to report it.
253     *
254     * @memberof HEURIST4.msg
255     * @param {boolean} isdlg - If true, displays the message in a modal dialog (`showMsgDlg`).
256     *                          If false, displays it as a temporary flash message (`showMsgFlash`).
257     * @param {string} [message="this feature"] - A string to insert into the error message, indicating
258     *                                            what feature encountered the loading problem.
259     * @returns {void}
260     */
261    showMsg_ScriptFail: function( isdlg, message ){
262
263        if(window.hWin.HEURIST4.util.isempty(message)){
264            message = "this feature";
265        }
266
267        message = 
268'Unfortunately we have encountered a program error. Please report this to us using ' 
269+'create ticket under Help at the top right of the main screen, or via email '
270+'to [email protected] so that we can fix it immediately.' 
271
272+'<br><br>Please remember to tell us the context in which this occurred. '
273+'A screenshot including the URL is very useful.';
274        
275        if(isdlg){
276            window.hWin.HEURIST4.msg.showMsgDlg(message, null, "Work in Progress");    
277        }else{
278            window.hWin.HEURIST4.msg.showMsgFlash(message, 4000, {title:'Work in Progress',height:160});
279        }
280        
281    },
282
283    /**
284     * Displays a dialog prompting the user for input.
285     * The dialog contains a message and an input field. If the provided message
286     * does not include an element with ID `dlg-prompt-value`, an input field is automatically created.
287     *
288     * @memberof HEURIST4.msg
289     * @param {string} message - The message or HTML content to display in the prompt.
290     *                           If it contains an element with `id="dlg-prompt-value"`, that element is used for input.
291     *                           Otherwise, a new text input is created.
292     * @param {function} callbackFunc - A function to call when the user submits the dialog (e.g., clicks OK).
293     *                                  This function will be called with the value from the input field.
294     * @param {string} [sTitle="Specify value"] - The title for the prompt dialog.
295     * @param {Object} [ext_options] - Additional options passed to `showMsgDlg`.
296     * @param {string} [ext_options.dialogId="dialog-common-messages"] - The ID of the dialog element to use.
297     * @param {boolean} [ext_options.password=false] - If true, the created input field will be of type "password".
298     * @returns {jQuery} The jQuery object representing the dialog element.
299     */
300    showPrompt: function(message, callbackFunc, sTitle, ext_options){
301        
302        if(message.indexOf('dlg-prompt-value')<0){
303            const value = window.hWin.HEURIST4.util.htmlEscape(ext_options?.value??'');
304            
305            message = message+'<input id="dlg-prompt-value" class="text ui-corner-all" '
306                + (ext_options?.password?'type="password"':'') 
307                + ` style="max-width: 250px; min-width: 10em; width: 250px; margin-left:0.2em" value="${value}"/>`;    
308                
309        }
310
311        let dlg_id = ext_options?.dialogId??'dialog-common-messages';
312
313        return window.hWin.HEURIST4.msg.showMsgDlg( message,
314        function(){ // This is the OK button callback for showMsgDlg
315            if(window.hWin.HEURIST4.util.isFunction(callbackFunc)){
316                let $dlg = window.hWin.HEURIST4.msg.getMsgDlg(dlg_id);      
317                let ele = $dlg.find('#dlg-prompt-value');
318                let val = '';
319                if(ele.attr('type')!='checkbox' || ele.is(':checked')){ // Also handles checkbox type input
320                    val =  ele.val();
321                }
322                callbackFunc.call(this, val); // Call the user-provided callback with the value
323            }
324        },
325        window.hWin.HEURIST4.util.isempty(sTitle)?'Specify value':sTitle, ext_options);
326        
327    },
328    
329    /**
330     * Retrieves or creates the primary jQuery UI Dialog element used for common messages.
331     * If the dialog element with the specified ID (default "dialog-common-messages") does not exist,
332     * it is created and appended to the body.
333     *
334     * @memberof HEURIST4.msg
335     * @param {string} [dialogId="dialog-common-messages"] - The ID of the dialog element to get or create.
336     *                                                       If a leading '#' is present, it's stripped.
337     * @returns {jQuery} The jQuery object representing the dialog div.
338     */
339    getMsgDlg: function(dialogId){
340
341        if(window.hWin.HEURIST4.util.isempty(dialogId)){
342            dialogId = "dialog-common-messages";
343        }else if(dialogId[0] == '#'){
344            dialogId = dialogId.slice(0, 1);
345        }
346
347        let $dlg = $( "#" + dialogId );
348        if($dlg.length==0){
349            $dlg = $('<div>',{id: dialogId})
350                .css({'min-wdith':'380px','max-width':'640px'}) //,padding:'1.5em 1em'
351                .appendTo( $('body') );
352        }
353        return $dlg.removeClass('ui-heurist-border');
354    },
355
356    /**
357     * Retrieves or creates the jQuery UI Dialog element used for flash messages.
358     * If the dialog element with ID "dialog-flash-messages" does not exist,
359     * it is created and appended to the body.
360     *
361     * @memberof HEURIST4.msg
362     * @returns {jQuery} The jQuery object representing the flash message dialog div.
363     */
364    getMsgFlashDlg: function(){
365        let $dlg = $( "#dialog-flash-messages" );
366        if($dlg.length==0){
367            $dlg = $('<div>',{id:'dialog-flash-messages'}).css({'min-wdith':'380px','max-width':'640px'})
368                .appendTo('body'); //$(window.hWin.document)
369        }
370        return $dlg.removeClass('ui-heurist-border');
371    },
372    
373    /**
374     * Retrieves or creates a generic jQuery UI Dialog element, typically used for popups that might require more space
375     * or custom styling (e.g., no width limit by default).
376     * If the dialog element with the specified ID (default "dialog-popup") does not exist,
377     * it is created and appended to the body.
378     *
379     * @memberof HEURIST4.msg
380     * @param {string} [element_id="dialog-popup"] - The ID of the dialog element to get or create.
381     * @returns {jQuery} The jQuery object representing the popup dialog div.
382     */
383    getPopupDlg: function(element_id){        
384        if(!element_id) element_id = 'dialog-popup';
385        let $dlg = $( '#'+element_id );
386        if($dlg.length==0){
387            $dlg = $('<div>',{id:element_id})
388                .css({'padding':'2em','min-wdith':'380px',overflow:'hidden'}).appendTo('body');
389            $dlg.removeClass('ui-heurist-border');
390        }
391        return $dlg;
392    },
393
394    
395    /**
396     * Displays a jQuery UI tooltip associated with a given element for a specified duration.
397     * The tooltip content is taken from the `message` parameter.
398     *
399     * @memberof HEURIST4.msg
400     * @param {string} message - The message to display in the tooltip. This will be translated using `window.hWin.HR`.
401     * @param {number} [timeout=1000] - The duration in milliseconds for which the tooltip should be visible. Defaults to 1000ms.
402     * @param {jQuery|HTMLElement|Object} to_element - The target element for the tooltip.
403     *                                               Can be a jQuery object, DOM element, or an object specifying
404     *                                               `{of: targetElement, my: "...", at: "..."}` for custom positioning.
405     *                                               If not an object, default positioning is "left top" at "left bottom".
406     * @returns {void}
407     */
408    showTooltipFlash: function(message, timeout, to_element){
409        
410        if(!window.hWin.HEURIST4.util.isFunction(window.hWin.HR)){
411            alert(message); // Fallback if HR (translation) function is not available
412            return;
413        }
414        
415        if(window.hWin.HEURIST4.util.isempty(message) ||  window.hWin.HEURIST4.util.isnull(to_element)){
416            return;   
417        }
418        
419        let position;
420        
421        if($.isPlainObject(to_element)){ // Custom position provided
422                position = { my:to_element.my, at:to_element.at};
423                to_element =  to_element.of;
424        }else{ // Default position relative to the element
425                position = { my: "left top", at: "left bottom", of: $(to_element) };    
426        }
427
428        if (!(timeout>200)) {
429            timeout = 1000;
430        }
431        
432        $( to_element ).attr('title',window.hWin.HR(message)); // Set title attribute for jQuery UI Tooltip
433        $( to_element ).tooltip({
434            position: position,
435            // content: '<span>'+window.hWin.HR(message)+'</span>', // Alternative way to set content if needed
436            hide: { effect: "explode", duration: 500 } // Optional hide effect
437        });
438
439        $( to_element ).tooltip('open'); // Open the tooltip
440        
441        setTimeout(function(){ // Set timeout to close and clean up tooltip
442            $( to_element ).tooltip('close');
443            $( to_element ).attr('title',null); // Remove title to prevent native tooltip
444        }, timeout);
445        
446    },
447
448    /**
449     * Displays a buttonless dialog (flash message) that automatically closes after a specified timeout.
450     * The dialog's content is the provided message. Styling and positioning can be customized.
451     *
452     * @memberof HEURIST4.msg
453     * @param {string|null} message - The message to display. If null, the function returns early.
454     *                                This message will be translated using `window.hWin.HR`.
455     * @param {number|boolean}
455 [timeout=1000] - Duration in milliseconds before the dialog auto-closes.
456     *                                         If `false`, the dialog will not auto-close. Defaults to 1000ms.
457     * @param {Object|string} [options] - Configuration options for the dialog.
458     *                                    If a string, it's treated as the dialog title.
459     *                                    If an object, can include:
460     * @param {string} [options.title] - Title for the dialog. If null, title bar is hidden.
461     * @param {number} [options.height] - Height of the dialog. If not set, auto-calculated.
462     *                                    Other jQuery UI dialog options can also be included.
463     * @param {jQuery|HTMLElement|Object} [position_to_element] - Element or position object to position the dialog relative to.
464     *                                                          If an object, e.g., `{ my: "center", at: "center", of: window }`.
465     *                                                          Defaults to centering on the document.
466     * @returns {void}
467     */
468    showMsgFlash: function(message, timeout, options, position_to_element){
469
470        if(!window.hWin.HEURIST4.util.isFunction(window.hWin.HR)){
471            alert(message); // Fallback if HR (translation) function is not available
472            return;
473        }
474
475        if(!$.isPlainObject(options)){
476             options = {title:options};
477        }
478        
479        let $dlg = window.hWin.HEURIST4.msg.getMsgFlashDlg();
480
481        if(message==null){
482            return;
483        }
484
485        $dlg.empty();
486        let content = $('<span>'+window.hWin.HR(message)+'</span>')
487                    .css({'overflow':'hidden','font-weight':'bold','font-size':'1.2em'});
488                    
489        $dlg.append(content);
490        
491        let hideTitle = (options.title==null);
492        if(options.title){
493            options.title = window.hWin.HR(options.title);
494        }
495
496        $.extend(options, {
497            resizable: false,
498            width: 'auto',
499            modal: false,
500            //height: 80,
501            buttons: {}
502        });
503
504        if(position_to_element){
505           if($.isPlainObject(position_to_element)){
506                options.position = position_to_element.position;
507           }else{
508                options.position = { my: "left top", at: "left bottom", of: $(position_to_element) };    
509           } 
510        }else{
511            options.position = { my: "center center", at: "center center", of: $(document) };    
512        }
513        options.position.collision = 'none'; //FF fix
514
515        $dlg.dialog(options);
516        
517        if(!(options.height>0)){
518            let height = $(content).height()+90;
519           
520            $dlg.dialog({height:height});
521        }
522           
523        
524        content.position({ my: "center center", at: "center center", of: $dlg });
525        
526        if(hideTitle){
527            $dlg.parent().find('.ui-dialog-titlebar').hide();
528        }
529    
530        $dlg.parent().css({background: '#95A7B7', 'border-radius': '6px',   //#7092BE
531                    'outline-style':'none', outline:'none'})
532        $dlg.css({color:'white', border:'none !important', overflow:'hidden' });
533        
534        if(timeout!==false){
535
536            if (!(timeout>200)) {
537                timeout = 1000;
538            }
539            setTimeout(window.hWin.HEURIST4.msg.closeMsgFlash, timeout);
540            
541        }
542    },
543    
544    /**
545     * Closes the flash message dialog (`#dialog-flash-messages`).
546     * It also ensures the title bar is shown if it was previously hidden.
547     * This function respects a `coverallKeep` flag; if true, it won't close the dialog.
548     *
549     * @memberof HEURIST4.msg
550     * @returns {void}
551     */
552    closeMsgFlash: function(){
553        
554        if(window.hWin.HEURIST4.msg.coverallKeep===true) return;
555        
556        let $dlg = window.hWin.HEURIST4.msg.getMsgFlashDlg();
557        if($dlg.dialog('instance')) $dlg.dialog('close');
558        $dlg.parent().find('.ui-dialog-titlebar').show(); // Ensure title bar is visible for next time
559    },
560
561    /**
562     * Displays a simple error dialog with the given message and an "Error" title.
563     * This is a convenience wrapper around `showMsgDlg`.
564     *
565     * @memberof HEURIST4.msg
566     * @param {string} message - The error message to display.
567     * @returns {void}
568     * @todo The name `redirectToError` is misleading as it doesn't actually redirect. Consider renaming or clarifying its purpose.
569     */
570    redirectToError: function(message){
571        window.hWin.HEURIST4.msg.showMsgDlg(message, null, 'Error');
572    },
573
574    /**
575     * Checks if the length of an input field's value is within a specified range [min, max].
576     * If the length is outside the range, it displays a flash error message.
577     * 
578     * Uses `checkLength2` to get the error message text and add error class.
579     *
580     * @memberof HEURIST4.msg
581     * @param {jQuery} input - jQuery object representing the input element.
582     * @param {string} title - A title or name for the input field, used in the error message (e.g., "Username").
583     * @param {string|jQuery|null} message - If a jQuery object, its text is updated with the error message
584     *                                     and 'ui-state-error' class is added/removed (this part is commented out).
585     *                                     If a string, this custom error message is used for the flash display,
586     *                                     overriding the default generated by `checkLength2`.
587     *                                     If null or empty, `checkLength2`'s message is used.
588     * @param {number} min - The minimum allowed length for the input value.
589     * @param {number} max - The maximum allowed length for the input value. Use 0 or negative for no max limit.
590     * @returns {boolean} True if the length is valid, false otherwise.
591     */
592    checkLength: function( input, title, message, min, max ) {
593        let message_text = window.hWin.HEURIST4.msg.checkLength2( input, title, min, max );
594        if(message_text!=''){
595            // Use custom message string if provided
596            if(!window.hWin.HEURIST4.util.isempty(message) && typeof message === 'string') message_text = message; 
597             
598            window.hWin.HEURIST4.msg.showMsgFlash('<span style="padding:10px;border:0;">'+message_text+'</span>', 3000);
599            
600            return false;
601        }else{
602            return true;
603        }
604
605    },
606
607    /**
608     * Generates an error message if an input field's value length is outside a specified range.
609     * It also adds or removes the "ui-state-error" class from the input field based on validity.
610     *
611     * @memberof HEURIST4.msg
612     * @param {jQuery} input - jQuery object representing the input element.
613     * @param {string} title - A title or name for the input field, used in constructing the error message (e.g., "Username").
614     * @param {number} min - The minimum allowed length.
615     * @param {number} max - The maximum allowed length. If 0 or negative, no maximum is enforced by this part of the logic,
616     *                       but the message might still reflect "between min and max" if min > 1.
617     * @returns {string} An error message string if the length is invalid, or an empty string if valid.
618     */
619    checkLength2: function( input, title, min, max ) {
620
621        let len = input.val().length;
622        let message_text = '';
623
624        if ( (max>0 &&  len > max) || len < min ) {
625            input.addClass( "ui-state-error" );
626            if(max>0 && min>1){
627                message_text = window.hWin.HR(title)+" "+window.hWin.HR("length must be between ") +
628                min + " "+window.hWin.HR("and")+" " + max + ". ";
629                if(len>=min){ // This condition seems off if len > max, it will always be true.
630                              // The message should probably distinguish between too short and too long.
631                    message_text = message_text + (len-max) + window.hWin.HR(" characters over");
632                }
633
634            }else if(min>1){ // Only min length is specified (or max <= 0)
635               message_text = window.hWin.HR(title)+" "+window.hWin.HR('. At least '+min+' characters required'); 
636            }else if(min==1){ // Required field (min length 1)
637                message_text = window.hWin.HR(title)+" "+window.hWin.HR(" is required field");
638            }
639            // If only max is specified (min is 0 or 1, and it's too long), current logic doesn't explicitly state "too long".
640            // It might fall into the "length must be between" if min > 1, or no specific message if min <=1.
641
642            return message_text;
643
644        } else {
645            input.removeClass( "ui-state-error" );
646            return '';
647        }
648    },
649
650    
651    /**
652    * Displays a URL content within an iframe embedded in a jQuery UI popup dialog.
653    * This function is highly configurable through the `options` parameter.
654    * It handles creating the dialog, loading the iframe, and setting up communication
655    * (e.g., overriding `alert`, providing `close` and `doDialogResize` functions to the iframe content).
656    *
657    * @memberof HEURIST4.msg
658    * @param {string} url - The URL to load into the iframe. If empty, behavior might be undefined or rely on other options.
659    * @param {Object} [options] - Configuration options for the dialog and iframe.
660    * @param {string} [options.dialogid] - A unique ID for the dialog. If provided, the dialog div is reused/reopened
661    *                                      instead of being removed on close.
662    * @param {boolean} [options.force_reload=false] - If `dialogid` is used and the dialog already exists,
663    *                                                force iframe content to reload even if URL is the same.
664    * @param {jQuery} [options.container] - If provided, uses `showDialogInDiv` to render the dialog inline within this container.
665    * @param {string} [options.title=''] - Title for the dialog.
666    * @param {Window} [options.window=window] - The opener window object.
667    * @param {Object} [options.params] - Parameters to pass to the iframe's `assignParameters` function, if it exists.
668    * @param {function} [options.onpopupload] - Callback function executed after the iframe content has loaded.
669    *                                          Receives the iframe DOM element as an argument.
670    * @param {function} [options.callback] - Callback function executed when the iframe attempts to close (e.g., via `contentWindow.close()`).
671    *                                        If it returns `false`, the dialog closing is prevented.
672    * @param {function} [options.afterclose] - Callback function executed after the dialog has been closed.
673    * @param {boolean} [options.modal=true] - Whether the dialog should be modal.
674    * @param {function} [options.onmouseover] - Event listener for mouseover on the dialog title bar or iframe.
675    * @param {string} [options.default_palette_class] - A CSS class to apply to the dialog for theming.
676    * @param {string} [options.padding] - CSS padding for the dialog content area.
677    * @param {string} [options.padding-content] - CSS padding for the direct ui-dialog-content parent of the iframe.
678    * @param {boolean} [options.allowfullscreen=false] - If true, adds `allowfullscreen` attributes to the iframe.
679    * @param {boolean} [options.noClose=false] - If true, hides the close button on the dialog title bar.
680    * @param {boolean} [options.borderless=false] - If true, removes dialog border and title bar.
681    * @param {string|number} [options.width='640'] - Initial width of the dialog.
682    * @param {string|number} [options.height='480'] - Initial height of the dialog.
683    * @param {boolean} [options.resizable=true] - Whether the dialog is resizable.
684    * @param {boolean} [options.draggable=true] - Whether the dialog is draggable.
685    * @param {boolean} [options.closeOnEscape=true] - Whether to close the dialog on ESC key.
686    * @param {function} [options.beforeClose] - Callback function executed before the dialog closes.
687    * @param {function} [options.onOpen] - Callback function executed when the dialog opens.
688    * @param {string} [options.class] - Additional CSS class for the dialog main div.
689    * @param {boolean} [options.is_h6style=false] - If true, applies Heurist 6 specific styling and layout adjustments.
690    * @param {Object} [options.position] - jQuery UI position object for dialog placement.
691    * @param {boolean} [options.maximize=false] - If true (and not `is_h6style`), attempts to maximize the dialog.
692    * @param {string} [options.coverMsg] - Message to display on the loading coverall while iframe loads.
693    * @returns {void} (The function primarily has side effects by creating and showing a dialog).
694    *                 If `options.container` is provided, it effectively calls `showDialogInDiv` which also returns void.
695    *                 The created dialog jQuery object is not directly returned.
696    */
697    showDialog: function(url, options){
698
699        if(!options) options = {};
700
701        if(options.container){
702            window.hWin.HEURIST4.msg.showDialogInDiv(url, options);
703            return;
704        }
705
706
707        if(!options.title) options.title = ''; // removed 'Information'  which is not a particualrly useful title
708
709        let opener = options['window']?options['window'] :window;
710
711        //.appendTo( that.document.find('body') )
712        let $dlg = [];
713
714        if(options['dialogid']){
715            $dlg = $(opener.document).find('body #'+options['dialogid']);
716        }
717        
718        
719        let $dosframe;
720        
721        
722        function __canAccessIframe(iframe) {
723          try {
724            return Boolean(iframe.contentDocument);
725          }
726          catch(e){
727            return false;
728          }
729        }        
730
731        if($dlg.length>0){
732            //reassign dialog onclose and call new parameters
733            
734            $dlg.dialog('open');  
735            
736            $dosframe = $dlg.find('iframe');
737            if(__canAccessIframe($dosframe[0]))
738            {
739                let content = $dosframe[0].contentWindow;
740                
741                //close dialog from inside of frame - need redifine each time
742                content.close = function() {
743                    let rval = true;
744                    let closeCallback = options['callback'];
745                    if(window.hWin.HEURIST4.util.isFunction(closeCallback)){
746                        rval = closeCallback.apply(opener, arguments);
747                    }
748                    if ( rval===false ){ //!rval  &&  rval !== undefined){
749                        return false;
750                    }
751                    $dlg.dialog('close');
752                    return true;
753                };
754
755                // if content in iframe has function "assignParameters" we may pass parameters
756                if(options['params'] && window.hWin.HEURIST4.util.isFunction(content.assignParameters)) {
757                    content.assignParameters(options['params']);
758                }
759            }
760            if(options['height']>0 && options['width']>0){
761                 $dlg.dialog('option', 'width', options['width']);    
762                 $dlg.dialog('option', 'height', options['height']);    
763            }
764
765            if($dosframe.attr('src')!=url || options['force_reload']){ // hide previous content
766                $dosframe.hide();
767                $dlg.addClass('loading');
768            }
769
770        }else{
771
772           
773            $dlg = $('<div>')
774            .addClass('loading')
775            .appendTo( $(opener.document).find('body') );
776
777            if(options['dialogid']){
778                $dlg.attr('id', options['dialogid']);
779            }else{
780                $dlg.uniqueId();
781            }
782
783
784            if(options.class){
785                $dlg.addClass(options.class);
786            }
787
788            $dosframe = $( "<iframe>").attr('parent-dlg-id', $dlg.attr('id'))
789            .css({border:'none', overflow: 'none !important', width:'100% !important'}).appendTo( $dlg );
790            
791            if(options['allowfullscreen']){
792                $dosframe.attr('allowfullscreen',true);
793                $dosframe.attr('webkitallowfullscreen',true);
794                $dosframe.attr('mozallowfullscreen',true);
795                
796               
797            }
798
799            $dosframe.hide();
800            //callback function to resize dialog from internal frame functions
801            $dosframe[0].doDialogResize = function(width, height) {
802                
803                /*
804                let body = $(this.document).find('body');
805                let dim = { h: Math.max(400, body.innerHeight()-10), w:Math.max(400, body.innerWidth()-10) };
806
807                if(width>0)
808                $dlg.dialog('option','width', Math.min(dim.w, width));
809                if(height>0)
810                $dlg.dialog('option','height', Math.min(dim.h, height));
811                */    
812            };
813
814            //on load content event listener
815            $dosframe.on('load', function(){
816                if(window.hWin.HEURIST4.util.isempty($dosframe.attr('src'))){
817                    return;
818                }
819
820                window.hWin.HEURIST4.msg.sendCoverallToBack();
821
822                let has_access = __canAccessIframe($dosframe[0]);
823                
824                if(has_access)
825                {
826                    
827                    let content = $dosframe[0].contentWindow;
828                    try{
829                        //replace standard "alert" to Heurist dialog    
830                        content.alert = function(txt){
831                            let $dlg_alert = window.hWin.HEURIST4.msg.showMsgDlg(txt, null, ""); // Title was an unhelpful and inelegant "Info"
832                            $dlg_alert.dialog('open');
833                            return true;
834                        }
835                    }catch(e){
836                        console.error(e);
837                    }
838
839                    if(!options["title"]){
840                        $dlg.dialog( "option", "title", content.document.title );
841                    }  
842
843                    /*
844                    content.confirm = function(txt){
845                    var resConfirm = false,
846                    isClosed = false;
847
848                    var $confirm_dlg = window.hWin.HEURIST4.msg.showMsgDlg(txt, function(){
849                    resConfirm = true;
850                    }, "Confirm");
851
852                    $confirm_dlg.dialog('option','close',
853                    function(){
854                    isClosed = true;        
855                    });
856
857                    while(!isClosed){
858                    $.wait(1000);
859                    }
860
861                    return resConfirm;
862                    }*/
863
864                   
865                   
866                    //functions in internal document
867                    //content.close = $dosframe[0].close;    // make window.close() do what we expect
868
869                    //close dialog from inside of frame
870                    content.close = function() {
871                        let rval = true;
872                        let closeCallback = options['callback'];
873                        if(window.hWin.HEURIST4.util.isFunction(closeCallback)){
874                            rval = closeCallback.apply(opener, arguments);
875                        }
876                        if ( rval===false ){ //!rval  &&  rval !== undefined){
877                            return false;
878                        }
879                        $dlg.dialog('close');
880                        return true;
881                    };
882
883                   
884                    content.doDialogResize = $dosframe[0].doDialogResize;
885
886                }
887                
888                $dlg.removeClass('loading');
889                $dosframe.show();    
890
891                let onloadCallback = options['onpopupload'];
892                if(onloadCallback){
893                    onloadCallback.call(opener, $dosframe[0]);
894                }
895
896                if(has_access){
897                    let content = $dosframe[0].contentWindow;
898
899                    if(window.hWin.HEURIST4.util.isFunction(content.onFirstInit)) {  //see mapPreview
900                        content.onFirstInit();
901                    }
902                    //pass parameters to frame 
903                    if(options['params'] && window.hWin.HEURIST4.util.isFunction(content.assignParameters)) {
904                        content.assignParameters(options['params']);
905                    }
906                }
907                
908                if(options['onmouseover']){ 
909                    $dlg.parent().find('.ui-dialog-titlebar').on('mouseover', function(){
910                        options['onmouseover'].call();
911                    });
912                    $dosframe.on('mouseover', function(){
913                        options['onmouseover'].call();
914                    });
915                }
916                
917
918            });
919
920            options.width = window.hWin.HEURIST4.msg._setDialogDimension(options, 'width');
921            options.height = window.hWin.HEURIST4.msg._setDialogDimension(options, 'height');
922         
923            let opts = {
924                autoOpen: true,
925                width : options.width,
926                height: options.height,
927                modal: (options['modal']!==false),
928                resizable: (options.resizable!==false),
929                draggable: (options.draggable!==false),
930                title: options["title"],
931                resizeStop: function( event, ui ) {
932                    $dosframe.css('width','100%');
933                },
934                closeOnEscape: options.closeOnEscape,
935                beforeClose: options.beforeClose,
936                close: function(event, ui){
937                    let closeCallback = options['afterclose'];
938                    if(window.hWin.HEURIST4.util.isFunction(closeCallback)){
939                        closeCallback.apply();
940                    }
941                    if(!options['dialogid']){
942                        $dlg.remove();
943                    }
944                },
945                open: options.onOpen
946            };
947            $dlg.dialog(opts);
948            
949            if($dlg.attr('data-palette'))
950                $dlg.parent().removeClass($dlg.attr('data-palette'));
951            if(options.default_palette_class){
952                $dlg.attr('data-palette', options.default_palette_class);
953                $dlg.parent().addClass(options.default_palette_class);
954            }else{
955                $dlg.attr('data-palette', null);
956            }
957            
958            $dlg.parent().find(".ui-dialog-title").html(options["title"]);
959            $dlg.parent().find('.ui-dialog-content').css({'overflow':'hidden'});
960
961            if(options.noClose){
962                $dlg.parent().find('.ui-dialog-titlebar').find('.ui-icon-closethick').parent().hide();
963            }
964
965
966            if(!window.hWin.HEURIST4.util.isempty(options['padding'])){ //by default 2em
967                $dlg.css('padding', options.padding);
968            }
969            if(!window.hWin.HEURIST4.util.isempty(options['padding-content'])){ 
970                $dlg.parent().find('.ui-dialog-content').css('padding', options['padding-content']);
971            }
972            
973            if(!options.is_h6style && options.maximize){
974                        function __maximizeOneResize(){
975                            let dialog_height = window.innerHeight;
976                            $dlg.dialog( 'option', 'height', dialog_height);
977                            $dlg.dialog( 'option', 'width', '100%'); //dialog_width
978                        }
979                        __maximizeOneResize();
980            }     
981            if(options.borderless){
982                $dlg.css('padding',0);
983                $dlg.parent() //s(".ui-dialog")
984                      .css("border", "0 none")
985                      .find(".ui-dialog-titlebar").remove();
986            }
987
988        }
989
990
991        if(options.is_h6style)
992        {
993
994            $dlg.parent().addClass('ui-dialog-heurist ui-heurist-explore');
995
996            if(options.container){
997
998                $dlg.dialog( 'option', 'position', 
999                    { my: "left top", at: "left top", of:options.container, collision:'none'});
1000
1001                function __adjustOneResize(e){
1002                    let ele = e ?$(e.target) :options.container;
1003
1004                    let dialog_height = ele.height(); 
1005                    $dlg.dialog( 'option', 'height', dialog_height);
1006                    let dialog_width = ele.width(); 
1007                    $dlg.dialog( 'option', 'width', dialog_width);
1008                }
1009                //$(window).on('onresize',__adjustOneResize)
1010                options.container.off('resize');
1011                options.container.on('resize', __adjustOneResize);
1012                __adjustOneResize();
1013
1014            }else
1015                if(options.position){
1016                    $dlg.dialog( 'option', 'position', options.position );   
1017                    if(options.maximize){
1018                        function __maximizeOneResize(){
1019                            let dialog_height = window.innerHeight - $dlg.parent().position().top - 5;
1020                            $dlg.dialog( 'option', 'height', dialog_height);
1021                            let dialog_width = window.innerWidth - $dlg.parent().position().left - 5;
1022                            $dlg.dialog( 'option', 'width', dialog_width);
1023                        }
1024                        __maximizeOneResize();
1025                    }else{
1026                        if($dlg.parent().position().left<0){
1027                            $dlg.parent().css({left:0});
1028                        }else{
1029                            let max_width = window.innerWidth - $dlg.parent().position().left - 5;
1030                            let dlg_width = $dlg.dialog( 'option', 'width');
1031                            if(max_width<380 || $dlg.parent().position().left<0){
1032                                $dlg.parent().css({left:0});
1033                                $dlg.dialog( 'option', 'width', 380);    
1034                            }else if(dlg_width>max_width){
1035                                $dlg.dialog( 'option', 'width', max_width);    
1036                            }
1037                        }
1038                    }
1039                }
1040        }
1041
1042        //start content loading
1043        if(url!='' && ($dosframe.attr('src')!=url || options['force_reload'])){
1044            if(options.coverMsg){
1045                window.hWin.HEURIST4.msg.bringCoverallToFront($dlg, {'font-size': '16px', color: 'white'}, options.coverMsg); 
1046            }
1047            $dosframe.attr('src', url);
1048        }
1049
1050    },
1051
1052    /**
1053     * Displays URL content within an iframe embedded directly (inline) into a specified container element,
1054     * styled according to Heurist 6 UI guidelines. This is an alternative to popup dialogs.
1055     *
1056     * @memberof HEURIST4.msg
1057     * @param {string} url - The URL to load into the iframe.
1058     * @param {Object} [options] - Configuration options.
1059     * @param {jQuery} options.container - The jQuery object of the container element where the iframe will be placed.
1060     * @param {string} [options.title='&nbsp;'] - Title to display in a header bar above the iframe content.
1061     *                                            If empty, the header bar might be hidden or take less space.
1062     * @param {string} [options.context_help] - URL for a context-sensitive help page, linked via an info button in the header.
1063     * @param {boolean} [options.show_help_on_init=true] - If `context_help` is provided, whether to open the help panel immediately.
1064     * @param {Object} [options.params] - Parameters to pass to the iframe's `assignParameters` function.
1065     * @param {function} [options.doDialogResize] - A function to be made available to the iframe content for resizing itself.
1066     * @param {function} [options.onContentLoad] - Callback executed after iframe content loads. Receives iframe DOM element.
1067     * @param {function} [options.beforeClose] - Callback executed before the container/dialog is hidden or closed.
1068     *                                          If it returns `false`, closing is prevented.
1069     * @param {function} [options.afterClose] - Callback executed after the container/dialog is hidden or closed.
1070     * @param {string} [options.padding] - CSS padding for the main container.
1071     * @returns {jQuery|undefined} The jQuery object of the iframe if it's reused, otherwise undefined.
1072     *                             The function primarily modifies the DOM and sets up event handlers.
1073     */
1074    showDialogInDiv: function(url, options){
1075
1076            if(!options) options = {};
1077
1078            if(!options.title) options.title = '&nbsp;';
1079            
1080            let $container = options['container'];
1081            
1082            let $dosframe = $container.find('iframe');
1083            let _innerTitle = $container.children('.ui-heurist-header');
1084            let frame_container = $container.children('.ent_content_full');
1085            let $info_button;
1086               
1087            if($dosframe.length==0)
1088            {
1089                $container.empty();    
1090                //add h6 style header    
1091                _innerTitle = $('<div>').addClass('ui-heurist-header')
1092                        .appendTo($container);
1093
1094                frame_container = $('<div>').addClass('ent_content_full')
1095                        .css({'top':'37px','bottom':'1px', 'overflow':'hidden'})
1096                        .appendTo($container);
1097
1098                
1099                $dosframe = $( "<iframe>")
1100                            .css({height:'100%', width:'100%'})
1101                            .appendTo( frame_container );
1102            }else{
1103                if($dosframe.attr('src')==url){ //not changed
1104                    $container.show();
1105                    return $dosframe;        
1106                }
1107            }
1108
1109            _innerTitle.empty();
1110            if(options.title){
1111                _innerTitle.text(options['title']);
1112                _innerTitle.show();
1113                frame_container.css('top','37px');
1114            }else{
1115                _innerTitle.text('');
1116                _innerTitle.hide();
1117                frame_container.css('top','0px');
1118            }
1119            
1120            function __onDialogClose() {
1121                
1122                let canClose = true;
1123                if(window.hWin.HEURIST4.util.isFunction(options['beforeClose'])){
1124                    canClose = options['beforeClose'].call( $dosframe[0], arguments );
1125                }
1126                if(canClose===false){
1127                    return false;
1128                }else{
1129                    $container.hide();
1130                    if(window.hWin.HEURIST4.util.isFunction(options['afterClose'])){
1131                        canClose = options['afterClose'].call( $dosframe[0], arguments );
1132                    }
1133                    return true;
1134                }
1135            };
1136
1137            //init close button     
1138            $('<button>').button({icon:'ui-icon-closethick',showLabel:false, title:'Close'}) 
1139                                    //classes:'ui-corner-all ui-dialog-titlebar-close'})
1140                     .css({'position':'absolute', 'right':'4px', 'top':'6px', height:24, width:24})
1141                     .appendTo(_innerTitle)
1142                     .on({click:function(){
1143                         __onDialogClose();
1144                     }});
1145                     
1146            //init help button     
1147            if( options['context_help'] && window.hWin.HEURIST4.ui ){
1148                    
1149                    $info_button = $('<button>')
1150                            .button({icon:'ui-icon-circle-help', showLabel:false, label:'Help'})
1151                            .addClass('ui-helper-popup-button')
1152                            .css({'position':'absolute', 'right':'34px', 'top':'6px', height:24, width:24})
1153                            .appendTo(_innerTitle);
1154                    
1155                    window.hWin.HEURIST4.ui.initHelper({
1156                            button:$info_button, 
1157                            url: window.hWin.HRes(options['context_help']),
1158                            position:{my:'right top', at:'right top', of:$container},
1159                            container: $container,
1160                            is_open_at_once: options['show_help_on_init']===false ? false : true
1161                    });
1162            }else{
1163                //hide helper div
1164                frame_container.css({right:1});
1165                $container.children('.ui-helper-popup').hide();
1166            }
1167                
1168            if(navigator.userAgent.indexOf('Firefox')<0){            
1169                $dosframe.hide();
1170            }
1171            
1172            frame_container.addClass('loading');
1173            
1174            //callback function to resize dialog from internal iframe functions
1175           
1176
1177            $dosframe.off('load');
1178            //on load content event listener
1179            $dosframe.on('load', function(){
1180                         
1181                if(window.hWin.HEURIST4.util.isempty($dosframe.attr('src'))){
1182                    return;
1183                }
1184                    
1185                let content = $dosframe[0].contentWindow;
1186                
1187                //replace native alert 
1188                try{
1189                    content.alert = function(txt){
1190                        let $dlg_alert = window.hWin.HEURIST4.msg.showMsgDlg(txt, null, ""); // Title was an unhelpful and inelegant "Info"
1191                        $dlg_alert.dialog('open');
1192                        return true;
1193                }
1194                }catch(e){
1195                    console.error(e);
1196                }
1197                
1198                frame_container.removeClass('loading');
1199                $dosframe.show();    
1200                
1201                //close dialog from inside of frame
1202                content.close = __onDialogClose;
1203                
1204               
1205                if(window.hWin.HEURIST4.util.isFunction(options['doDialogResize'])){
1206                    content.doDialogResize = options['doDialogResize'];
1207                } 
1208                if(window.hWin.HEURIST4.util.isFunction(options['onContentLoad'])){
1209                    options['onContentLoad'].call(this, $dosframe[0]);
1210                } 
1211                       
1212                //pass params into iframe
1213                if(options['params'] && window.hWin.HEURIST4.util.isFunction(content.assignParameters)) {
1214                    content.assignParameters(options['params']);
1215                }
1216                
1217            }); //onload
1218
1219                        
1220            if(!window.hWin.HEURIST4.util.isempty(options['padding'])){
1221                //by default 2em
1222                $container.css('padding', options.padding);
1223            } 
1224                    
1225            //start content loading
1226            $dosframe.attr('src', url);
1227                        
1228           
1229    },   
1230    
1231    //
1232    //
1233    //
1234    _setDialogDimension: function(options, axis){
1235        
1236            let opener = options['window']?options['window'] :window;
1237        
1238            let wp = 0;
1239            
1240            if(axis=='width'){
1241                wp = (opener && opener.innerWidth>0)? opener.innerWidth
1242                    :(window.hWin?window.hWin.innerWidth:window.innerWidth);
1243            }else{
1244                wp = (opener && opener.innerHeight>0)? opener.innerHeight
1245                    :(window.hWin?window.hWin.innerHeight:window.innerHeight);
1246            }
1247            
1248            let res;
1249
1250            if(typeof options[axis]==='string'){
1251                let isPercent = (options[axis].indexOf('%')>0);
1252                
1253                res = parseInt(options[axis], 10);
1254                
1255                if(isPercent){
1256                    res = wp*res/100;
1257                }
1258            }else{
1259                res = options[axis];
1260            }
1261            
1262            if(isNaN(res) || res<100){
1263                res = (axis=='width')?640:480;
1264            } 
1265            
1266            if(res > wp){
1267                res = wp;
1268            }
1269            
1270            return res;
1271        
1272    },
1273    
1274    /**
1275     * Displays an existing DOM element within a jQuery UI popup dialog.
1276     * The element is temporarily detached from its original position in the DOM,
1277     * shown in the dialog, and then re-appended to its original parent when the dialog is closed.
1278     * The dialog itself is removed from the DOM after closing, unless `onCloseCalback` returns false.
1279     *
1280     * @memberof HEURIST4.msg
1281     * @param {Object} options - Configuration options for the dialog.
1282     * @param {HTMLElement} options.element - The DOM element to display in the dialog.
1283     * @param {Window} [options.window=window] - The window object where the dialog will be created.
1284     * @param {string|number} [options.width='640'] - Initial width of the dialog.
1285     * @param {string|number} [options.height='480'] - Initial height of the dialog.
1286     * @param {boolean} [options.autoOpen=true] - Whether to open the dialog immediately.
1287     * @param {boolean} [options.modal=true] - Whether the dialog should be modal.
1288     * @param {boolean} [options.resizable=true] - Whether the dialog is resizable.
1289     * @param {string} [options.title] - Title for the dialog.
1290     * @param {Object<string, function>} [options.buttons] - Buttons for the dialog.
1291     * @param {function} [options.open] - Callback executed when the dialog opens.
1292     * @param {function} [options.beforeClose] - Callback executed before the dialog closes.
1293     * @param {function} [options.close] - Callback executed after the dialog closes and the element is reattached.
1294     *                                     If this callback returns `false`, the dialog div is not removed from DOM.
1295     * @param {Object} [options.position] - jQuery UI position object for dialog placement.
1296     * @param {boolean} [options.borderless=false] - If true, removes dialog border and title bar.
1297     * @param {string} [options.default_palette_class] - A CSS class to apply to the dialog for theming.
1298     * @param {string} [options.h6style_class] - Additional class for Heurist 6 styling.
1299     * @returns {jQuery} The jQuery object representing the created dialog.
1300     */
1301    showElementAsDialog: function(options){
1302
1303            let opener = options['window']?options['window'] :window;
1304
1305            let $dlg = $('<div>')
1306               .appendTo( $(opener.document).find('body') );
1307
1308            let element = options['element'];
1309            let originalParentNode = element.parentNode;
1310            originalParentNode.removeChild(element);
1311
1312            $(element).show().appendTo($dlg);
1313
1314            let dimW = window.hWin.HEURIST4.msg._setDialogDimension(options, 'width');
1315            let dimH = window.hWin.HEURIST4.msg._setDialogDimension(options, 'height');
1316            
1317            let onCloseCalback = (options['close'])?options.close:null;
1318            
1319            let opts = {
1320                    autoOpen:(options['autoOpen']!==false),
1321                    width : dimW,
1322                    height: dimH,
1323                    modal: (options.modal!==false),
1324                    resizable: (options.resizable!==false),
1325                    //draggable: false,
1326                    title: options["title"],
1327                    buttons: options["buttons"],
1328                    open: options.open,  //callback
1329                    beforeClose: options.beforeClose,
1330                    close: function(event, ui){
1331
1332                        let need_remove = true;                        
1333                        if(window.hWin.HEURIST4.util.isFunction(onCloseCalback)){
1334                             need_remove = onCloseCalback.call(this, event, ui);
1335                        }
1336                        
1337                        if(need_remove!==false){
1338                            element.parentNode.removeChild(element);
1339                            element.style.display = "none";
1340                            originalParentNode.appendChild(element);
1341
1342                            $dlg.remove();
1343                        }
1344                    }
1345            };
1346            if(options["position"]) opts["position"] = options["position"];
1347            
1348            $dlg.dialog(opts);
1349
1350            if(options.borderless){
1351                $dlg.css('padding',0);
1352                $dlg.parent() //s(".ui-dialog")
1353                      .css("border", "0 none")
1354                      .find(".ui-dialog-titlebar").remove();
1355            }
1356            
1357            if(options.default_palette_class){
1358                $dlg.attr('data-palette', options.default_palette_class);
1359                $dlg.parent().addClass(options.default_palette_class);
1360            }else {
1361                $dlg.attr('data-palette', null);
1362                if (options.h6style_class){
1363                    $dlg.parent().addClass('ui-dialog-heurist '+options.h6style_class);
1364                }
1365            }
1366
1367            
1368            
1369            return $dlg;
1370    },
1371
1372    /**
1373     * Creates an HTML string for a styled alert message div.
1374     * The div includes an alert icon and the provided message, styled with "ui-state-error".
1375     *
1376     * @memberof HEURIST4.msg
1377     * @param {string} msg - The message content for the alert.
1378     * @returns {string} HTML string representing the alert div.
1379     */
1380    createAlertDiv: function(msg){
1381        
1382        return '<div class="ui-state-error" style="width:90%;margin:auto;margin-top:10px;padding:10px;">'+
1383                                '<span class="ui-icon ui-icon-alert" style="float: left; margin-right: .3em;"></span>'+
1384                                msg+'</div>';
1385    },
1386    
1387    /**
1388     * Displays a "coverall" overlay, typically used as a loading indicator that blocks UI interaction.
1389     * It creates or reuses a div element, styles it, sets a message, and appends it to a specified element or body.
1390     *
1391     * @memberof HEURIST4.msg
1392     * @param {jQuery|HTMLElement} [ele=jQuery('body')] - The element to append the coverall to. Defaults to document body.
1393     * @param {Object} [styles] - CSS styles to apply to the coverall div.
1394     *                            Defaults to an opacity, background color, text color, font size, and font weight.
1395     * @param {string} [message="Loading Content..."] - The message to display within the coverall.
1396     *                                                 Translated using `window.hWin.HR` if available.
1397     * @returns {void}
1398     */
1399    bringCoverallToFront: function(ele, styles, message) {
1400        if (!  window.hWin.HEURIST4.msg.coverall ) {
1401            window.hWin.HEURIST4.msg.coverall = 
1402                $('<div><div class="internal_msg" style="position: absolute;top: 30px;left: 30px;">'
1403                        +'</div></div>').addClass('coverall-div')
1404                .css({
1405                    'zIndex': 60000, // Higher zIndex to cover most elements
1406                    'font-size': '1.2em'
1407                });
1408        }else{
1409            window.hWin.HEURIST4.msg.coverall.detach(); // Detach if exists to re-append later, ensuring it's on top or in new context
1410        }
1411        
1412        if(!message){
1413            message = 'Loading Content';
1414            message = (window.hWin.HEURIST4.util.isFunction(window.hWin.HR)?window.hWin.HR(message):message)+'...';
1415        }    
1416        window.hWin.HEURIST4.msg.coverall.find('.internal_msg').html(message);
1417        
1418        let $appendTo = ele ? $(ele) : $('body');
1419        if($appendTo.find('.coverall-div').length==0) { // Append only if not already there (e.g. if ele is specific)
1420            window.hWin.HEURIST4.msg.coverall.appendTo($appendTo);
1421        }
1422        
1423        if(!styles){
1424            styles = {opacity: '0.6', 'background-color': "rgb(0, 0, 0)", color: 'rgb(0, 190, 0)', 'font-size': '20px', 'font-weight': 'bold'};
1425        }
1426        window.hWin.HEURIST4.msg.coverall.css( styles );
1427        
1428        $(window.hWin.HEURIST4.msg.coverall).show();
1429    },    
1430    
1431    /**
1432     * Hides the "coverall" overlay.
1433     * This function respects a `coverallKeep` flag; if true, it won't hide the overlay unless `force_close` is also true.
1434     *
1435     * @memberof HEURIST4.msg
1436     * @param {boolean} [force_close=false] - If true, forces the `coverallKeep` flag to false, ensuring the overlay is hidden.
1437     * @returns {void}
1438     */
1439    sendCoverallToBack: function(force_close) {
1440        if(force_close===true) window.hWin.HEURIST4.msg.coverallKeep = false;
1441        if(window.hWin.HEURIST4.msg.coverallKeep===true) return; // Don't hide if explicitly kept
1442        if(window.hWin.HEURIST4.msg.coverall) $(window.hWin.HEURIST4.msg.coverall).hide(); // Hide only if coverall exists
1443    },
1444  
1445
1446    /**
1447     * A simplified method to show a message dialog. It's a wrapper around `showMsgDlg`.
1448     * This function is intended for displaying a simple message with default "OK" button behavior.
1449     *
1450     * @memberof HEURIST4.msg
1451     * @param {string|jQuery|Object} message - The message content to display. Can be a string, HTML, or jQuery object.
1452     * @param {Object} [options] - Additional options passed directly to `showMsgDlg`.
1453     *                             These options can customize aspects like title, buttons (though typically not used here),
1454     *                             positioning, and styling. See `showMsgDlg` for more details on `ext_options`.
1455     * @returns {jQuery} The jQuery object representing the dialog element.
1456     */
1457    showMsg: function(message, options){
1458        return window.hWin.HEURIST4.msg.showMsgDlg(message, null, null, options )
1459    },
1460    
1461    /**
1462     * Displays a general-purpose jQuery UI dialog with a message and customizable buttons.
1463     * This is a core function for creating various types of dialogs (info, confirmation, error).
1464     *
1465     * @memberof HEURIST4.msg
1466     * @param {string|jQuery|Object|null} message - The content to display in the dialog.
1467     *                                            Can be a string (HTML is allowed), a jQuery object, or a DOM element.
1468     *                                            If null, the dialog is created/opened, but content must be set separately.
1469     * @param {Object<string, function>|function} [buttons] - Defines the buttons for the dialog.
1470     *                                                        - If an object: Keys are button labels (will be translated by `HR`),
1471     *                                                          values are click handler functions.
1472     *                                                        - If a function: Assumed to be a callback for a default "Yes"/"No" button set.
1473     *                                                          "Yes" executes the callback, "No" closes the dialog.
1474     *                                                        - If null/undefined: A default "OK" button is shown which closes the dialog.
1475     * @param {string|Object<string, string>} [labels] - Custom labels for title and buttons.
1476     *                                                 - If a string: Used as the dialog title (will be translated by `HR`).
1477     *                                                 - If an object: Can contain `title`, `yes`, `no`, `ok`, `cancel` properties
1478     *                                                   for localized strings (will be translated by `HR`).
1479     *                                                   Default title is "·" (was 'Info').
1480     * @param {Object} [ext_options] - Extended options for jQuery UI Dialog and custom behavior.
1481     * @param {string} [ext_options.default_palette_class] - CSS class for dialog theming.
1482     * @param {Object} [ext_options.position] - jQuery UI position object. Defaults to center of window.
1483     *                                          Can also be specified via `my`, `at`, `of` properties directly in `ext_options`.
1484     * @param {jQuery} [ext_options.container] - If provided, uses `getPopupDlg` with this container for dialog creation.
1485     * @param {boolean} [ext_options.isPopupDlg=false] - If true (or `container` is set), uses `getPopupDlg`.
1486     * @param {string} [ext_options.dialogId="dialog-common-messages"] - ID for the dialog element.
1487     * @param {boolean} [ext_options.removeOnClose=false] - If true, the dialog DOM element is removed when closed.
1488     * @param {boolean} [ext_options.hideTitle=false] - If true, hides the dialog title bar.
1489     * @param {number|string} [ext_options.width='auto'] - Dialog width. For non-popup, defaults to 'auto'. For popups, defaults to 705.
1490     * @param {number|string} [ext_options.height] - Dialog height. For popups, defaults to 515.
1491     * @param {boolean} [ext_options.resizable=false] - Whether the dialog is resizable. (Popup default: true).
1492     * @param {boolean} [ext_options.modal=true] - Whether the dialog is modal.
1493     * @returns {jQuery} The jQuery object representing the dialog element.
1494     */
1495    showMsgDlg: function(message, buttons, labels, ext_options){
1496
1497        if(!window.hWin.HEURIST4.util.isFunction(window.hWin.HR)){ // Fallback if HR (translation) function is not available
1498            alert(message);
1499            return;
1500        }
1501        
1502        if(!ext_options) ext_options = {};
1503        
1504        
1505        if(ext_options['buttons']){
1506            buttons = ext_options['buttons'];
1507            delete ext_options['buttons'];
1508        }
1509        if(ext_options['labels']){
1510            labels = ext_options['labels'];
1511            delete ext_options['labels'];
1512        }
1513        
1514        let isPopupDlg = (ext_options.isPopupDlg || ext_options.container);
1515        let dialogId = (!ext_options.dialogId) ? 'dialog-common-messages' : ext_options.dialogId;
1516
1517        let $dlg = isPopupDlg  //show popup in specified container
1518                    ?window.hWin.HEURIST4.msg.getPopupDlg(ext_options.container)
1519                    :window.hWin.HEURIST4.msg.getMsgDlg(dialogId);
1520
1521        if(message!=null){
1522            
1523            let isobj = (typeof message ===  "object");
1524
1525            if(!isobj){
1526                isPopupDlg = isPopupDlg || (message.indexOf('#')===0 && $(message).length>0);
1527            }
1528 
1529            if(isPopupDlg){
1530
1531                $dlg = window.hWin.HEURIST4.msg.getPopupDlg( ext_options.container );
1532                if(isobj){
1533                    $dlg.append(message);
1534                }else if(message.indexOf('#')===0 && $(message).length>0){
1535                    //it seems it is in Digital Harlem only
1536                    $dlg.html($(message).html());
1537                }else{
1538                    $dlg.html(message);
1539                }
1540
1541            }else{
1542                
1543                $dlg.empty();
1544                
1545                if(isobj){
1546                    $dlg.append(message);
1547                }else{
1548                    $dlg.append('<span>'+window.hWin.HR(message)+'</span>');
1549                }
1550            }
1551        }
1552
1553        let title = '·', // 'Info' removed - it's a useless popup window title, better to have none at all
1554            lblYes = window.hWin.HR('Yes'),
1555            lblNo =  window.hWin.HR('No'),
1556            lblOk = window.hWin.HR('OK'),
1557            lblCancel = window.hWin.HR('Cancel');
1558        
1559        if($.isPlainObject(labels)){
1560            if(labels.title)  title = labels.title;
1561            if(labels.yes)    lblYes = window.hWin.HR(labels.yes);
1562            if(labels.no)     lblNo = window.hWin.HR(labels.no);
1563            if(labels.ok)     lblOk = window.hWin.HR(labels.ok);
1564            if(labels.cancel) lblCancel = window.hWin.HR(labels.cancel);
1565        }else if (labels!=''){
1566            title = labels;
1567        }
1568        
1569        if (window.hWin.HEURIST4.util.isFunction(buttons)){ //}typeof buttons === "function"){
1570
1571            let callback = buttons;
1572
1573            buttons = {};
1574            buttons[lblYes] = function() {
1575                $dlg.dialog( "close" );
1576                callback.call();
1577            };
1578            buttons[lblNo] = function() {
1579                $dlg.dialog( "close" );
1580            };
1581        }else if(!buttons){    //buttons are not defined - the only one OK button
1582
1583            buttons = {};
1584            buttons[lblOk] = function() {
1585                $dlg.dialog( "close" );
1586            };
1587
1588        }
1589
1590        let options =  {
1591            title: window.hWin.HR(title),
1592            resizable: false,
1593            modal: true,
1594            closeOnEscape: true,
1595            buttons: buttons
1596            
1597        };
1598        
1599        if(ext_options){
1600
1601           if( $.isPlainObject(ext_options) ){
1602                $.extend(options, ext_options);
1603           }
1604           if(ext_options.my && ext_options.at && ext_options.of){
1605               options.position = {my:ext_options.my, at:ext_options.at, of:ext_options.of};
1606           }
1607           else if(!ext_options.options && !$.isPlainObject(ext_options)){  
1608                //it seems this is not in use
1609                let posele = $(ext_options);
1610                if(posele.length>0)
1611                    options.position = { my: "left top", at: "left bottom", of: $(ext_options) };
1612           }
1613           
1614           if(ext_options['removeOnClose']){
1615                options.close = function(event, ui){  $dlg.remove(); };   
1616           }
1617           
1618        }
1619        if(!options.position){
1620            options.position = { my: "center center", at: "center center", of: window };    
1621        }
1622        options.position.collision = 'none'; //FF fix
1623        
1624
1625        if(isPopupDlg){
1626
1627            if(!options.open){
1628                options.open = function(event, ui){
1629                    $dlg.scrollTop(0);
1630                };
1631            }
1632
1633            if(!options.height) options.height = 515;
1634            if(!options.width) options.width = 705;
1635            if(window.hWin.HEURIST4.util.isempty(options.resizable)) options.resizable = true;
1636            /* auto height dialog
1637            if(options.resizable === true){
1638            options.resizeStop = function( event, ui ) {
1639 
1640               var nh = $dlg.parent().height()
1641                            - $dlg.parent().find('.ui-dialog-titlebar').height() - $dlg.parent().find('.ui-dialog-buttonpane').height(); //-20
1642
1643                    $dlg.css({overflow: 'none !important','width':'100%', 'height':nh });
1644                };
1645            }*/
1646        }else if(!options.width){
1647            options.width = 'auto';
1648        }
1649
1650        
1651        $dlg.dialog(options);
1652        
1653        if(options.hideTitle){
1654            $dlg.parent().find('.ui-dialog-titlebar').hide();
1655        }else{
1656            $dlg.parent().find('.ui-dialog-titlebar').show();
1657        }
1658
1659        
1660        if($dlg.attr('data-palette'))
1661            $dlg.parent().removeClass($dlg.attr('data-palette'));
1662        if(ext_options.default_palette_class){
1663            $dlg.attr('data-palette', ext_options.default_palette_class);
1664            $dlg.parent().addClass(ext_options.default_palette_class);
1665        }else{
1666            $dlg.attr('data-palette', null);
1667        }
1668        
1669        if(options.enable_buttons_after>0){
1670            const btns = $dlg.parent().find('.ui-dialog-buttonset > button');
1671            window.hWin.HEURIST4.util.setDisabled(btns, true);
1672            setTimeout(()=>{ window.hWin.HEURIST4.util.setDisabled(btns, false); }, options.enable_buttons_after)
1673        }
1674        if(options.noClose){
1675            $dlg.parent().find('.ui-dialog-titlebar').find('.ui-icon-closethick').parent().hide();
1676        }
1677        
1678        return $dlg;
1679       
1680       
1681       
1682        //'#8ea9b9 none repeat scroll 0 0 !important'     none !important','background-color':'none !important
1683    },  
1684    
1685    // for progress message
1686    _progressInterval: 0, // Internal: Stores interval ID for progress polling
1687    _progressDiv: null, // Internal: Stores jQuery object of the progress display div
1688    _progressPopup: null, // Internal: Stores jQuery object of the progress popup dialog
1689    _progressWidgetEl: null,
1690    
1691    /**
1692     * Displays a progress indicator, either in a popup dialog or an existing container.
1693     * It periodically polls a progress URL to update the status, steps, and progress bar.
1694     *
1695     * @memberof HEURIST4.msg
1696     * @param {Object} options - Configuration options for the progress display.
1697     * @param {jQuery} [options.container] - If provided, the progress indicator is shown within this jQuery element.
1698     *                                       Otherwise, a popup dialog is created.
1699     * @param {string|false} [options.content] - Custom HTML content for the progress display area.
1700     *                                         - If a string: Used as the HTML content.
1701     *                                         - If `false`: The function uses the `options.container`'s existing content.
1702     *                                         - If undefined/null: Default content with steps and progress bar is generated.
1703     * @param {Array<string>} [options.steps] - An array of strings representing the steps of the progress,
1704     *                                          used if default content is generated.
1705     * @param {function} [options.onComplete] - Callback function to execute when the progress is terminated/completed.
1706     * @param {string|number} [options.session_id] - A unique session ID for the progress tracking.
1707     *                                             If not provided, a random one is generated.
1708     * @param {number} [options.interval=900] - The interval in milliseconds for polling the progress URL. Defaults to 900ms.
1709     * @param {number} [options.width=500] - Width of the popup dialog, if applicable.
1710     * @param {boolean} [options.hideTitle=true] - Whether to hide the title of the popup dialog, if applicable.
1711     * @returns {string|undefined} The session ID used for the progress tracking, or undefined if a previous progress
1712     *                             operation is still active (indicated by `_progressInterval > 0`).
1713     */
1714    showProgress: function( options ){
1715        options = options || {};  
1716
1717        let $el;
1718        if(options.container && options.container.length>0){ //container element
1719            options.showDialog = false;
1720            $el = $(options.container);
1721            //$el.css('border','2px solid red');
1722        }else{
1723            options.showDialog = true;
1724            $el = $('<div>');
1725        }   
1726     
1727        // if already has instance, destroy first to mimic old singleton behavior
1728        const inst = $el.progressReport('instance');
1729        if (inst) {
1730            $el.progressReport('destroy');
1731        }     
1732     
1733        const session_id = $el.progressReport(options).progressReport('start');
1734        
1735        this._progressWidgetEl = $el;
1736        
1737        return session_id;
1738    },
1739    
1740    hideProgress: function(instance){
1741
1742          // allow hideProgress() with no args to close the singleton (old callers)
1743          let $el = instance ? $(instance) : this._progressWidgetEl;
1744
1745          if ($el && $el.length && $el.progressReport('instance')) {
1746            $el.progressReport('destroy');
1747          }
1748          this._progressWidgetEl = null;
1749    },
1750     
1751    showProgress2: function( options ){
1752        if(window.hWin.HEURIST4.msg._progressInterval>0){
1753            console.log('previous progress is not completed'); // Log and exit if another progress is running
1754            return 0;
1755        }
1756        
1757        let $progress_div;
1758        let content = options?.content;
1759        let onComplete = options?.onComplete;
1760        let is_popup = true;
1761        
1762        if(!options){
1763            options = {};
1764        }        
1765
1766        if(options?.container){ //container element
1767            is_popup = false;
1768            $progress_div = options.container;
1769        }        
1770        
1771        if (window.hWin.HEURIST4.util.isempty(content)) {
1772            //default content
1773            content = '';
1774            
1775            if(Array.isArray(options?.steps)){
1776                
1777                content = '<ol type="1" style="font-size:12px;height:80%;padding-top:20px;" class="progress-steps">';
1778                
1779                options.steps.forEach((item)=>{
1780                    content += `<li style="color:gray">${item}</li>`;
1781                });
1782                
1783                content += '</ol>';
1784            }else{
1785                content = '<div class="loading" style="height:80%;min-height:150px"></div>';
1786            }
1787            
1788            content += '<div style="display:none;width:80%;height:40px;padding:5px;text-align:center;margin:auto;margin-top:10px">'
1789                +'<div id="progressbar"><div class="progress-label">Processing data...</div></div>'
1790                +'<div class="progress_stop" style="text-align:center;margin-top:4px">Abort</div>'
1791            +'</div>';
1792            
1793        }else if(typeof content !== 'string'){
1794            content = false;
1795        }
1796        
1797        if(is_popup){
1798            options['buttons'] = {}; //no buttons
1799            options['width'] = 500;
1800            options['hideTitle'] = true;
1801   
1802            window.hWin.HEURIST4.msg._progressPopup = window.hWin.HEURIST4.msg.showMsg( 
1803                    '<div class="progressbar_div" style="margin:10px"></div>', options );
1804        
1805            $progress_div = window.hWin.HEURIST4.msg._progressPopup.find('div.progressbar_div');
1806        }
1807        
1808        
1809        let progress_url = window.hWin.HAPI4.baseURL + "hserv/controller/progress.php";
1810        
1811        let session_id = options.session_id;
1812        if(!(session_id>0)) session_id = window.hWin.HEURIST4.util.random();
1813
1814        let t_interval = options.interval;
1815        if(!(t_interval>0)) t_interval = 900;
1816        
1817        window.hWin.HEURIST4.msg._progressDiv = $progress_div;
1818        $progress_div.show(); 
1819        document.body.style.cursor = 'progress';
1820        
1821        //add progress bar content
1822        if(content){
1823            $progress_div.html(content);
1824        }
1825        
1826
1827        //elements        
1828        let btn_stop = $progress_div.find('.progress_stop').button();
1829        
1830        // termination
1831        btn_stop.on({click:function(){
1832                let request = {terminate:1, t:(new Date()).getMilliseconds(), session:session_id};
1833                window.hWin.HEURIST4.util.sendRequest(progress_url, request, null, function(response){
1834                    window.hWin.HEURIST4.msg.hideProgress();
1835                });
1836            }});
1837    
1838        let progressSteps = $progress_div.find('.progress-steps');
1839        let div_loading = $progress_div.find('.loading').show();
1840        let pbar = $progress_div.find('#progressbar');
1841        let progressLabel = pbar.find('.progress-label').text('');
1842        pbar.progressbar({value:0});
1843
1844        let elapsed = 0;
1845        
1846        window.hWin.HEURIST4.msg._progressInterval = setInterval(function(){ 
1847            
1848            let request = {t:Date.now(), session:session_id};            
1849            
1850            window.hWin.HEURIST4.util.sendRequest(progress_url, request, null, function(response){
1851
1852                if(response?.status==window.hWin.ResponseStatus.UNKNOWN_ERROR){
1853                    window.hWin.HEURIST4.msg.hideProgress();
1854                }else if(response){
1855                    //it may return terminate,done,
1856                    
1857                    let resp = response.split(',');
1858                    if(response=='terminate' || !(resp.length>=2)){
1859                        //first or last response
1860                        if(response=='terminate'){
1861                            
1862                            if(onComplete){
1863                                onComplete.call();    
1864                            }
1865                            
1866                            window.hWin.HEURIST4.msg.hideProgress();
1867                        }else{
1868                            div_loading.show();    
1869                        }
1870                    }else{
1871                        div_loading.hide();
1872                        pbar.parent().show();
1873                        
1874                        if(resp.length==3 || resp.length==1){
1875                            let newStep = resp.shift();
1876                            let percentage = 0;
1877                            progressSteps.show();
1878
1879                            if(window.hWin.HEURIST4.util.isNumber(newStep)){
1880                                newStep = parseInt(newStep);
1881                                let all_li = progressSteps.find('li');
1882                                if(newStep>0){
1883                                    let arr = all_li.slice(0,newStep);
1884                                    arr.css('color','black');
1885                                    arr.find('span.processing').remove(); //remove rotation icon
1886                                }
1887                                if(newStep<all_li.length){
1888                                        if(percentage>0){
1889                                            if(percentage>100) percentage = 100;
1890                                            percentage = percentage+'%'; 
1891                                        }else{
1892                                            percentage = 'processing...'
1893                                        }
1894                                        let ele = $(all_li[newStep]).find('span.percentage');
1895                                        if(ele.length==0){
1896                                            percentage = '<span class="percentage">'+percentage+'</span>';
1897                                            $('<span class="processing"> <span class="ui-icon ui-icon-loading-status-balls"></span> '
1898                                                +percentage+'</span>')
1899                                                .appendTo( $(all_li[newStep]) );
1900                                            $(all_li[newStep]).css('color','black');
1901                                        }else{
1902                                            ele.text(percentage);
1903                                        }
1904                                }
1905                            }else{
1906                                let container = progressSteps.find('ol');
1907                                let li_ele = container.find('li:contains("'+newStep+'")');
1908                                if(li_ele.length==0){ //not added yet
1909                                    $('<li>'+newStep+'</li>').appendTo(container);    
1910                                }
1911                            }
1912                        }
1913
1914                        let cnt=0, total=0;
1915                        if(resp.length>1){
1916                            cnt = resp[0];
1917                            total = resp[1];
1918                        }
1919                        
1920                        if(cnt>0 && total>0){
1921                            const val = cnt*100/total;
1922                            pbar.progressbar( "value", val );
1923
1924                            elapsed += t_interval;
1925                            let est_remaining = (elapsed / resp[0]) * (resp[1] - resp[0]);
1926
1927                            if(est_remaining < 10000){ // less than 10 seconds
1928                                est_remaining = '10 seconds';
1929                            }else if(est_remaining < 60000){ // less than a minute
1930                                est_remaining = `${Math.ceil(est_remaining / 1000)} seconds`;
1931                            }else{
1932                                est_remaining = `${Math.ceil(est_remaining / 60000)} minutes`;
1933                            }
1934
1935                            progressLabel.text(`${resp[0]} of ${resp[1]}   ${(resp.length==3?resp[2]:'')} (approximately ${est_remaining} remaining)`);
1936                        }else{
1937                            progressLabel.text('preparing...');
1938                            pbar.progressbar( "value", 0 );
1939                        }
1940                    }
1941                    
1942                }
1943            },'text');
1944          
1945        
1946        }, t_interval);                
1947        
1948        return session_id;
1949    },
1950    
1951    /**
1952     * Hides the currently active progress indicator.
1953     * It clears the polling interval, hides the progress div, and closes the progress popup 
1953if it was used.
1954     * Also resets the mouse cursor from 'progress' to 'auto'.
1955     *
1956     * @memberof HEURIST4.msg
1957     * @returns {void}
1958     */
1959    hideProgress2: function(){
1960        $('body').css('cursor','auto');
1961
1962        if(window.hWin.HEURIST4.msg._progressInterval!=null){
1963            clearInterval(window.hWin.HEURIST4.msg._progressInterval);
1964            window.hWin.HEURIST4.msg._progressInterval = null;
1965        }
1966        if(window.hWin.HEURIST4.msg._progressDiv){
1967            window.hWin.HEURIST4.msg._progressDiv.hide();    
1968            window.hWin.HEURIST4.msg._progressDiv = null;
1969        }
1970        if(window.hWin.HEURIST4.msg._progressPopup){
1971            if (window.hWin.HEURIST4.msg._progressPopup.dialog('instance')) {
1972                 window.hWin.HEURIST4.msg._progressPopup.dialog( "close" );
1973            }
1974            window.hWin.HEURIST4.msg._progressPopup = null;
1975        }
1976        
1977    },
1978    
1979    
1980    /**
1981     * Displays a warning dialog prompting the user to save or ignore changes before exiting.
1982     * Typically used when there are unsaved modifications.
1983     *
1984     * @memberof HEURIST4.msg
1985     * @param {string} sMessage - The message content for the dialog (e.g., "You have unsaved changes...").
1986     *                            This message is translated using `window.hWin.HR`.
1987     *                            Note: The `sMessage` parameter is defined but not directly used in the current implementation;
1988     *                            instead, a hardcoded "Warn_Lost_Data" key is used for the message.
1989     * @param {function} onSave - Callback function to execute if the user chooses to "Save".
1990     * @param {function} onIgnore - Callback function to execute if the user chooses to "Ignore and close".
1991     * @returns {void}
1992     */
1993    showMsgOnExit: function(sMessage, onSave, onIgnore){ // sMessage seems unused
1994        let $dlg, buttons = {};
1995        buttons[window.hWin.HR('Save')] = function(){  // Using HR for button labels directly based on other patterns
1996            onSave();
1997            $dlg.dialog('close'); 
1998        }; 
1999        buttons[window.hWin.HR('Ignore and close')] = function(){
2000            onIgnore();
2001            $dlg.dialog('close'); 
2002        };
2003
2004        $dlg = window.hWin.HEURIST4.msg.showMsgDlg(
2005            window.hWin.HR('Warn_Lost_Data'), // sMessage is passed but 'Warn_Lost_Data' is used
2006            buttons,
2007            {title: window.hWin.HR('Confirm'),
2008               // yes: window.hWin.HR('Save'), // These are covered by direct button definition above
2009               // no: window.hWin.HR('Ignore and close')
2010            },
2011            {default_palette_class:'ui-heurist-design'});
2012        
2013    },
2014    
2015    /**
2016     * Checks if an error message string indicates the use of a disabled PHP function in a Smarty template (custom report).
2017     * If such an error is detected, it displays a warning dialog informing the user and suggesting they request
2018     * the function to be enabled by submitting a ticket.
2019     *
2020     * @memberof HEURIST4.msg
2021     * @param {string} txt - The error message string to check.
2022     * @returns {boolean} True if a warning dialog was shown (meaning the specific error was detected), false otherwise.
2023     */
2024    showWarningAboutDisabledFunction: function(txt){
2025      
2026            if(txt.indexOf('Exception on execution: Syntax error in template')==0 
2027            && txt.indexOf('not allowed by security setting')>0){
2028                
2029                    let buttons = null;
2030                    let $dlgm; // Define $dlgm here to be accessible in button callbacks
2031                    if(window.hWin.HAPI4 && window.hWin.HAPI4.actionHandler){ // Check HAPI4 exists
2032                        buttons = 
2033                        {
2034                            [window.hWin.HR('Send Bug Report')]: function() {
2035                                window.hWin.HAPI4.actionHandler.executeActionById('menu-help-bugreport');
2036                                if ($dlgm) $dlgm.dialog( 'close' );
2037                                },
2038                            [window.hWin.HR('Cancel')]:function() { // Translate button label
2039                                if ($dlgm) $dlgm.dialog( 'close' );
2040                            }
2041                        };
2042                    }
2043
2044                    $dlgm = window.hWin.HEURIST4.msg.showMsgDlg(
2045    '<p>Sorry, native php functions in custom reports are disabled by default<br>'
2046    +'as a security precaution. </p>'
2047    +'<p>Please use the Create ticket function to ask that this function be enabled. </p>',
2048                        buttons,
2049                        window.hWin.HR('Warning')); // Translate title
2050                        
2051                        return true;
2052                
2053            }
2054            
2055            return false;
2056        
2057    },
2058    
2059    /**
2060     * Prepares and optionally displays a detailed message about parent record pointers
2061     * that could not be automatically removed due to existing reverse child record pointers.
2062     * The message explains the situation and provides links to view the affected records.
2063     *
2064     * @memberof HEURIST4.msg
2065     * @param {Object} details - An object where keys are parent record IDs. Each value is an object:
2066     *                           `{title: string, type: string, restored: Array<Object>}`.
2067     *                           `restored` is an array of child info: `{field: string, id: string, title: string, type: string}`.
2068     * @param {boolean} [create_popup=false] - If true, displays the generated message in a dialog.
2069     *                                         If false, returns an object with message components.
2070     * @returns {jQuery|Object|string}
2071     *          - If `create_popup` is true: Returns the jQuery dialog object.
2072     *          - If `create_popup` is false and details are found: Returns an object
2073     *            `{message: string, title: string, handlers: Object}` for manual dialog creation.
2074     *          - If `create_popup` is false and no relevant details found: Returns an empty string.
2075     */
2076    prepareParentRecordMsg: function(details, create_popup = false){
2077
2078        let link_styling = "text-decoration: underline; cursor: pointer; color: black;";
2079
2080        let title = 'Parent record pointers which could not be removed';
2081        let msg = 'Not all parent record pointers could be deleted, because some have a child record pointer in the reverse direction.<br><br>'
2082                + 'To delete these parent record pointers you will need to delete the child record pointers in the affected parent records, or<br>'
2083                + 'change the Child Record Pointer field(s) in the parent record types to a standard Record pointer field (by editing the field in Structure Modification mode and unchecking the Child checkbox)<br><br>'
2084                + `Click <span style="${link_styling}" id="parent_list">here</span> for a list of two-way pointers which were not deleted.<br><br>`
2085                + `Click <span style="${link_styling}" id="parent_search">here</span> for a search result of all parent records containing such pointers.<br><br>`
2086                + 'Note: we do not delete the existing parent-child relations because of the risk that this unintentionally deletes data<br>'
2087                + 'that is still required (as simple record pointer connections). A manual process as above is much safer.'
2088
2089        let icon_bg = `${window.hWin.HAPI4.baseURL}hclient/assets/16x16.gif`;
2090
2091        let list = '<div>';
2092        for(const parent_ID in details){
2093
2094            const parent_title = details[parent_ID].title;
2095            const parent_type = details[parent_ID].type;
2096            let restored_children = details[parent_ID].restored;
2097
2098            if(window.hWin.HEURIST4.util.isempty(restored_children)){
2099                continue;
2100            }
2101
2102            list += '<div>'
2103                 +  `<img src="${icon_bg}" class="rt-icon" style="background-image:url('${window.hWin.HAPI4.iconBaseURL}${parent_type}');">`
2104                 + `<span style="${link_styling} vertical-align: 4px;" class="record_search_link" data-recid=="${parent_ID}">${parent_title}</span>`
2105                 + '<div style="margin: 5px 15px 10px;">';
2106
2107            for(const idx in restored_children){
2108
2109                let child = restored_children[idx];
2110
2111                list += `<em style="vertical-align: 4px;">${$Db.rst(parent_type, child.field, 'rst_DisplayName')}</em> <span style="vertical-align: 4px;">&rArr;</span> `
2112                     + `<img src="${icon_bg}" class="rt-icon" style="background-image:url('${window.hWin.HAPI4.iconBaseURL}${child.type}');">`
2113                     + `<span style="${link_styling} vertical-align: 4px;" class="record_search_link" data-recid=="${child.id}">${child.title}</span><br>`;
2114            }
2115
2116            list += '</div></div>';
2117        }
2118        if(list.length === 5){
2119            return '';
2120        }
2121        list += '</div>';
2122
2123        let handlers = {
2124            '#parent_list': () => {
2125
2126                let $dlg = window.hWin.HEURIST4.msg.showMsgDlg(list, null, {title: 'List of pointers not deleted'}, {default_palette_class: 'ui-heurist-explore', dialogId: 'retained-parents'});
2127
2128                $dlg.on('click', (event) => {
2129                    let recID = $(event.target).attr('data-recid');
2130                    let url = `${window.hWin.HAPI4.baseURL}viewers/record/renderRecordData.php?db=${window.hWin.HAPI4.database}&recID=${recID}`;
2131                    window.open(url, '_blank');
2132                })
2133            },
2134            '#parent_search': () => {
2135                let query = `ids:${Object.keys(details).join(',')}`;
2136                let url = `${window.hWin.HAPI4.baseURL}?db=${window.hWin.HAPI4.database}&q=${encodeURIComponent(query)}`;
2137                window.open(url, '_blank');
2138            }
2139        };
2140
2141        if(!create_popup){
2142            return {
2143                message: msg,
2144                title: title,
2145                handlers: handlers
2146            };
2147        }
2148
2149        let $dlg = window.hWin.HEURIST4.msg.showMsgDlg(msg, null, {title: title}, {default_palette_class: 'ui-heurist-populate', dialogId: 'restore-parent-records'});
2150
2151        $dlg.find('#parent_list').on('click', handlers['#parent_list']);
2152        $dlg.find('#parent_search').on('click', handlers['#parent_search']);
2153
2154        $dlg.find('.record_search_link').on('click', handlers['#parent_list.record_search_link']);
2155
2156        return $dlg;
2157    }
2158};

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.