/* global getScript, AGGrid, Server, DateTimeUtils */
/**
* Created by Blake McBride on 1/20/16.
*/
'use strict';
let Component = {};
Component.ComponentsBeingLoaded = 0;
Component.ComponentList = [];
let Kiss = {};
/**
* This is how components are accessed.
*
* @param {string} id for radio buttons this is the id of the group, for other controls this is the control's id
* @returns {*} the component object
*/
function $$(id) {
if (typeof Kiss !== 'undefined' && typeof Kiss.RadioButtons !== 'undefined' && Kiss.RadioButtons.groups[id]) {
const rbObj = {};
let originalValue;
rbObj.getValue = function () {
return Kiss.RadioButtons.getValue(id);
};
rbObj.getIntValue = function () {
let val = Kiss.RadioButtons.getValue(id);
return val ? Number(val) : 0;
};
rbObj.setValue = function (val) {
Kiss.RadioButtons.setValue(id, val);
originalValue = rbObj.getValue();
return rbObj;
};
rbObj.clear = function () {
Kiss.RadioButtons.clear(id);
originalValue = rbObj.getValue();
return rbObj;
};
rbObj.isDirty = function () {
return originalValue !== rbObj.getValue(id);
};
rbObj.readOnly = function () {
Kiss.RadioButtons.readOnly(id);
return rbObj;
};
rbObj.readWrite = function () {
Kiss.RadioButtons.readWrite(id);
return rbObj;
};
rbObj.isReadOnly = function () {
return Kiss.RadioButtons.isReadOnly(id);
};
rbObj.onChange = function (fun) {
Kiss.RadioButtons.onChange(id, fun);
return rbObj;
};
rbObj.isError = function (lbl) {
return Kiss.RadioButtons.isError(id, lbl);
};
rbObj.enable = function (flg=true) {
Kiss.RadioButtons.enable(id, flg);
return rbObj;
};
rbObj.disable = function (flg=true) {
Kiss.RadioButtons.disable(id, flg);
return rbObj;
};
rbObj.hide = function (flg=true) {
Kiss.RadioButtons.hide(id, flg);
return rbObj;
};
rbObj.show = function (flg=true) {
Kiss.RadioButtons.show(id, flg);
return rbObj;
};
rbObj.isHidden = function () {
return Kiss.RadioButtons.isHidden(id);
};
rbObj.isVisible = function () {
return Kiss.RadioButtons.isVisible(id);
};
rbObj.focus = function () {
Kiss.RadioButtons.focus(id);
return rbObj;
};
return rbObj;
}
const e = $((id.charAt(0) === '#' ? '' : '#') + id);
if (e.length)
return e[0].kiss;
else {
console.log("$$: field " + id + " does not exist.");
return null;
}
}
/**
* General utilities class
*/
class Utils {
/**
* Display a popup window with a message to the user. The user will click "Ok" when they have read the message.
* If the title is 'Error' the popup will appear in red.
*
* @param {string} title appears on the title bar of the message window
* @param {string} message the message to be displayed
* @returns {Promise} when popup disappears
*/
static showMessage(title, message) {
const self = this;
Utils.waitMessageEnd();
return new Promise(function (resolve, reject) {
let modal = $('#msg-modal');
if (!modal.length) {
$('body').append(
'<div id="msg-modal" class="msg-modal">' +
' <!-- Modal content -->' +
' <div class="msg-modal-content" id="msg-modal-content-tab">' +
' <div class="msg-modal-header" id="msg-modal-header-tab">' +
' <span id="msg-close-btn" class="msg-close">×</span>' +
' <p id="msg-header" style="margin-top: 2px;">Modal Header</p>' +
' </div>' +
' <div class="msg-modal-body">' +
' <p id="msg-message" style="margin-top: 5px, margin-bottom: 5px;"></p>' +
' </div>' +
' <div class="msg-modal-footer">' +
' <input type="button" value="Ok" id="message-ok" style="margin-top: 5px; margin-bottom: 10px;">' +
' </div>' +
' </div>' +
'</div>');
modal = $('#msg-modal'); // the append changes this
// Adjust width for mobile
const content = $('#msg-modal-content-tab');
const smaller = screen.width < screen.height ? screen.width : screen.height;
if (smaller < content.width() + 20)
content.width(smaller-20);
}
$('#msg-header').text(title);
const header = $('#msg-modal-header-tab');
self.makeDraggable(header, $('#msg-modal-content-tab'));
$('#msg-message').text(message);
const closeBtn = $('#msg-close-btn');
if (title === 'Error')
header.css('background-color', 'red');
else
header.css('background-color', '#6495ed');
function endfun() {
modal.hide();
resolve();
}
modal.show();
let waitForKeyUp = false;
closeBtn.off('click').click(function (e) {
endfun();
});
$('#message-ok').off('click').off('keyup').click(function (e) {
if (!waitForKeyUp)
endfun();
}).focus().on('keyup', function (e) {
e.stopPropagation();
if (waitForKeyUp && e.key === 'Enter')
endfun();
}).on('keydown', function (e) {
e.stopPropagation();
if (e.key === 'Enter')
waitForKeyUp = true;
});
});
}
/**
* Display a modal popup that asks the user a yes/no question.
*
* The yesFun/noFun can be used, or this function returns a promise
* so that is can be used with async/await.
*
* @param {string} title text on the title bar of the popup
* @param {string} message the question being asked
* @param {function} yesFun function that gets executed if the user clicks 'Yes'
* @param {function} noFun function that gets executed if the user clicks 'No'
*/
static yesNo(title, message, yesFun = null, noFun = null) {
return new Promise(function(resolve, reject) {
if (!$('#yesno-modal').length) {
$('body').append(
'<div id="yesno-modal" class="msg-modal">' +
' <!-- Modal content -->' +
' <div class="msg-modal-content" id="yesno-popup-content">' +
' <div class="msg-modal-header" id="yesno-popup-header">' +
' <span id="yesno-close-btn" class="msg-close">×</span>' +
' <p id="yesno-header" style="margin-top: 2px;">Modal Header</p>' +
' </div>' +
' <div class="msg-modal-body">' +
' <p id="yesno-message" style="margin-top: 5px, margin-bottom: 5px;"></p>' +
' </div>' +
' <div class="msg-modal-footer">' +
' <input type="button" value="Yes" id="yesno-yes" style="margin-top: 5px; margin-bottom: 10px;">' +
' <input type="button" value="No" id="yesno-no" style="margin-top: 5px; margin-bottom: 10px; margin-left: 10px;">' +
' </div>' +
' </div>' +
'</div>');
// Adjust width for mobile
const content = $('#yesno-popup-content');
const smaller = screen.width < screen.height ? screen.width : screen.height;
if (smaller < content.width() + 20)
content.width(smaller-20);
}
Utils.makeDraggable($('#yesno-popup-header'), $('#yesno-popup-content'));
$('#yesno-header').text(title);
$('#yesno-message').text(message);
const modal = $('#yesno-modal');
const span = $('#yesno-close-btn');
modal.show();
span.off('click').click(function () {
modal.hide();
if (noFun)
noFun();
resolve(false);
});
$('#yesno-yes').off('click').click(function () {
modal.hide();
if (yesFun)
yesFun();
resolve(true);
});
$('#yesno-no').off('click').click(function () {
modal.hide();
if (noFun)
noFun();
resolve(false);
});
});
}
/**
* Display a modal popup with a message. The message stays there until the application executes waitMessageEnd().
* This method is used when the user needs to be notified to wait for a long-running process.
* <br><br>
* This method tracks nested wait messages.
* It displays the last one but shows previous ones once the last one has ended.
*
* @param {string} message the message to be displayed
*/
static waitMessage(message) {
Utils.waitMessageStack.push(message);
if (Utils.waitMessageStack.length > 1 && Utils.waitMessageStack[Utils.waitMessageStack.length-2] === message)
return;
if (!$('#wmsg-modal').length) {
$('body').append(
'<div id="wmsg-modal" class="msg-modal">' +
' <!-- Modal content -->' +
' <div class="wmsg-modal-content" id="wait-msg-content">' +
' <div class="msg-modal-body">' +
' <p id="wmsg-message" style="margin-top: 5px, margin-bottom: 5px;"></p>' +
' </div>' +
' </div>' +
'</div>');
// Adjust width for mobile
const content = $('#wait-msg-content');
const smaller = screen.width < screen.height ? screen.width : screen.height;
if (smaller < content.width() + 20)
content.width(smaller-20);
}
const content = $('#wait-msg-content');
this.makeDraggable(content, content);
$('#wmsg-message').text(message);
$('#wmsg-modal').show();
}
/**
* This terminates the wait message initiated by <code>waitMessage()</code>
* unless there is a previous one in which case it reverts to the previous one.
*/
static waitMessageEnd() {
Utils.waitMessageStack.pop();
if (!Utils.waitMessageStack.length)
$('#wmsg-modal').hide();
else
$('#wmsg-message').text(Utils.waitMessageStack[Utils.waitMessageStack.length-1]);
}
static getID(id) {
let e = $('#' + id);
if (!e.length) {
id = id.replace(/_/g, '-');
e = $('#' + id);
}
if (!e.length) {
id = id.replace(/-/g, '_');
e = $('#' + id);
}
return e.length ? id : null;
}
static zeroPad(num, places) {
const zero = places - num.toString().length + 1;
return Array(+(zero > 0 && zero)).join("0") + num;
}
/**
* Test the validity of a domain name.
*
* @param d {string} the domain to be tested
* @returns {boolean}
*/
static isValidDomain(d) {
if (!d || typeof d !== 'string' || d.length < 3)
return false;
if (d.replaceAll(/[abcdefghijklmnopqrstuvwxyz0123456789.-]/gi, ''))
return false;
if (d[0] === '.' || d[d.length-1] === '.')
return false;
if (d.indexOf('..') !== -1)
return false;
if (d.indexOf('.') === -1)
return false;
return true;
}
/**
* Test the validity of an email address.
*
* @param add {string} the email address to be tested
* @returns {boolean}
*/
static isValidEmailAddress(add) {
if (!add || typeof add !== 'string' || add.length < 5)
return false;
if (add.replaceAll(/[abcdefghijklmnopqrstuvwxyz0123456789._-]/gi, '') !== '@')
return false;
const idx = add.indexOf("@");
const dom = add.substr(idx+1);
if (!Utils.isValidDomain(dom))
return false;
const user = add.substr(0, idx);
if (user.length < 1 || user[0] === '.' || user[user.length-1] === '.')
return false;
if (user.indexOf('..') !== -1)
return false;
return true;
}
/**
* Determines if an email address with or without a name is valid.
* This accepts things like:
*
* name@abc.com
* <name@abc.com>
* George Tall <name@abc.com>
* "Tall, George" <name.abc.com>
*
* @param ad {string}
* @returns {boolean}
*/
static isValidEmailAddressWithName(ad) {
if (!ad || typeof ad !== 'string' || ad.length < 5)
return false;
const idx1 = ad.indexOf("<");
if (idx1 === -1)
return Utils.isValidEmailAddress(ad);
const idx2 = ad.indexOf(">");
if (idx2 < idx1 + 2)
return false;
const email = ad.substring(idx1+1, idx2-1);
return Utils.isValidEmailAddress(email);
}
/**
* Returns the email portion of an email address with a name.
* For example, all of the following will return "abc.com":
*
* name@abc.com
* <name@abc.com>
* George Tall <name@abc.com>
* "Tall, George" <name.abc.com>
*
* @param ad {string}
* @returns {string}
*/
static getEmailFromAddressWithName(ad) {
const idx1 = ad.indexOf("<");
if (idx1 === -1)
return ad;
const idx2 = ad.indexOf(">");
return ad.substring(idx1+1, idx2);
}
/**
* Returns the name portion of an email address with a name.
* For example:
*
* name@abc.com -> ""
* <name@abc.com> -> ""
* George Tall <name@abc.com> -> "George Tall"
* "Tall, George" <name.abc.com> -> "Tall, George"
*
* @param ad {string}
* @returns {string}
*/
static getNameFromAddressWithName(ad) {
const idx1 = ad.indexOf("<");
if (idx1 === -1)
return "";
let name = ad.substring(0, idx1).trim();
if (!name)
return "";
if (name[0] === "'" && name[name.length-1] === "'" || name[0] === '"' && name[name.length-1] === '"')
name = name.substring(1, name.length-1);
return name;
}
/**
* Splits a string containing any number of full email addresses with possible names into an array
* where each element is a single address.
*
* @param s {string}
* @returns {object} an array with the separated email addresses
*/
static splitEmailAddresses(s) {
if (!s || typeof s !== 'string' || s.length < 5)
return [];
const a = [];
let inQuote = false;
let quote;
let add = "";
for (let i=0 ; i < s.length ; i++) {
let c = s[i];
if (inQuote) {
if (c === quote)
inQuote = false;
add += c;
} else {
if (c === '"' || c === "'") {
inQuote = true;
quote = c;
add += c;
} else if (c === ",") {
a.push(add);
add = "";
} else
add += c;
}
}
if (add)
a.push(add);
return a;
}
/**
* APL-like take for strings.
*
* @param {string} s
* @param {number} n
* @returns {string}
*/
static take(s, n) {
if (!s)
s = '';
if (s.length === n)
return s;
if (n >= 0) {
if (n < s.length)
return s.substring(0, n);
for (n -= s.length; n-- > 0;)
s += ' ';
return s;
} else {
n = -n;
if (n < s.length)
return Utils.drop(s, s.length - n);
let sb = '';
for (n -= s.length; n-- > 0;)
sb += ' ';
sb += s;
return sb;
}
}
/**
* APL-like drop for strings.
*
* @param {string} s
* @param {number} n
* @returns {string}
*/
static drop(s, n) {
if (!s)
s = '';
if (!n)
return s;
if (n >= s.length || -n >= s.length)
return '';
if (n > 0)
return s.substring(n);
return s.substring(0, s.length + n);
}
static localeCurrencyMap = {
'en-US': 'USD', // United States Dollar
'en-GB': 'GBP', // British Pound Sterling
'de-DE': 'EUR', // Euro (Germany)
'fr-FR': 'EUR', // Euro (France)
'es-ES': 'EUR', // Euro (Spain)
'it-IT': 'EUR', // Euro (Italy)
'nl-NL': 'EUR', // Euro (Netherlands)
'el-GR': 'EUR', // Euro (Greece)
'pt-PT': 'EUR', // Euro (Portugal)
'ja-JP': 'JPY', // Japanese Yen
'zh-CN': 'CNY', // Chinese Yuan
'ru-RU': 'RUB', // Russian Ruble
'in-IN': 'INR', // Indian Rupee
'ar-SA': 'SAR', // Saudi Riyal
'ko-KR': 'KRW', // South Korean Won
'tr-TR': 'TRY', // Turkish Lira
'sv-SE': 'SEK', // Swedish Krona
'da-DK': 'DKK', // Danish Krone
'nb-NO': 'NOK', // Norwegian Krone
'fi-FI': 'EUR', // Euro (Finland)
'pl-PL': 'PLN', // Polish Zloty
'cs-CZ': 'CZK', // Czech Koruna
'hu-HU': 'HUF', // Hungarian Forint
'ro-RO': 'RON', // Romanian Leu
'bg-BG': 'BGN', // Bulgarian Lev
'en-CA': 'CAD', // Canadian Dollar
'en-AU': 'AUD', // Australian Dollar
'en-NZ': 'NZD', // New Zealand Dollar
'th-TH': 'THB', // Thai Baht
'id-ID': 'IDR', // Indonesian Rupiah
'ms-MY': 'MYR', // Malaysian Ringgit
'vi-VN': 'VND', // Vietnamese Dong
'tl-PH': 'PHP', // Philippine Peso
'he-IL': 'ILS', // Israeli New Shekel
'ar-AE': 'AED', // United Arab Emirates Dirham
'en-ZA': 'ZAR', // South African Rand
'en-SG': 'SGD', // Singapore Dollar
'en-HK': 'HKD', // Hong Kong Dollar
'es-MX': 'MXN', // Mexican Peso
'pt-BR': 'BRL', // Brazilian Real
'en-IE': 'EUR', // Euro (Ireland)
'en-CH': 'CHF', // Swiss Franc (Switzerland)
'nl-BE': 'EUR', // Euro (Belgium)
'en-AT': 'EUR', // Euro (Austria)
'en-CY': 'EUR', // Euro (Cyprus)
'et-EE': 'EUR', // Euro (Estonia)
'fi-AX': 'EUR', // Euro (Åland Islands)
'fr-GF': 'EUR', // Euro (French Guiana)
'es-AR': 'ARS', // Argentine Peso
'pt-AO': 'AOA', // Angolan Kwanza
'en-BS': 'BSD', // Bahamian Dollar
'en-BB': 'BBD', // Barbadian Dollar
'en-BZ': 'BZD', // Belize Dollar
'en-BM': 'BMD', // Bermudian Dollar
'es-BO': 'BOB', // Bolivian Boliviano
'nl-BQ': 'USD', // United States Dollar (Caribbean Netherlands)
'pt-CV': 'CVE', // Cape Verdean Escudo
'en-KY': 'KYD', // Cayman Islands Dollar
'es-CL': 'CLP', // Chilean Peso
'es-CO': 'COP', // Colombian Peso
'es-CR': 'CRC', // Costa Rican Colón
'es-CU': 'CUP', // Cuban Peso
'en-DM': 'XCD', // East Caribbean Dollar (Dominica)
'es-DO': 'DOP', // Dominican Peso
'fr-DJ': 'DJF', // Djiboutian Franc
'ar-DZ': 'DZD', // Algerian Dinar
'en-EG': 'EGP', // Egyptian Pound
'am-ET': 'ETB', // Ethiopian Birr
'en-FJ': 'FJD', // Fijian Dollar
'fr-GA': 'XAF', // Central African CFA Franc (Gabon)
'en-GH': 'GHS', // Ghanaian Cedi
'en-GM': 'GMD', // Gambian Dalasi
'en-GY': 'GYD', // Guyanese Dollar
'es-GT': 'GTQ', // Guatemalan Quetzal
'en-GD': 'XCD', // East Caribbean Dollar (Grenada)
'fr-GP': 'EUR', // Euro (Guadeloupe)
'es-HN': 'HNL', // Honduran Lempira
'en-JM': 'JMD', // Jamaican Dollar
'ar-JO': 'JOD', // Jordanian Dinar
'kk-KZ': 'KZT', // Kazakhstani Tenge
'rw-RW': 'RWF', // Rwandan Franc
'ko-KP': 'KPW', // North Korean Won
'ar-KW': 'KWD', // Kuwaiti Dinar
'kk-KG': 'KGS', // Kyrgyzstani Som
'lo-LA': 'LAK', // Lao Kip
'lv-LV': 'EUR', // Euro (Latvia)
'ar-LB': 'LBP', // Lebanese Pound
'st-ST': 'STD', // São Tomé and Príncipe Dobra
'fr-CI': 'XOF', // CFA Franc BCEAO (Ivory Coast)
'pt-MZ': 'MZN', // Mozambican Metical
'sw-TZ': 'TZS', // Tanzanian Shilling
'en-KE': 'KES', // Kenyan Shilling
'fr-CM': 'XAF', // Central African CFA Franc (Cameroon)
'en-NG': 'NGN', // Nigerian Naira
'fr-MG': 'MGA', // Malagasy Ariary
'en-UG': 'UGX', // Ugandan Shilling
'en-ZM': 'ZMW', // Zambian Kwacha
'fr-BF': 'XOF', // CFA Franc BCEAO (Burkina Faso)
'fr-SN': 'XOF', // CFA Franc BCEAO (Senegal)
'en-LR': 'LRD', // Liberian Dollar
'en-SL': 'SLL', // Sierra Leonean Leone
'ar-SD': 'SDG', // Sudanese Pound
'ar-ER': 'ERN', // Eritrean Nakfa
'so-SO': 'SOS', // Somali Shilling
'ar-EG': 'EGP', // Egyptian Pound (Egypt)
'sw-KE': 'KES', // Kenyan Shilling (Kenya)
'en-TZ': 'TZS', // Tanzanian Shilling (Tanzania)
'fr-BJ': 'XOF', // West African CFA franc (Benin)
'en-ZW': 'ZWL', // Zimbabwean Dollar (Zimbabwe)
'en-NA': 'NAD', // Namibian Dollar (Namibia)
'fr-CD': 'CDF', // Congolese Franc (Democratic Republic of the Congo)
'pt-GW': 'XOF', // CFA Franc BCEAO (Guinea-Bissau)
'fr-MC': 'EUR', // Euro (Monaco)
'en-SC': 'SCR', // Seychellois Rupee (Seychelles)
'en-AG': 'XCD', // East Caribbean Dollar (Antigua and Barbuda)
'fr-BI': 'BIF', // Burundian Franc (Burundi)
'fr-TD': 'XAF', // Central African CFA Franc (Chad)
'fr-KM': 'KMF', // Comorian Franc (Comoros)
'pt-TL': 'USD', // United States Dollar (East Timor)
'en-FK': 'FKP', // Falkland Islands Pound (Falkland Islands)
'fr-GQ': 'XAF', // Central African CFA Franc (Equatorial Guinea)
'en-SH': 'SHP', // Saint Helena Pound (Saint Helena)
'en-KI': 'AUD', // Australian Dollar (Kiribati)
'en-MH': 'USD', // United States Dollar (Marshall Islands)
'en-FM': 'USD', // United States Dollar (Micronesia)
'en-NR': 'AUD', // Australian Dollar (Nauru)
'en-PW': 'USD', // United States Dollar (Palau)
'en-PG': 'PGK', // Papua New Guinean Kina (Papua New Guinea)
'en-WS': 'WST', // Samoan Tala (Samoa)
'en-SB': 'SBD', // Solomon Islands Dollar (Solomon Islands)
'en-TO': 'TOP', // Tongan Paʻanga (Tonga)
'en-TV': 'AUD', // Australian Dollar (Tuvalu)
'en-VU': 'VUV', // Vanuatu Vatu (Vanuatu)
'fr-PF': 'XPF', // CFP Franc (French Polynesia)
'fr-NC': 'XPF', // CFP Franc (New Caledonia)
'fr-WF': 'XPF', // CFP Franc (Wallis and Futuna)
'en-GG': 'GBP', // British Pound Sterling (Guernsey)
'en-IM': 'GBP', // British Pound Sterling (Isle of Man)
'en-JE': 'GBP', // British Pound Sterling (Jersey)
'en-GI': 'GIP', // Gibraltar Pound (Gibraltar)
'fr-ML': 'XOF', // West African CFA franc (Mali)
'pt-ST': 'STN', // São Tomé and Príncipe Dobra (São Tomé and Príncipe)
'fr-NE': 'XOF', // West African CFA franc (Niger)
'en-LS': 'LSL', // Lesotho Loti (Lesotho)
'en-BW': 'BWP', // Botswana Pula (Botswana)
'en-MU': 'MUR', // Mauritian Rupee (Mauritius)
'ar-IQ': 'IQD', // Iraqi Dinar (Iraq)
'ar-LY': 'LYD', // Libyan Dinar (Libya)
'ar-MA': 'MAD', // Moroccan Dirham (Morocco)
'ar-TN': 'TND', // Tunisian Dinar (Tunisia)
'ar-YE': 'YER', // Yemeni Rial (Yemen)
'en-BN': 'BND', // Brunei Dollar (Brunei)
'ms-BN': 'BND', // Brunei Dollar (Brunei, Malay)
'km-KH': 'KHR', // Cambodian Riel (Cambodia)
'my-MM': 'MMK', // Myanmar Kyat (Myanmar)
'ne-NP': 'NPR', // Nepalese Rupee (Nepal)
'dz-BT': 'BTN', // Bhutanese Ngultrum (Bhutan)
'en-BT': 'BTN', // Bhutanese Ngultrum (Bhutan, English)
'en-MV': 'MVR', // Maldivian Rufiyaa (Maldives)
'dv-MV': 'MVR', // Maldivian Rufiyaa (Maldives, Dhivehi)
'en-CK': 'NZD', // New Zealand Dollar (Cook Islands)
'bi-KI': 'AUD', // Australian Dollar (Kiribati, Gilbertese)
'sm-WF': 'XPF', // CFP Franc (Wallis and Futuna, Samoan)
'ty-PF': 'XPF', // CFP Franc (French Polynesia, Tahitian)
// ... add more as needed
};
/**
* Numeric formatter. Takes a number and converts it to a nicely formatted String (for number in base 10).
* Correctly handles international formatting rules.
*
* @param {number} num number to be formatted
* @param {string} msk format mask - any combination of the following:<br>
* <ul>
* <li>B = blank if zero</li>
* <li>C = add commas</li>
* <li>L = left justify number</li>
* <li>P = put parentheses around negative numbers</li>
* <li>Z = zero fill</li>
* <li>D = floating dollar or monetary symbol</li>
* <li>M = monetary (same as 'D')</li>
* <li>R = add a percent sign to the end of the number</li>
* </ul>
* @param {number} wth total field width (0 means auto)
* @param {number} dp number of decimal places (-1 means auto)
* @return {string} string the formatted String
*<p>
* example:
*<br><br>
* let r = Utils.format(-12345.348, "CP", 12, 2);
*<br><br>
* result in r: "(12,345.35)"
* </p>
*/
static format(num, msk, wth, dp) {
const options = {};
let blnk=false, comma=false, left=false, paren=false, zfill=false, dol=false, ucase=false, percent=false;
if (msk) {
msk = msk.toUpperCase();
for (let i2 = 0; i2 < msk.length; i2++)
switch (msk.charAt(i2)) {
case 'B': // blank if zero
blnk = true;
break;
case 'S': // add separator
case 'C': // add commas
comma = true;
break;
case 'L': // left justify
left = true;
break;
case 'P': // parens around negative numbers
paren = true;
break;
case 'Z': // zero fill
zfill = true;
break;
case 'M': // monetary
case 'D': // dollar sign
dol = true;
break;
case 'U': // upper case letters
ucase = true;
break;
case 'R': // add percent
percent = true;
break;
}
}
if (percent)
num /= 100;
if (blnk && num < .0001 && num > -.0001)
return wth > 0 ? ' '.repeat(wth) : '';
options.useGrouping = comma;
if (dp !== -1) {
options.minimumFractionDigits = dp;
options.maximumFractionDigits = dp;
}
if (dol) {
options.style = "currency";
let currency = this.localeCurrencyMap[navigator.language];
if (!currency)
currency = "USD";
options.currency = currency;
if (paren)
options.currencySign = "accounting";
} else if (percent)
options.style = "percent";
let ret;
if (!dol && paren) {
if (num < 0)
ret = '(' + (new Intl.NumberFormat(navigator.language, options)).format(-num) + ')';
else
ret = (new Intl.NumberFormat(navigator.language, options)).format(num) + ' ';
} else
ret = (new Intl.NumberFormat(navigator.language, options)).format(num);
if (wth > 0) {
if (ret.length > wth)
if (comma)
return Utils.format(num, base, msk.replaceAll("[Cc]", ""), wth, dp);
else
return '*'.repeat(wth);
if (left)
ret = ret.padEnd(wth, ' ');
else
ret = ret.padStart(wth, zfill ? '0' : ' ');
}
return ret;
}
/**
* Numeric formatter. Takes a number and converts it to a nicely formatted String in a specified number base.
*
* @param {number} num number to be formatted
* @param {number} base numeric base (like base 2 = binary, 16=hex...)
* @param {string} msk format mask - any combination of the following:<br>
* <ul>
* <li>B = blank if zero</li>
* <li>C = add commas</li>
* <li>L = left justify number</li>
* <li>P = put parentheses around negative numbers</li>
* <li>Z = zero fill</li>
* <li>D = floating dollar sign</li>
* <li>U = uppercase letters in conversion</li>
* <li>R = add a percent sign to the end of the number</li>
* </ul>
* @param {number} wth total field width (0 means auto)
* @param {number} dp number of decimal places (-1 means auto)
* @return {string} the formatted String
*<p>
* Example:
*<br><br>
* let r = Utils.formatb(-12345.348, 10, "CP", 12, 2);
*<br><br>
* result in r: "(12,345.35)"
* </p>
*
* @see Utils.format
*/
static formatb(num, base, msk, wth, dp) {
let si, i, r, n;
let sign, blnk, comma, left, paren, zfill, nd, dol, tw, dl, ez, ucase, cf, percent;
let dbase;
const alpha = '0123456789abcdefghijklmnopqrstuvwxyz';
if (base < 2 || base > alpha.length)
base = 10;
dbase = base;
if (num < 0.0) {
num = -num;
sign = 1;
} else
sign = 0;
/* round number */
if (dp >= 0) {
r = Math.pow(dbase, dp);
// n = Math.floor(base/20.0 + n * r) / r;
num = Math.floor(.5 + num * r) / r;
}
switch (base) {
case 10:
cf = 3;
dl = num < 1.0 ? 0 : 1 + Math.floor(Math.log10(num));
/* # of digits left of . */
break;
case 2:
cf = 4;
dl = num < 1.0 ? 0 : 1 + Math.floor(Math.log(num) / .6931471806);
/* # of digits left of . */
break;
case 8:
cf = 3;
dl = num < 1.0 ? 0 : 1 + Math.floor(Math.log(num) / 2.079441542);
/* # of digits left of . */
break;
case 16:
cf = 4;
dl = num < 1.0 ? 0 : 1 + Math.floor(Math.log(num) / 2.772588722);
/* # of digits left of . */
break;
default:
cf = 3;
dl = num < 1.0 ? 0 : 1 + Math.floor(Math.log(num) / Math.log(dbase));
/* # of digits left of . */
break;
}
if (dp < 0) { /* calculate the number of digits right of decimal point */
n = num < 0.0 ? -num : num;
dp = 0;
while (dp < 20) {
n -= Math.floor(n);
if (1E-5 >= n)
break;
dp++;
/* round n to 5 places */
r = Math.pow(10, 5);
r = Math.floor(.5 + Math.abs(n * base * r)) / r;
n = n < 0.0 ? -r : r;
}
}
blnk = comma = left = paren = zfill = dol = ucase = percent = 0;
if (msk) {
msk = msk.toUpperCase();
for (let i2 = 0; i2 < msk.length; i2++)
switch (msk.charAt(i2)) {
case 'B': // blank if zero
blnk = 1;
break;
case 'C': // add commas
comma = Math.floor((dl - 1) / cf);
if (comma < 0)
comma = 0;
break;
case 'L': // left justify
left = 1;
break;
case 'P': // parens around negative numbers
paren = 1;
break;
case 'Z': // zero fill
zfill = 1;
break;
case 'D': // dollar sign
dol = 1;
break;
case 'U': // upper case letters
ucase = 1;
break;
case 'R': // add percent
percent = 1;
break;
}
}
/* calculate what the number should take up */
ez = num < 1.0 ? 1 : 0;
tw = dol + paren + comma + sign + dl + dp + (dp === 0 ? 0 : 1) + ez + percent;
if (wth < 1)
wth = tw;
else if (tw > wth) {
if (ez)
tw -= ez--;
if ((i = dol) && tw > wth)
tw -= dol--;
if (tw > wth && comma) {
tw -= comma;
comma = 0;
}
if (tw < wth && i) {
tw++;
dol = 1;
}
if (tw > wth && paren)
tw -= paren--;
if (tw > wth && percent)
tw -= percent--;
if (tw > wth) {
let tbuf = '';
for (i = 0; i < wth; i++)
tbuf += '*';
return tbuf;
}
}
let buf = new Array(wth);
num = Math.floor(.5 + num * Math.floor(.5 + Math.pow(dbase, dp)));
if (blnk && num === 0.0) {
for (i = 0; i < wth;)
buf[i++] = ' ';
return buf.join('');
}
si = wth;
if (left && wth > tw)
for (i = wth - tw; i--;)
buf[--si] = ' ';
if (paren)
buf[--si] = sign ? ')' : ' ';
if (percent)
buf[--si] = '%';
for (nd = 0; nd < dp && si; nd++) {
num /= dbase;
i = Math.floor(dbase * (num - Math.floor(num)) + .5);
num = Math.floor(num);
buf[--si] = ucase && i > 9 ? alpha.charAt(i).toUpperCase() : alpha.charAt(i);
}
if (dp)
if (si)
buf[--si] = '.';
else
num = 1.0;
if (ez && si > sign + dol)
buf[--si] = '0';
nd = 0;
while (num > 0.0 && si)
if (comma && nd === cf) {
buf[--si] = ',';
nd = 0;
} else {
num /= dbase;
i = Math.floor(dbase * (num - Math.floor(num)) + .5);
num = Math.floor(num);
buf[--si] = ucase && i > 9 ? alpha.charAt(i).toUpperCase() : alpha.charAt(i);
nd++;
}
if (zfill)
for (i = sign + dol; si > i;)
buf[--si] = '0';
if (dol && si)
buf[--si] = '$';
if (sign)
if (si)
buf[--si] = paren ? '(' : '-';
else
num = 1.0;
/* signal error condition */
while (si)
buf[--si] = ' ';
if (num !== 0.0) /* should never happen. but just in case */
for (i = 0; i < wth;)
buf[i++] = '*';
return buf.join('');
}
/**
* Get the extension of the file name. So, "abc.def" would return "def".
*
* @param {string} filename
* @returns {string}
*/
static fileNameExtension(filename) {
const fileSplit = filename.split('.');
let fileExt = '';
if (fileSplit.length > 1)
fileExt = fileSplit[fileSplit.length - 1];
return fileExt;
}
/**
* When calling a SOAP web service, array returns are not arrays but single objects when there is only one element.
* (XML to Json problem). This function assures that an element is an array when one is expected.
*
* @param x
* @returns {Array}
*/
static assureArray(x) {
if ($.isArray(x))
return x;
let r = [];
if (x)
r[0] = x;
return r;
}
/**
* Parse the URL string and extract the URL parameters.
*
* @param {string} key parameter name
* @returns {string} the parameter value or null if not there
*/
static getURLParam(key) {
const url = window.location.href;
const args = url.split('?');
if (args.length !== 2)
return null;
const pairs = args[1].split('&');
if (pairs.length === 0)
return null;
for (let i = 0; i < pairs.length; i++) {
let keyval = pairs[i].split('=');
if (keyval[0] === key)
if (keyval.length > 0)
return decodeURI(keyval[1]);
else
return '';
}
return null;
}
/**
* Returns the root URL of the application.
*
* @returns {string}
*/
static getAppUrl() {
let loc = window.location.href;
let i = loc.indexOf('?');
if (i > 0)
loc = loc.substring(0, i);
i = loc.lastIndexOf('/');
return i > 0 ? loc.substring(0, i) : loc;
}
/**
* Initialize a tagless component.
*
* @param {string} path back-end path to the component
*/
static useTaglessComponent(path) {
const npath = path.charAt(0).toLowerCase() + path.substring(1) + '/' + path.charAt(0).toUpperCase() + path.substring(1);
getScript('/kiss/component/' + npath + '.js');
}
// internal
static tagReplace(inp, rpl) {
for (let prop in rpl) {
let val = rpl[prop];
if (!val && val !== 0)
val = '';
inp = inp.replace(new RegExp('{' + prop + '}', 'g'), val);
}
return inp;
}
// internal
static afterComponentsLoaded(fun) {
Component.AfterAllComponentsLoaded = fun;
}
/**
* Rescan the HTML file and replace KISS components with HTML components.
* This needs to be done each time new KISS controls are attached.
*
* This method is mainly used internally. However, it may be useful when dynamically
* adding controls to the DOM. At the end of those additions, this method would be called
* to activate the Kiss custom HTML tags.
*/
static rescan() {
let n = -1;
while (n) { // keep replacing until nothing left to replace
n = 0;
for (let i = 0; i < Component.ComponentList.length; i++) {
let ci = Component.ComponentList[i];
if (ci.tag && ci.name === 'Popup') {
$(ci.tag).each(function () {
let elm = $(this);
ci.processor(elm, Utils.getAllAttributes(elm), elm.html());
n++;
});
break;
}
}
for (let i = 0; i < Component.ComponentList.length; i++) {
let ci = Component.ComponentList[i];
if (ci.tag && ci.name !== "Popup")
$(ci.tag).each(function () {
let elm = $(this);
ci.processor(elm, Utils.getAllAttributes(elm), elm.html());
n++;
});
}
}
}
/**
* Loads a component (new HTML element)
*
* @param {string} path back-end path to the component HTML and JS files.
*/
static useComponent(path) {
Component.ComponentsBeingLoaded++;
const npath = 'kiss/component/' + path.charAt(0).toLowerCase() + path.substring(1) + '/' + path.charAt(0).toUpperCase() + path.substring(1) + '.js';
getScript(npath).then(function () {
if (!--Component.ComponentsBeingLoaded && Component.AfterAllComponentsLoaded) {
Utils.rescan(); // does all the tag replacement
Component.AfterAllComponentsLoaded();
Component.AfterAllComponentsLoaded = null;
}
});
}
static getAllAttributes(elm) {
const ret = {};
$.each(elm[0].attributes, function () {
ret[this.name] = this.value;
});
return ret;
}
static removeQuotes(s) {
if (!s || !s.length)
return '';
let c = s.charAt(0);
if (c === '"' || c === "'")
s = s.substr(1);
if (!s.length)
return '';
c = s.slice(-1); // last character
if (c === '"' || c === "'")
s = s.substring(0, s.length - 1);
return s;
}
static newComponent(ci) {
Component.ComponentList.push(ci);
Component[ci.name] = {};
}
/**
* Emits an audible beep on the user's computer.
*
*/
static beep() {
const snd = new Audio("data:audio/wav;base64,//uQRAAAAWMSLwUIYAAsYkXgoQwAEaYLWfkWgAI0wWs/ItAAAGDgYtAgAyN+QWaAAihwMWm4G8QQRDiMcCBcH3Cc+CDv/7xA4Tvh9Rz/y8QADBwMWgQAZG/ILNAARQ4GLTcDeIIIhxGOBAuD7hOfBB3/94gcJ3w+o5/5eIAIAAAVwWgQAVQ2ORaIQwEMAJiDg95G4nQL7mQVWI6GwRcfsZAcsKkJvxgxEjzFUgfHoSQ9Qq7KNwqHwuB13MA4a1q/DmBrHgPcmjiGoh//EwC5nGPEmS4RcfkVKOhJf+WOgoxJclFz3kgn//dBA+ya1GhurNn8zb//9NNutNuhz31f////9vt///z+IdAEAAAK4LQIAKobHItEIYCGAExBwe8jcToF9zIKrEdDYIuP2MgOWFSE34wYiR5iqQPj0JIeoVdlG4VD4XA67mAcNa1fhzA1jwHuTRxDUQ//iYBczjHiTJcIuPyKlHQkv/LHQUYkuSi57yQT//uggfZNajQ3Vmz+Zt//+mm3Wm3Q576v////+32///5/EOgAAADVghQAAAAA//uQZAUAB1WI0PZugAAAAAoQwAAAEk3nRd2qAAAAACiDgAAAAAAABCqEEQRLCgwpBGMlJkIz8jKhGvj4k6jzRnqasNKIeoh5gI7BJaC1A1AoNBjJgbyApVS4IDlZgDU5WUAxEKDNmmALHzZp0Fkz1FMTmGFl1FMEyodIavcCAUHDWrKAIA4aa2oCgILEBupZgHvAhEBcZ6joQBxS76AgccrFlczBvKLC0QI2cBoCFvfTDAo7eoOQInqDPBtvrDEZBNYN5xwNwxQRfw8ZQ5wQVLvO8OYU+mHvFLlDh05Mdg7BT6YrRPpCBznMB2r//xKJjyyOh+cImr2/4doscwD6neZjuZR4AgAABYAAAABy1xcdQtxYBYYZdifkUDgzzXaXn98Z0oi9ILU5mBjFANmRwlVJ3/6jYDAmxaiDG3/6xjQQCCKkRb/6kg/wW+kSJ5//rLobkLSiKmqP/0ikJuDaSaSf/6JiLYLEYnW/+kXg1WRVJL/9EmQ1YZIsv/6Qzwy5qk7/+tEU0nkls3/zIUMPKNX/6yZLf+kFgAfgGyLFAUwY//uQZAUABcd5UiNPVXAAAApAAAAAE0VZQKw9ISAAACgAAAAAVQIygIElVrFkBS+Jhi+EAuu+lKAkYUEIsmEAEoMeDmCETMvfSHTGkF5RWH7kz/ESHWPAq/kcCRhqBtMdokPdM7vil7RG98A2sc7zO6ZvTdM7pmOUAZTnJW+NXxqmd41dqJ6mLTXxrPpnV8avaIf5SvL7pndPvPpndJR9Kuu8fePvuiuhorgWjp7Mf/PRjxcFCPDkW31srioCExivv9lcwKEaHsf/7ow2Fl1T/9RkXgEhYElAoCLFtMArxwivDJJ+bR1HTKJdlEoTELCIqgEwVGSQ+hIm0NbK8WXcTEI0UPoa2NbG4y2K00JEWbZavJXkYaqo9CRHS55FcZTjKEk3NKoCYUnSQ0rWxrZbFKbKIhOKPZe1cJKzZSaQrIyULHDZmV5K4xySsDRKWOruanGtjLJXFEmwaIbDLX0hIPBUQPVFVkQkDoUNfSoDgQGKPekoxeGzA4DUvnn4bxzcZrtJyipKfPNy5w+9lnXwgqsiyHNeSVpemw4bWb9psYeq//uQZBoABQt4yMVxYAIAAAkQoAAAHvYpL5m6AAgAACXDAAAAD59jblTirQe9upFsmZbpMudy7Lz1X1DYsxOOSWpfPqNX2WqktK0DMvuGwlbNj44TleLPQ+Gsfb+GOWOKJoIrWb3cIMeeON6lz2umTqMXV8Mj30yWPpjoSa9ujK8SyeJP5y5mOW1D6hvLepeveEAEDo0mgCRClOEgANv3B9a6fikgUSu/DmAMATrGx7nng5p5iimPNZsfQLYB2sDLIkzRKZOHGAaUyDcpFBSLG9MCQALgAIgQs2YunOszLSAyQYPVC2YdGGeHD2dTdJk1pAHGAWDjnkcLKFymS3RQZTInzySoBwMG0QueC3gMsCEYxUqlrcxK6k1LQQcsmyYeQPdC2YfuGPASCBkcVMQQqpVJshui1tkXQJQV0OXGAZMXSOEEBRirXbVRQW7ugq7IM7rPWSZyDlM3IuNEkxzCOJ0ny2ThNkyRai1b6ev//3dzNGzNb//4uAvHT5sURcZCFcuKLhOFs8mLAAEAt4UWAAIABAAAAAB4qbHo0tIjVkUU//uQZAwABfSFz3ZqQAAAAAngwAAAE1HjMp2qAAAAACZDgAAAD5UkTE1UgZEUExqYynN1qZvqIOREEFmBcJQkwdxiFtw0qEOkGYfRDifBui9MQg4QAHAqWtAWHoCxu1Yf4VfWLPIM2mHDFsbQEVGwyqQoQcwnfHeIkNt9YnkiaS1oizycqJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU=");
snd.play();
}
static nextID() {
return "ID-" + Utils.count++;
}
static replaceHTML(id, elm, template, rplobj) {
if (!id)
id = Utils.nextID();
rplobj.id = id;
const newHTML = Utils.tagReplace(template, rplobj);
elm.replaceWith(newHTML);
const jqObj = $('#' + id);
const newElm = jqObj[0];
if (!newElm) {
console.log(elm[0].localName + ' is missing an ID');
return undefined;
}
newElm.kiss = {};
newElm.kiss.jqObj = jqObj;
newElm.kiss.elementInfo = {};
return newElm.kiss;
}
static getHTML(url) {
return new Promise(async function (resolve, reject) {
let response;
try {
response = await fetch(url + (SystemInfo.controlCache ? '?ver=' + SystemInfo.softwareVersion : ''), {
method: 'GET',
headers: {
'Content-type': 'text/plain'
}
});
} catch (err) {
console.log(err.message);
console.log(err.stack);
reject(err);
return;
}
try {
let r = await response.text();
resolve(r);
} catch (err) {
console.log(err.message);
console.log(err.stack);
reject(err);
}
});
};
/**
* Loads a new HTML/JS page. The new page will replace the body of the current page.
* Also, the loaded code is processed for custom tags / components.
* <br><br>
* <code>.html</code> and <code>.js</code> are appended to <code>page</code> to determine what to load.
* The HTML file is loaded first and then the JS file.
*
* @param {string} page path to the page to be loaded
* @param {string} tag optional ID of div to fill (if empty, "body" tag is used)
* @param {string} initialFocus optional, ID of control to set initial focus on
* @param {object} argv arguments for the page being loaded
* @param {object} retv return value array from child screen (mainly used internally)
*
* @see Utils.pushPage
* @see Utils.popPage
* @see Utils.getPageArgv
* @see Utils.getPageRetv
*/
static loadPage(page, tag, initialFocus, argv, retv) {
return new Promise(function (resolve, reject) {
Utils.cleanup();
Utils.lastScreenLoaded.page = page;
Utils.lastScreenLoaded.tag = tag;
Utils.lastScreenLoaded.initialFocus = initialFocus;
Utils.lastScreenLoaded.argv = argv;
Utils.lastScreenLoaded.retv = retv;
Utils.getHTML(page + '.html').then(function (text) {
if (tag)
$('#' + tag).html(text);
else
$('body').html(text);
Utils.rescan(); // does all the tag replacement
window.scrollTo(0, 0);
getScript(page + '.js').then(function () {
if (initialFocus) {
const ctl = $$(initialFocus);
if (ctl)
ctl.focus();
else
console.log("loadPage: can't set focus to unknown field " + initialFocus);
}
resolve();
}, function () {
reject();
});
}, function(err) {
console.log("loadPage: error loading " + pg);
reject();
});
});
}
/**
* Load a new screen but also remember what is loaded so that it can be returned to.
*
* @param {string} page path to the page to be loaded
* @param {string} tag optional ID of div to fill (if empty, "body" tag is used)
* @param {string} initialFocus optional, ID of control to set initial focus on
* @param {object} argv values being passed to the new screen
*
* @see Utils.popPage
* @see Utils.getPageArgv
* @see Utils.getPageRetv
* @see Utils.loadPage
*/
static pushPage(page, tag, initialFocus, argv) {
// push the previous screen with args
const stackFrame = {
path: Utils.lastScreenLoaded.page,
tag: Utils.lastScreenLoaded.tag,
initialFocus: Utils.initialFocus,
argv: Utils.lastScreenLoaded.argv
};
Utils.screenStack.push(stackFrame);
Utils.loadPage(page, tag, initialFocus, argv);
}
/**
* Re-load the prior screen.
* <br><br>
* Be careful. While this function loads the old screen, execution continues after this call
* and not in the new screen until this screen's code exits.
*
* @param {object} retv values being returned to the prior screen
* @param {number} howmany how many screens to go back (default 1)
*
* @see Utils.pushPage
* @see Utils.getPageArgv
* @see Utils.getPageRetv
*/
static popPage(retv, howmany=1) {
let stackFrame;
while (howmany--) {
if (!Utils.screenStack.length) {
console.log("Utils.popPage: no screen to pop");
return;
}
stackFrame = Utils.screenStack.pop();
}
Utils.loadPage(stackFrame.path, stackFrame.tag, stackFrame.initialFocus, stackFrame.argv, retv);
}
/**
* Convert an object into an array. This is especially useful for the <code>arguments</code> variable.
*
* @param obj
* @returns {array}
*/
static convertToArray(obj) {
const a = [];
for (let i=0 ; i < obj.length ; i++)
a.push(obj[i]);
return a;
}
/**
* Returns an object representing the values passed to the current screen by a parent screen.
* <br><br>
* This value remains constant through subsequent <code>pushPage</code> and <code>popPage</code> calls.
*
* @returns {object}
*
* @see Utils.pushPage
* @see Utils.popPage
* @see Utils.getPageRetv
*/
static getPageArgv() {
return Utils.lastScreenLoaded.argv;
}
/**
* Returns an object representing the return value from the child screen.
*
* @returns {object}
*
* @see Utils.pushPage
* @see Utils.popPage
* @see Utils.getPageArgv
*/
static getPageRetv() {
return Utils.lastScreenLoaded.retv;
}
/**
* Is the user's computer connected to the Internet?
*
* @returns {boolean}
*/
static isOnline() {
//return false; // used to simulate off-line condition
return navigator.onLine;
}
/**
* Create a new UUID.
*
* @returns {string}
*/
static uuid() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
/**
* This makes a window draggable.
*
* @param header jQuery object
* @param content jQuery object
*/
static makeDraggable(header, content) {
function handle_mousedown(e)
{
const body = $('body');
const drag = {};
drag.pageX0 = e.pageX;
drag.pageY0 = e.pageY;
drag.elem = content;
drag.offset0 = $(this).offset();
function handle_dragging(e) {
const left = drag.offset0.left + (e.pageX - drag.pageX0);
const top = drag.offset0.top + (e.pageY - drag.pageY0);
$(drag.elem)
.offset({top: top, left: left});
}
function handle_mouseup(e) {
body
.off('mousemove')
.off('mouseup');
}
body.on('mouseup', handle_mouseup)
.on('mousemove', handle_dragging);
}
// for mobile devices
function handle_touchstart(e)
{
const body = $('body');
const drag = {};
e.preventDefault();
drag.pageX0 = e.originalEvent.touches[0].clientX;
drag.pageY0 = e.originalEvent.touches[0].clientY;
drag.elem = content;
drag.offset0 = $(this).offset();
function handle_dragging(e) {
const left = drag.offset0.left + (e.originalEvent.touches[0].clientX - drag.pageX0);
const top = drag.offset0.top + (e.originalEvent.touches[0].clientY - drag.pageY0);
$(drag.elem)
.offset({top: top, left: left});
}
function handle_mouseup(e) {
body
.off('touchmove')
.off('touchend');
}
body
.on('touchmove', handle_dragging)
.on('touchend', handle_mouseup);
}
header.css('cursor', 'all-scroll');
header.on('mousedown', handle_mousedown);
header.on('touchstart', handle_touchstart);
}
/**
* Open a modal popup window identified by id <code>id</code>.
* <br><br>
* If <code>replace</code> is used and there isn't a prior popup, it just acts like a plain open.
* So, if the popup is being used as a wizard, all the opens should set this to <code>true</code>.
*
* @param {string} id the id of the popup to evoke
* @param {string} focus_ctl optional, control to set initial focus
* @param {boolean} replace optional, if true, replace prior popup with this popup at the same coordinates
*
* @see Utils.popup_close
*/
static popup_open(id, focus_ctl=null, replace = false) {
const w = $('#' + id);
if (!w.length)
throw new Error(`Popup ${id} not found.`);
else if (w.length > 1)
throw new Error(`Popup ${id} found more than once.`);
let prior_offset = null;
if (replace && Utils.popup_context.length) {
const prior_context = Utils.popup_context[Utils.popup_context.length - 1];
const prior_id = prior_context.id;
const prior_w = $('#' + prior_id);
const prior_content = prior_w.children();
prior_offset = $(prior_content).offset();
Utils.popup_close();
}
let content;
let both_parts;
let header;
let body;
Utils.popup_context.push({
id: id,
globalEnterHandler: Utils.globalEnterHandler(null)
});
if (typeof AGGrid !== 'undefined')
AGGrid.newGridContext();
if (typeof Editor !== 'undefined')
Editor.newEditorContext();
Utils.newEnterContext();
if (!w.hasClass('popup-background')) {
let width = w.css('width');
let height = w.css('height');
w.addClass('popup-background');
w.css('z-index', Utils.popup_zindex++);
w.css('width', '100%');
w.css('height', '100%');
w.wrapInner('<div></div>');
content = w.children();
content.addClass('popup-content');
content.css('z-index', Utils.popup_zindex++);
content.attr('id', id + '--width');
both_parts = content.children();
header = both_parts.first();
header.addClass('popup-header');
body = header.next();
body.addClass('popup-body');
body.attr('id', id + '--height');
content.css('width', width);
body.css('height', height);
} else {
w.css('z-index', Utils.popup_zindex++);
content = w.children();
content.css('z-index', Utils.popup_zindex++);
both_parts = content.children();
header = both_parts.first();
body = header.next();
}
w.show();
if (prior_offset)
$(content).offset(prior_offset);
this.makeDraggable(header, content);
if (focus_ctl) {
const fctl = $('#' + focus_ctl);
if (fctl.length)
fctl.focus();
else
console.log("popup_open: can't set focus to nonexistent field " + focus_ctl);
} else {
const ctl = $(':focus');
if (ctl)
ctl.blur();
}
}
/**
* Dynamically change the height of a popup.
* Only works after a popup is open.
*
* @param {string} id popup id
* @param {string} height like "200px"
*/
static popup_set_height(id, height) {
const ctl = $('#' + id + '--height');
if (ctl.length)
ctl.css('height', height);
else
console.log("Utils.popup_set_height: can't set height before popup " + id + " is open");
}
/**
* Dynamically change the width of a popup.
* Only works after a popup is open.
*
* @param {string} id popup id
* @param {string} width like "200px"
*/
static popup_set_width(id, width) {
const ctl = $('#' + id + '--width');
if (ctl.length)
ctl.css('width', width);
else
console.log("Utils.popup_set_width: can't set width before popup " + id + " is open");
}
/**
* Close the most recent modal popup.
*
* @see Utils.popup_open
*/
static popup_close() {
const context = Utils.popup_context.pop();
if (typeof AGGrid !== 'undefined')
AGGrid.popGridContext();
if (typeof Editor !== 'undefined')
Editor.popEditorContext();
Utils.popEnterContext();
$('#' + context.id).hide();
Utils.globalEnterHandler(context.globalEnterHandler);
Utils.popup_zindex -= 2;
if (Utils.popup_zindex < 10)
Utils.popup_zindex = 10;
}
/**
* Return the number of files the user selected for upload.
*
* @param {string} id the id of the control
* @returns {number}
*
* @see Server.fileUploadSend
* @see Utils.getFileUploadFormData
*/
static getFileUploadCount(id) {
const ctl = $('#'+id);
const file_list = ctl.get(0).files;
return file_list.length;
}
/**
* Creates and initializes a FormData instance used for file uploading.
* Before the FormData instance is sent, arbitrary additional data may be added
* by using <code>fd.append(name, data)</code>
* <br><br>
* where <code>fd</code> is the object return by this call.
*
* @param {string} id the id of the control
* @returns {FormData}
*
* @see Server.fileUploadSend
* @see Utils.getFileUploadCount
*/
static getFileUploadFormData(id) {
const ctl = $('#'+id);
const file_list = ctl.get(0).files;
const data = new FormData();
for (let i=0 ; i < file_list.length ; i++)
data.append('file-'+i, file_list[i]);
return data;
}
/**
* Turn undefined, null, NaN, "", a number, or a string into a number.
* Handles M and K suffix.
* Anything (except a valid string or number) becomes a zero.
*
* @param v
* @returns {number}
*/
static toNumber(v) {
/**
* Removes the currency symbol from the beginning of the given amount string.
*
* @param {string} amountString - The amount string from which to remove the currency symbol.
* @return {string} The amount string with the currency symbol removed.
*/
function removeCurrencySymbol(amountString) {
// Expanded list of common currency symbols
const symbols = [
'$', '€', '£', '¥', '₹', '₽', 'R', '₺', 'A$', 'C$', 'NZ$', 'S$', 'HK$', 'kr', 'Mex$', 'Fr',
'CHF', 'SEK', 'DKK', 'NOK', 'THB', '₪', '₫', '₱', 'RM', 'zł', 'Kč', 'HUF', '₡', '₲',
'Q', '₸', '₼', 'лв', '₴', '₾', '֏', '₼', 'Bs.', '₭', '₦', '₨', '₲', '₵', '₸'
];
// Remove the symbol if it is present
for (let symbol of symbols)
if (amountString.startsWith(symbol))
return amountString.replace(symbol, '').trim();
// Return the original string if no symbol was found
return amountString;
}
/**
* Detects the grouping separator used in numeric formatting based on the specified locale.
*
* @param {string} [locale=navigator.language] - The locale to use for formatting the number. Defaults to the local locale.
* @return {string} The grouping separator character used in numeric formatting, or an empty string if not found.
*/
function detectGroupingSeparator(locale = navigator.language) {
if (Utils.numericGroupCharacter === undefined) {
const largeNumber = 1234567; // Use a number that will definitely have grouping separators
const formattedNumber = new Intl.NumberFormat(locale).format(largeNumber);
// Find the character that is not a digit and comes after the first digit (which will be the grouping separator)
for (let i = 0; i < formattedNumber.length; i++) {
if (!/\d/.test(formattedNumber[i]) && /\d/.test(formattedNumber[i - 1])) {
Utils.numericGroupCharacter = formattedNumber[i];
break;
}
}
if (Utils.numericGroupCharacter === undefined)
Utils.numericGroupCharacter = '';
}
return Utils.numericGroupCharacter;
}
if (typeof v === 'number')
return isNaN(v) ? 0 : v;
if (typeof v !== 'string' || !v.trim())
return 0;
v = removeCurrencySymbol(v).replaceAll('[ %]', '').replaceAll(detectGroupingSeparator(), '');
let kilo, mega;
if (kilo = (/k/i.test(v)))
v = v.replace(/k/i, '');
if (mega = (/m/i.test(v)))
v = v.replace(/m/i, '');
const r = Number(v);
return isNaN(r) ? 0 : r * (kilo ? 1000 : mega ? 1000000 : 1);
}
/**
* Converts a potentially non-ASCII string to ASCII.
*
* @param str
* @returns {str}
*/
static toASCII(str) {
if (!str)
return str;
str = str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); // first, try to remove accents
return str.replace(/[^\x00-\x7F]/g, ""); // then, remove any remaining non-ASCII characters
}
/**
* Displays a report given a URL from the back-end.
* Correctly handles dual server development situations.
*
* @param url {string} report url
*/
static showReport(url) {
if (!url) {
console.log("Utils.showReport: no url provided");
return;
}
let path;
if (url.charAt(0) !== '/')
url = '/' + url;
if (window.location.href.search("localhost:8000") !== -1 ||
window.location.href.search("localhost:8001") !== -1 ||
window.location.href.search("localhost:8002") !== -1 ||
window.location.href.search("localhost:63342") !== -1 ||
window.location.href.search("localhost:63340") !== -1) // if debugging with a local server
path = "http://localhost:8080" + url;
else {
const server = Server.url;
let ns = 0; // number of slashes
let ts = 0; // index of third slash
for (let i=0 ; i < server.length ; i++)
if (server.charAt(i) === '/')
if (++ns === 3) {
ts = i;
break;
}
path = ts ? server.substr(ts) + url : url;
}
window.open(path, "_blank");
}
//--------------------------
/**
* Returns true if the key hit is the kind of keyboard character that changes the value.
* (Textarea will need to check 'Enter' too.)
*
* @param event
* @returns {boolean}
*/
static isChangeChar(event) {
if (!event || !event.key)
return false;
return !(event.ctrlKey && event.key !== 'v') && (event.key.length === 1 || event.key === 'Backspace' || event.key === 'Delete');
}
/**
* This function is called to indicate to the system that <em>some</em> control value changed by the user.
* Programmatic changed to not set this.
* It is mainly used internally as all the Kiss controls use this function.
* <br><br>
* If there is a function connected to some-control-value-changing via the setSomeControlValueChangeFunction
* function, it will be executed the <em>first time only</em> that any control value is changed.
* Subsequent changes will not trigger the some-control-value-changing function.
* The some-control-value-changing function is passed the single argument <code>true</code>.
* <br><br>
* Non-Kiss controls should call this function if their value changes.
*
*/
static someControlValueChanged() {
if (!Utils.someControlValueChangedFlag) {
Utils.someControlValueChangedFlag = true;
if (Utils.someControlValueChangedFun)
Utils.someControlValueChangedFun(Utils.someControlValueChangedFlag);
}
}
/**
* This function clears the state of some control value having been changed.
* <br><br>
* Also, if a function is attached (via the setSomeControlValueChangeFunction function),
* it will be executed if some value had changed and a <code>true</code> argument is passed
* to this function. The some-control-value-changed function will be passed a <code>false</code>.
*
* @param executeFunction optional boolean
*/
static clearSomeControlValueChanged(executeFunction = true) {
if (Utils.someControlValueChangedFlag) {
Utils.someControlValueChangedFlag = false;
if (Utils.someControlValueChangedFun && executeFunction)
Utils.someControlValueChangedFun(Utils.someControlValueChangedFlag);
}
}
/**
*
* @returns {boolean} true if any control value changed, false otherwise
*/
static didSomeControlValueChange() {
return Utils.someControlValueChangedFlag;
}
/**
* Used for setting an application specific function to be executed the first
* time any control value is changed by the user or if the change status is cleared.
* Other programmatic changes do not trigger this condition.
* <br><br>
* A <code>null</code> value may be passed in order to clear the function.
* <br><br>
* The function is passed a single boolean value. <code>true</code> means
* a control value has changed, and <code>false</code> if the state is
* being reset.
*
* @param fun {function} the function to be executed
*/
static setSomeControlValueChangeFunction(fun) {
Utils.someControlValueChangedFun = fun;
}
/**
* Count properties associated with an object.
*
* @param obj
* @returns {number}
*/
static countProperties(obj) {
return Object.keys(obj).length;
}
/**
* Perform a shallow clone on an array.
*
* @param ary
* @returns {Array} shallow copy of ary
*/
static cloneArrayShallow(ary) {
return [...ary];
}
/**
* Set a global handler for the enter/return key.
* If fun is null, the enter function is cancelled.
*
* @param {function} fun the new enter function handler or null
* @return {function} the previous enter function or null if none
*/
static globalEnterHandler(fun) {
const prevFun = Utils.globalEnterFunction;
Utils.globalEnterFunction = fun;
const obj = $('body');
obj.off('keyup');
if (fun)
obj.on('keyup', function (e) {
if (e.key === 'Enter')
fun();
});
return prevFun;
}
/**
* Perform all cleanup operations between screens
*/
static cleanup() {
Utils.clearSomeControlValueChanged(false);
if (typeof Kiss !== 'undefined' && typeof Kiss.RadioButtons !== 'undefined')
Kiss.RadioButtons.resetGroups();
if (typeof AGGrid !== 'undefined') {
AGGrid.popAllGridContexts();
AGGrid.newGridContext(); // for the new screen we are loading
}
if (typeof Editor !== 'undefined') {
Editor.popAllEditorContexts();
Editor.newEditorContext(); // for the new screen we are loading
}
Utils.clearAllEnterContexts();
Utils.newEnterContext();
Utils.globalEnterHandler(null);
Utils.popup_context = [];
const ctl = $(':focus'); // remove any focus
if (ctl)
ctl.blur();
}
/**
* Create a new enter key context.
*/
static newEnterContext() {
Utils.enterFunctionStack.push(Utils.enterFunction);
Utils.enterFunction = null;
};
/**
* Destroy all grids in last context and remove the context
*/
static popEnterContext() {
if (Utils.enterFunctionStack.length)
Utils.enterFunction = Utils.enterFunctionStack.pop();
else
Utils.enterFunction = null;
};
/**
* Clear all enter key contexts that have been created
*/
static clearAllEnterContexts() {
Utils.enterFunction = null;
Utils.enterFunctionStack = [];
};
/**
* Function to execute if the user hits the enter key
*
* @param fun
*/
static setEnterFunction(fun) {
Utils.enterFunction = fun;
}
/**
* Converts a text string into a string suitable to HTML.
*
* @param text
* @returns {string}
*/
static textToHtml(text) {
return text
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'")
.replace(/\n/g, '<br>')
// .replace(/ /g, ' ') kills text wrapping
;
}
/**
* Convert an HTML string into a text string.
* Also convert Unicode to ASCII when possible.
*
* @param html
* @returns {string}
*/
static htmlToText(html) {
if (!html)
return '';
return html
.replace(/<br *[^>]*>/g, '\n')
// iPhone uses Unicode! Convert to ASCII.
.replace(/\u2018/g, "'")
.replace(/\u2019/g, "'")
.replace(/\u201B/g, "'")
.replace(/\u201C/g, '"')
.replace(/\u201F/g, '"')
.replace(/\u201D/g, '"')
.replace(/\u275D/g, '"')
.replace(/\u275E/g, '"')
.replace(/\u301D/g, '"')
.replace(/\u301E/g, '"')
.replace(/\u275B/g, "'")
.replace(/\u275C/g, "'")
.replace(/<div *[^>]*>/g, '\n')
.replace(/<\/div>/g, '')
.replace(/<span *[^>]*>/g, '')
.replace(/<\/span>/g, '')
.replace(/<h1 *[^>]*>/g, '')
.replace(/<\/h1>/g, '')
.replace(/<font *[^>]*>/g, '')
.replace(/<\/font>/g, '')
.replace(/<[ap] *[^>]*>/g, '')
.replace(/<\/[ap]>/g, '')
// these need to be last
.replace(/&sp;/g, ' ')
.replace(/ /g, ' ')
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, "'")
;
}
/**
* Append html (as text) to the list of children of a node.
* <br><br>
* @code{tag} can be the id of the control or its jQuery node.
* This function returns the jQuery node that can be used
* for additional calls to this function (so it'll be faster).
*
* @param tag {object|string} see above
* @param html {string} the html to be
* @returns {object} the jQuery node
*/
static appendChild(tag, html) {
let node;
if (typeof tag === 'string') {
node = $('#' + tag);
if (!node || !node.length) {
console.log('tag ' + tag +' not found.');
return;
}
} else
node = tag;
node.append(html);
return node;
}
/**
* Erase all the child nodes.
* <br><br>
* @code{tag} can be the id of the control or its jQuery node.
* This function returns the jQuery node that can be used
* for additional calls to this function.
*
* @param tag {object|string} see above
* @returns {object} the jQuery node
*/
static eraseChildren(tag) {
let node;
if (typeof tag === 'string') {
node = $('#' + tag);
if (!node || !node.length) {
console.log('tag ' + tag +' not found.');
return;
}
} else
node = tag;
node.empty();
return node;
}
/**
* Return number of milliseconds since 1970 UTC from a date and time control.
*
* @param {string} dateField the ID of the date field
* @param {string} timeField the ID of the time field
* @returns {number}
*/
static getMilliseconds(dateField, timeField) {
return DateTimeUtils.toMilliseconds($$(dateField).getIntValue(), $$(timeField).getValue());
}
/**
* Get current location.
*
* @returns {Promise<GeolocationCoordinates>} or null
*/
static getLocation() {
return new Promise(function(resolve, reject) {
const getPos = function (p) {
resolve(p.coords);
};
const handleError = function(error) {
resolve(null);
};
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(getPos, handleError);
} else
resolve(null);
});
}
/**
* Toggle full screen mode. This is mainly good for tablets and phones.
* It doesn't make sense for desktops or laptops.
* <br><br>
* You can tell if it is a tablet or phone via:
* <code>if (screen.width * screen.height < 1000000) ...</code>
* <br><br>
* This function only works when it is attached to a button on the screen.
* This is a restriction placed on us by the browsers to assure the user
* is making the choice.
* <br><br>
* Note also that this does not work on an iPad. Apple doesn't allow it.
* They also make sure Chrome on an iPad doesn't work. Presumably this
* is to assure that browser apps don't compete against their app store.
* <br><br>
* This does work on Android devices.
*/
static toggleFullscreen() {
if (!Utils.isFullScreen) {
const element = document.documentElement;
// Check which implementation is available
const requestMethod = element.requestFullScreen ||
element.webkitRequestFullscreen ||
element.mozRequestFullScreen ||
element.msRequestFullscreen;
if (requestMethod)
requestMethod.apply(element);
Utils.isFullScreen = true;
} else {
if (document.exitFullscreen)
document.exitFullscreen();
else if (document.webkitExitFullscreen) /* Safari */
document.webkitExitFullscreen();
else if (document.mozExitFullscreen)
document.mozExitFullscreen();
else if (document.msExitFullscreen) /* IE11 */
document.msExitFullscreen();
Utils.isFullScreen = false;
}
}
/**
* Save an application global data item associated with a key.
*
* @param {string} key
* @param {*} val
*
* @see Utils.getData
* @see Utils.getAndEraseData
*/
static saveData(key, val) {
Utils.globalData[key] = val;
};
/**
* Retrieve the application global data associated with a key.
*
* @param {string} key
* @returns {*}
*
* @see Utils.saveData
* @see Utils.getAndEraseData
*/
static getData(key) {
return Utils.globalData[key];
};
/**
* Erase an application global data item returning its prior value.
*
* @param {string} key
* @returns {*} the value before it was erased
*
* @see Utils.saveData
* @see Utils.getData
*/
static getAndEraseData(key) {
const r = Utils.globalData[key];
delete Utils.globalData[key];
return r;
};
/**
* Convert a byte array to base64.
* <br><br>
* Taken from https://gist.github.com/jonleighton/958841
* @param bytes
* @returns {string}
*
* @see Server.binaryCall
*/
static toBase64(bytes) {
let base64 = '';
const encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
const byteLength = bytes.byteLength;
const byteRemainder = byteLength % 3;
const mainLength = byteLength - byteRemainder;
let a, b, c, d;
let chunk;
// Main loop deals with bytes in chunks of 3
for (let i = 0; i < mainLength; i = i + 3) {
// Combine the three bytes into a single integer
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
// Use bitmasks to extract 6-bit segments from the triplet
a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
d = chunk & 63; // 63 = 2^6 - 1
// Convert the raw binary segments to the appropriate ASCII encoding
base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
}
// Deal with the remaining bytes and padding
if (byteRemainder === 1) {
chunk = bytes[mainLength];
a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
// Set the 4 least significant bits to zero
b = (chunk & 3) << 4; // 3 = 2^2 - 1
base64 += encodings[a] + encodings[b] + '==';
} else if (byteRemainder === 2) {
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
// Set the 2 least significant bits to zero
c = (chunk & 15) << 2; // 15 = 2^4 - 1
base64 += encodings[a] + encodings[b] + encodings[c] + '=';
}
return base64;
}
/**
* Formats a social security number into a standard format.
* If it is an invalid SSN, whatever is passed in is returned.
*
* @param s {string|null}
* @returns {string|null}
*/
static formatSsn(s) {
if (!s)
return null;
const n = s.replace(/\D/g, '');
if (n.length !== 9)
return s;
return n.substring(0, 3) + '-' + n.substring(3, 5) + '-' + n.substring(5, 9);
}
/**
* Return <code>true</code> if the passed in string is a valid SSN.
*
* @param s {string|null}
* @returns {boolean}
*/
static isValidSsn(s) {
return s && s.replace(/\D/g, '').length === 9;
}
/**
* This method attempts to correct incorrect word capitalization.
* It tries to assure that the first letter of each word is uppercase and the rest is lowercase.
* If the incoming string is already mixed-case, it leaves it alone.
*
* @param s {string}
* @returns {string}
*/
static fixCapitalization(str) {
if (!str)
return str;
const nUpper = str.replace(/[^A-Z]/g, '').length;
const nLower = str.replace(/[^a-z]/g, '').length;
if (!(nLower + nUpper) || nLower && nUpper)
return str;
// Otherwise, capitalize the first letter of each word
return str.toLowerCase().replace(/(^|\s)\S/g, function(firstLetter) {
return firstLetter.toUpperCase();
});
}
}
// Class variables
Utils.count = 1;
Utils.popup_zindex = 10;
Utils.popup_context = [];
Utils.someControlValueChangedFlag = false;
Utils.someControlValueChangedFun = null;
Utils.globalEnterFunction = null;
Utils.isFullScreen = false;
Utils.enterFunction = null; // If defined, execute function when enter key hit
Utils.enterFunctionStack = []; // Save stack for enter key to handle popups
Utils.screenStack = [];
Utils.lastScreenLoaded = {}; // current stackframe
Utils.suspendDepth = 0; // when > 0 suspend buttons
Utils.globalData = {};
/**
* This is a stack for the <code>Utils.waitMessage</code> method.
* Each element contains a string which is the message at each nesting level.
*/
Utils.waitMessageStack = [];
/**
* <code>forceASCII</code> forces ASCII representation of all text input.
* This solves the problem of Unicode characters being too long for an ASCII SQL column.
* If your database uses Unicode, then this should be set to <code>false</code>.
* If set to <code>true</code>, it should be set elsewhere so that this file can remain
* untouched.
*
* @type {boolean}
*/
Utils.forceASCII = false;
$(document).on('keypress', function(e) {
if (Utils.enterFunction && e.key === 'Enter')
Utils.enterFunction();
});
// taken from https://github.com/accursoft/caret/blob/master/jquery.caret.js
(function($) {
function focus(target) {
if (!document.activeElement || document.activeElement !== target) {
target.focus();
}
}
$.fn.caret = function(pos) {
let target = this[0];
let isContentEditable = target && target.contentEditable === 'true';
if (arguments.length === 0) {
//get
if (target) {
//HTML5
if (window.getSelection) {
//contenteditable
if (isContentEditable) {
focus(target);
let selection = window.getSelection();
// Opera 12 check
if (!selection.rangeCount) {
return 0;
}
let range1 = selection.getRangeAt(0),
range2 = range1.cloneRange();
range2.selectNodeContents(target);
range2.setEnd(range1.endContainer, range1.endOffset);
return range2.toString().length;
}
//textarea
return target.selectionStart;
}
//IE<9
if (document.selection) {
focus(target);
//contenteditable
if (isContentEditable) {
let range1 = document.selection.createRange(),
range2 = document.body.createTextRange();
range2.moveToElementText(target);
range2.setEndPoint('EndToEnd', range1);
return range2.text.length;
}
//textarea
let pos = 0,
range = target.createTextRange(),
range2 = document.selection.createRange().duplicate(),
bookmark = range2.getBookmark();
range.moveToBookmark(bookmark);
while (range.moveStart('character', -1) !== 0) pos++;
return pos;
}
// Addition for jsdom support
if (target.selectionStart)
return target.selectionStart;
}
//not supported
return;
}
//set
if (target) {
if (pos === -1)
pos = this[isContentEditable? 'text' : 'val']().length;
//HTML5
if (window.getSelection) {
//contenteditable
if (isContentEditable) {
focus(target);
window.getSelection().collapse(target.firstChild, pos);
}
//textarea
else
target.setSelectionRange(pos, pos);
}
//IE<9
else if (document.body.createTextRange) {
if (isContentEditable) {
let range = document.body.createTextRange();
range.moveToElementText(target);
range.moveStart('character', pos);
range.collapse(true);
range.select();
} else {
let range = target.createTextRange();
range.move('character', pos);
range.select();
}
}
if (!isContentEditable)
focus(target);
}
return this;
};
})(jQuery);