feat: expose suffix and valedictory in user admin and esign (#6814)
[openemr.git] / library / js / utility.js
blobc7fcd9dd96bdc51f910b11983e924d6ec397816a
1 /**
2  * Javascript utility functions for openemr
3  *
4  * @package   OpenEMR
5  * @link      http://www.open-emr.org
6  * @author    Brady Miller <brady.g.miller@gmail.com>
7  * @author    Jerry Padgett <sjpadgett@gmail.com>
8  * @copyright Copyright (c) 2019 Brady Miller <brady.g.miller@gmail.com>
9  * @copyright Copyright (c) 2019-2021 Jerry Padgett <sjpadgett@gmail.com>
10  * @license   https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
11  */
12 /* We should really try to keep this library jQuery free ie javaScript only! */
14 // Translation function
15 // This calls the i18next.t function that has been set up in main.php
16 function xl(string) {
17     if (typeof top.i18next.t == 'function') {
18         return top.i18next.t(string);
19     } else {
20         // Unable to find the i18next.t function, so log error
21         console.log("xl function is unable to translate since can not find the i18next.t function");
22         return string;
23     }
26 // html escaping functions - special case when sending js string to html (see codebase for examples)
27 //   jsText (equivalent to text() )
28 //   jsAttr (equivalent to attr() )
29 // must be careful assigning const in this script. can't reinit a constant
30 if (typeof htmlEscapesText === 'undefined') {
31     const htmlEscapesText = {
32         '&': '&amp;',
33         '<': '&lt;',
34         '>': '&gt;'
35     };
36     const htmlEscapesAttr = {
37         '&': '&amp;',
38         '<': '&lt;',
39         '>': '&gt;',
40         '"': '&quot;',
41         "'": '&#x27;'
42     };
43     const htmlEscaperText = /[&<>]/g;
44     const htmlEscaperAttr = /[&<>"']/g;
45     jsText = function (string) {
46         return ('' + string).replace(htmlEscaperText, function (match) {
47             return htmlEscapesText[match];
48         });
49     };
50     jsAttr = function (string) {
51         return ('' + string).replace(htmlEscaperAttr, function (match) {
52             return htmlEscapesAttr[match];
53         });
54     };
57 // another useful function
58 async function syncFetchFile(fileUrl, type = 'text') {
59     let content = '';
60     let response = await fetch(fileUrl);
61     if (type == 'text') {
62         content = await response.text();
63     }
64     if (type == 'json') {
65         content = await response.json();
66     }
68     return content;
72 * function includeScript(srcUrl, type)
74 * @summary Dynamically include JS Scripts or Css.
76 * @param {string} url file location.
77 * @param {string} 'script' | 'link'.
79 * */
80 function includeScript(srcUrl, type) {
81     return new Promise(function (resolve, reject) {
82         if (type === 'script') {
83             let newScriptElement = document.createElement('script');
84             newScriptElement.src = srcUrl;
85             newScriptElement.onload = () => resolve(newScriptElement);
86             newScriptElement.onerror = () => reject(new Error(`Script load error for ${srcUrl}`));
88             document.head.append(newScriptElement);
89             console.log('Needed to load:[' + srcUrl + '] For: [' + location + ']');
90         }
91         if (type === "link") {
92             let newScriptElement = document.createElement("link")
93             newScriptElement.type = "text/css";
94             newScriptElement.rel = "stylesheet";
95             newScriptElement.href = srcUrl;
96             newScriptElement.onload = () => resolve(newScriptElement);
97             newScriptElement.onerror = () => reject(new Error(`Link load error for ${srcUrl}`));
99             document.head.append(newScriptElement);
100             console.log('Needed to load:[' + srcUrl + '] For: [' + location + ']');
101         }
102     });
106 * @function initDragResize(dragContext, resizeContext)
107 * @summary call this function from scripts you may want to provide a different
108 *  context other than the page context of this utility
110 * @param {object} context of element to apply drag.
111 * @param {object} optional context of element. document is default.
113 function initDragResize(dragContext, resizeContext = document) {
114     let isLoaded = typeof window.interact;
115     if (isLoaded !== 'function') {
116         (async (utilfn) => {
117             await includeScript(utilfn, 'script');
118         })(top.webroot_url + '/public/assets/interactjs/dist/interact.js').then(() => {
119             initInteractors(dragContext, resizeContext);
120         });
121     } else {
122         initInteractors(dragContext, resizeContext);
123     }
126 function setInteractorPosition(x, y, target) {
127     if ('webkitTransform' in target.style || 'transform' in target.style) {
128         target.style.webkitTransform =
129             target.style.transform =
130                 'translate(' + x + 'px, ' + y + 'px)';
131     } else {
132         target.style.left = x + 'px';
133         target.style.top = y + 'px';
134     }
136     target.setAttribute('data-x', x);
137     target.setAttribute('data-y', y);
140 /* function to init all page drag/resize elements. */
141 function initInteractors(dragContext = document, resizeContext = '') {
142     resizeContext = resizeContext ? resizeContext : dragContext;
144     function dragMoveListener(event) {
145         let target = event.target;
146         let x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx;
147         let y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy;
149         setInteractorPosition(x, y, target);
150     }
152     /* Draggable */
153     // reset
154     interact(".drag-action", {context: dragContext}).unset();
155     // init
156     interact(".drag-action", {context: dragContext}).draggable({
157         enabled: true,
158         inertia: true,
159         modifiers: [
160             interact.modifiers.snap({
161                 targets: [
162                     interact.createSnapGrid({x: 30, y: 30})
163                 ],
164                 range: Infinity,
165                 relativePoints: [{x: 0, y: 0}]
166             }),
167             interact.modifiers.restrict({
168                 restriction: "parent",
169                 elementRect: {top: 0, left: 0, bottom: 1, right: 1},
170                 endOnly: true
171             })
172         ],
173         autoScroll: false,
174         maxPerElement: 2
175     }).on('dragstart', function (event) {
176         event.preventDefault();
177     }).on('dragmove', dragMoveListener);
179     /* Resizable */
180     interact(".resize-action", {context: resizeContext}).unset();
182     interact(".resize-action", {context: resizeContext}).resizable({
183         enabled: true,
184         preserveAspectRatio: false,
185         edges: {
186             left: '.resize-s',
187             right: true,
188             bottom: true,
189             top: '.resize-s'
190         },
191         inertia: {
192             resistance: 30,
193             minSpeed: 100,
194             endSpeed: 50
195         },
196         snap: {
197             targets: [
198                 interact.createSnapGrid({
199                     x: 5, y: 5
200                 })
201             ],
202             range: Infinity,
203             relativePoints: [{x: 0, y: 0}]
204         },
205     }).on('resizestart', function (event) {
206         event.preventDefault();
207     }).on('resizemove', function (event) {
208         let target = event.target;
209         let x = (parseFloat(target.getAttribute('data-x')) || 0);
210         let y = (parseFloat(target.getAttribute('data-y')) || 0);
212         target.style.width = event.rect.width + 'px';
213         target.style.height = event.rect.height + 'px';
214         x += event.deltaRect.left;
215         y += event.deltaRect.top;
217         // TODO: @adunsulag not sure why this only does webkitTransform, seems like it should do the same
218         // as our other move here: setInteractorPosition(x, y, target);
219         target.style.webkitTransform = target.style.transform = 'translate(' + x + 'px,' + y + 'px)';
220         target.setAttribute('data-x', x);
221         target.setAttribute('data-y', y);
222     });
227 * @function oeSortable(callBackFn)
228 * @summary call this function from scripts you may need to use sortable
230 * @param function A callback function which is called with the sorted elements as parameter
232 function oeSortable(callBackFn) {
233     if (typeof window.interact !== 'function') {
234         (async (interactfn) => {
235             await includeScript(interactfn, 'script');
236         })(top.webroot_url + '/public/assets/interactjs/dist/interact.js').then(() => {
237             load();
238         });
239     } else {
240         load();
241     }
243     function clearTranslate(elem) {
244         elem.style.webkitTransform =
245             elem.style.transform =
246                 'translate(' + 0 + 'px, ' + 0 + 'px)'
247         elem.setAttribute('data-x', 0)
248         elem.setAttribute('data-y', 0)
249     }
251     function switchElem(elem1, elem2, clear = false) {
252         $(elem2).append($(elem1).children()[0]);
253         $(elem1).append($(elem2).children()[0]);
254         if (clear) {
255             clearTranslate($(elem2).children()[0]);
256             clearTranslate($(elem1).children()[0]);
257         }
258     }
260     function moveUp(elem) {
261         if (elem) {
262             let prevElem = $(elem).prev(".droppable");
263             if (prevElem.length > 0) {
264                 let childIsDragging = prevElem.children("li.is-dragging")[0];
265                 if (childIsDragging) {
266                     switchElem(elem, prevElem[0], true);
267                     return true;
268                 } else {
269                     if (prevElem[0]) {
270                         if (moveUp(prevElem[0])) {
271                             switchElem(elem, prevElem[0]);
272                         }
273                     }
274                 }
275             }
276         }
277         return false;
278     }
280     function moveDown(elem) {
281         if (elem) {
282             let nxtElem = $(elem).next(".droppable");
283             if (nxtElem.length > 0) {
284                 let childIsDragging = nxtElem.children("li.is-dragging")[0];
285                 if (childIsDragging) {
286                     switchElem(elem, nxtElem[0], true);
287                     return true;
288                 } else {
289                     if (nxtElem[0]) {
290                         if (moveDown(nxtElem[0])) {
291                             switchElem(elem, nxtElem[0]);
292                         }
293                     }
294                 }
295             }
296         }
297         return false;
298     }
300     function dragMoveListener(event) {
301         var target = event.target
302         var x = (parseFloat(target.getAttribute('data-x')) || 0) + event.dx
303         var y = (parseFloat(target.getAttribute('data-y')) || 0) + event.dy
304         target.style.webkitTransform =
305             target.style.transform =
306                 'translate(' + x + 'px, ' + y + 'px)'
307         target.setAttribute('data-x', x)
308         target.setAttribute('data-y', y)
309     }
311     function load() {
312         interact('.droppable').dropzone({
313             accept: null,
314             overlap: 0.9,
315             ondropactivate: function (event) {
316                 event.relatedTarget.classList.add('is-dragging');
317             },
318             ondragenter: function (event) {
319                 let isUpper = moveUp(event.target);
320                 if (!isUpper) {
321                     moveDown(event.target);
322                 }
323             },
324             ondropdeactivate: function (event) {
325                 if (event.target.firstChild.classList.contains('is-dragging')) {
326                     let items = event.target.parentNode.children;
327                     event.relatedTarget.classList.remove('is-dragging');
328                     clearTranslate(event.relatedTarget);
329                     callBackFn && callBackFn(items);
330                 }
331             }
332         })
334         interact('.draggable').draggable({
335             inertia: true,
336             modifiers: [
337                 interact.modifiers.restrictRect({
338                     restriction: null,
339                     endOnly: true
340                 })
341             ],
342             autoScroll: true,
343             listeners: {move: dragMoveListener}
344         })
345     }
350 * Universal async BS alert message with promise
351 * Note the use of new javaScript translate function xl().
354 if (typeof asyncAlertMsg !== "function") {
355     /* eslint-disable-next-line no-inner-declarations */
356     function asyncAlertMsg(message, timer = 5000, type = 'danger', size = '') {
357         let alertMsg = xl("Alert Notice");
358         $('#alert_box').remove();
359         size = (size == 'lg') ? 'left:25%;width:50%;' : 'left:35%;width:30%;';
360         let style = "position:fixed;top:25%;" + size + " bottom:0;z-index:9999;";
361         $("body").prepend("<div class='container text-center' id='alert_box' style='" + style + "'></div>");
362         let mHtml = '<div id="alertmsg" class="alert alert-' + type + ' alert-dismissable">' +
363             '<button type="button" class="close btn btn-link btn-cancel" data-dismiss="alert" aria-hidden="true"></button>' +
364             '<h5 class="alert-heading text-center">' + alertMsg + '</h5><hr>' +
365             '<p>' + message + '</p>' +
366             '</div>';
367         $('#alert_box').append(mHtml);
368         return new Promise(resolve => {
369             $('#alertmsg').on('closed.bs.alert', function () {
370                 clearTimeout(AlertMsg);
371                 $('#alert_box').remove();
372                 resolve('closed');
373             });
374             let AlertMsg = setTimeout(function () {
375                 $('#alertmsg').fadeOut(800, function () {
376                     $('#alert_box').remove();
377                     resolve('timedout');
378                 });
379             }, timer);
380         })
381     }
385 * function syncAlertMsg(()
387 * Universal sync BS alert message returns promise after resolve.
388 * Call below to return a promise after alert is resolved.
389 * Example: syncAlertMsg('Hello, longtime, 'success', 'lg').then( asyncRtn => ( ... log something });
391 * Or use as IIFE to run inline.
392 * Example:
393 *   (async (time) => {
394 *       await asyncAlertMsg('Waiting till x'ed out or timeout!', time); ...now go;
395 *   })(3000).then(rtn => { ... but then could be more });
397 * */
398 async function syncAlertMsg(message, timer = 5000, type = 'danger', size = '') {
399     return await asyncAlertMsg(message, timer, type, size);
402 /* Handy function to set values in globals user_settings table */
403 if (typeof persistUserOption !== "function") {
404     const persistUserOption = function (option, value) {
405         return $.ajax({
406             url: top.webroot_url + "/library/ajax/user_settings.php",
407             type: 'post',
408             contentType: 'application/x-www-form-urlencoded',
409             data: {
410                 csrf_token_form: top.csrf_token_js,
411                 target: option,
412                 setting: value
413             },
414             beforeSend: function () {
415                 top.restoreSession();
416             },
417             error: function (jqxhr, status, errorThrown) {
418                 console.log(errorThrown);
419             }
420         });
421     };
425  * User Debugging Javascript Errors
426  * Turn on/off in Globals->Logging
428  * @package   OpenEMR Utilities
429  * @link      http://www.open-emr.org
430  * @author    Jerry Padgett <sjpadgett@gmail.com>
431  */
433 if (typeof top.userDebug !== 'undefined' && (top.userDebug === '1' || top.userDebug === '3')) {
434     window.onerror = function (msg, url, lineNo, columnNo, error) {
435         const is_chrome = navigator.userAgent.toLowerCase().indexOf('chrome') > -1;
436         const is_firefox = navigator.userAgent.indexOf('Firefox') > -1;
437         const is_safari = navigator.userAgent.indexOf("Safari") > -1;
439         var showDebugAlert = function (message) {
440             let errorMsg = [
441                 'URL: ' + message.URL,
442                 'Line: ' + message.Line + ' Column: ' + message.Column,
443                 'Error object: ' + JSON.stringify(message.Error)
444             ].join("\n");
446             let msg = message.Message + "\n" + errorMsg;
447             console.error(xl('User Debug Error Catch'), message);
448             alert(msg);
450             return false;
451         };
452         try {
453             let string = msg.toLowerCase();
454             let substring = xl("script error"); // translate to catch for language of browser.
455             if (string.indexOf(substring) > -1) {
456                 let xlated = xl('Script Error: See Browser Console for Detail');
457                 showDebugAlert(xlated);
458             } else {
459                 let message = {
460                     Message: msg,
461                     URL: url,
462                     Line: lineNo,
463                     Column: columnNo,
464                     Error: JSON.stringify(error)
465                 };
467                 showDebugAlert(message);
468             }
469         } catch (e) {
470             let xlated = xl('Unknown Script Error: See Browser Console for Detail');
471             showDebugAlert(xlated);
472         }
474         return false;
475     };
478 (function(window, oeSMART) {
479     oeSMART.initLaunch = function(webroot, csrfToken) {
480         // allows this to be lazy defined
481         let xl = window.top.xl || function(text) { return text; };
482         let smartLaunchers = document.querySelectorAll('.smart-launch-btn');
483         for (let launch of smartLaunchers) {
484                 launch.addEventListener('click', function (evt) {
485                     let node = evt.target;
486                     let intent = node.dataset.intent;
487                     let clientId = node.dataset.clientId;
488                     if (!intent || !clientId) {
489                         console.error("mising intent parameter or client-id parameter");
490                         return;
491                     }
493                     let url = webroot + '/interface/smart/ehr-launch-client.php?intent='
494                         + encodeURIComponent(intent) + '&client_id=' + encodeURIComponent(clientId)
495                         + "&csrf_token=" + encodeURIComponent(csrfToken);
496                     let title = node.dataset.smartName || JSON.stringify(xl("Smart App"));
497                     // we allow external dialog's  here because that is what a SMART app is
498                     let height = window.top.innerHeight; // do our full height here
499                     dlgopen(url, '_blank', 'modal-full', height, '', title, {allowExternal: true});
500                 });
501         }
502     };
503     window.oeSMART = oeSMART;
504 })(window, window.top.oeSMART || {});