2 * Javascript utility functions for 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
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
17 if (typeof top.i18next.t == 'function') {
18 return top.i18next.t(string);
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");
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 = {
36 const htmlEscapesAttr = {
43 const htmlEscaperText = /[&<>]/g;
44 const htmlEscaperAttr = /[&<>"']/g;
45 jsText = function (string) {
46 return ('' + string).replace(htmlEscaperText, function (match) {
47 return htmlEscapesText[match];
50 jsAttr = function (string) {
51 return ('' + string).replace(htmlEscaperAttr, function (match) {
52 return htmlEscapesAttr[match];
57 // another useful function
58 async function syncFetchFile(fileUrl, type = 'text') {
60 let response = await fetch(fileUrl);
62 content = await response.text();
65 content = await response.json();
72 * function includeScript(srcUrl, type)
74 * @summary Dynamically include JS Scripts or Css.
76 * @param {string} url file location.
77 * @param {string} 'script' | 'link'.
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 + ']');
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 + ']');
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') {
117 await includeScript(utilfn, 'script');
118 })(top.webroot_url + '/public/assets/interactjs/dist/interact.js').then(() => {
119 initInteractors(dragContext, resizeContext);
122 initInteractors(dragContext, resizeContext);
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)';
132 target.style.left = x + 'px';
133 target.style.top = y + 'px';
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);
154 interact(".drag-action", {context: dragContext}).unset();
156 interact(".drag-action", {context: dragContext}).draggable({
160 interact.modifiers.snap({
162 interact.createSnapGrid({x: 30, y: 30})
165 relativePoints: [{x: 0, y: 0}]
167 interact.modifiers.restrict({
168 restriction: "parent",
169 elementRect: {top: 0, left: 0, bottom: 1, right: 1},
175 }).on('dragstart', function (event) {
176 event.preventDefault();
177 }).on('dragmove', dragMoveListener);
180 interact(".resize-action", {context: resizeContext}).unset();
182 interact(".resize-action", {context: resizeContext}).resizable({
184 preserveAspectRatio: false,
198 interact.createSnapGrid({
203 relativePoints: [{x: 0, y: 0}]
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);
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(() => {
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)
251 function switchElem(elem1, elem2, clear = false) {
252 $(elem2).append($(elem1).children()[0]);
253 $(elem1).append($(elem2).children()[0]);
255 clearTranslate($(elem2).children()[0]);
256 clearTranslate($(elem1).children()[0]);
260 function moveUp(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);
270 if (moveUp(prevElem[0])) {
271 switchElem(elem, prevElem[0]);
280 function moveDown(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);
290 if (moveDown(nxtElem[0])) {
291 switchElem(elem, nxtElem[0]);
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)
312 interact('.droppable').dropzone({
315 ondropactivate: function (event) {
316 event.relatedTarget.classList.add('is-dragging');
318 ondragenter: function (event) {
319 let isUpper = moveUp(event.target);
321 moveDown(event.target);
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);
334 interact('.draggable').draggable({
337 interact.modifiers.restrictRect({
343 listeners: {move: dragMoveListener}
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>' +
367 $('#alert_box').append(mHtml);
368 return new Promise(resolve => {
369 $('#alertmsg').on('closed.bs.alert', function () {
370 clearTimeout(AlertMsg);
371 $('#alert_box').remove();
374 let AlertMsg = setTimeout(function () {
375 $('#alertmsg').fadeOut(800, function () {
376 $('#alert_box').remove();
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.
394 * await asyncAlertMsg('Waiting till x'ed out or timeout!', time); ...now go;
395 * })(3000).then(rtn => { ... but then could be more });
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) {
406 url: top.webroot_url + "/library/ajax/user_settings.php",
408 contentType: 'application/x-www-form-urlencoded',
410 csrf_token_form: top.csrf_token_js,
414 beforeSend: function () {
415 top.restoreSession();
417 error: function (jqxhr, status, errorThrown) {
418 console.log(errorThrown);
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>
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) {
441 'URL: ' + message.URL,
442 'Line: ' + message.Line + ' Column: ' + message.Column,
443 'Error object: ' + JSON.stringify(message.Error)
446 let msg = message.Message + "\n" + errorMsg;
447 console.error(xl('User Debug Error Catch'), message);
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);
464 Error: JSON.stringify(error)
467 showDebugAlert(message);
470 let xlated = xl('Unknown Script Error: See Browser Console for Detail');
471 showDebugAlert(xlated);
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");
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});
503 window.oeSMART = oeSMART;
504 })(window, window.top.oeSMART || {});