//****************************************************************************************************** // gsf.web.client.js - Gbtc // // Copyright © 2016, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 01/26/2016 - J. Ritchie Carroll // Generated original version of source code. // //****************************************************************************************************** // ReSharper disable NativeTypePrototypeExtending // ReSharper disable UndeclaredGlobalVariableUsing // Grid Solutions Framework Core Web Client Script Functions "use strict"; const isIE = detectIE(); var textMetrics; $(function() { // Create a canvas object that will be used for text metrics calculations $("").appendTo("body"); // Get text metrics canvas context textMetrics = document.getElementById("textMetricsCanvas").getContext("2d"); }); // Miscellaneous functions function notNull(value, nonNullValue) { return value || (nonNullValue || ""); } function detectIE() { const ua = window.navigator.userAgent; const msie = ua.indexOf("MSIE "); if (msie > 0) { // IE 10 or older => return version number return parseInt(ua.substring(msie + 5, ua.indexOf(".", msie)), 10); } const trident = ua.indexOf("Trident/"); if (trident > 0) { // IE 11 => return version number const rv = ua.indexOf("rv:"); return parseInt(ua.substring(rv + 3, ua.indexOf(".", rv)), 10); } const edge = ua.indexOf("Edge/"); if (edge > 0) { // Edge (IE 12+) => return version number return parseInt(ua.substring(edge + 5, ua.indexOf(".", edge)), 10); } // Other browser return false; } function isNumber(val) { return !isNaN(parseFloat(val)) && isFinite(val); } function toHex(val, leftPadding) { if (leftPadding === undefined) leftPadding = 4; const isNegative = val < 0; return (isNegative ? "-" : "") + "0x" + Math.abs(val).toString(16).padLeft(leftPadding, "0").toUpperCase(); } function isBool(val) { if (typeof value === "boolean") return true; const lval = val.toString().toLowerCase(); return lval === "true" || lval === "false"; } function getBool(val) { const num = +val; return !isNaN(num) ? !!num : !!String(val).toLowerCase().replace(false, ""); } // Number functions Number.prototype.truncate = function() { if (typeof Math.trunc != "function") return parseInt(this.toString()); return Math.trunc(this); } Number.prototype.padLeft = function(totalWidth, paddingChar) { return this.truncate().toString().padLeft(totalWidth, paddingChar || "0"); } Number.prototype.padRight = function(totalWidth, paddingChar) { return this.truncate().toString().padRight(totalWidth, paddingChar || "0"); } // Array functions // Combines a dictionary of key-value pairs into a string. Values will be escaped within startValueDelimiter and endValueDelimiter // to contain nested key/value pair expressions like the following: "normalKVP=-1; nestedKVP={p1=true; p2=0.001}" when either the // parameterDelimiter or the keyValueDelimiter are detected in the value of the key/value pair. function joinKeyValuePairs (source, parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter) { if (!parameterDelimiter) parameterDelimiter = ";"; if (!keyValueDelimiter) keyValueDelimiter = "="; if (!startValueDelimiter) startValueDelimiter = "{"; if (!endValueDelimiter) endValueDelimiter = "}"; const values = []; for (let key in source) { if (source.hasOwnProperty(key)) { let value = source[key]; if (isBool(value)) value = value.toString().toLowerCase(); else value = value ? value.toString() : ""; if (value.indexOf(parameterDelimiter) >= 0 || value.indexOf(keyValueDelimiter) >= 0) value = startValueDelimiter + value + endValueDelimiter; values.push(key + keyValueDelimiter + value); } } return values.join(parameterDelimiter + " "); } Array.prototype.joinKeyValuePairs = function(parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter) { return joinKeyValuePairs(this, parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter); }; Array.prototype.forEachWithDelay = function(iterationCallback, timeout, completedCallback, thisArg) { var index = 0, count = this.length, self = this, next = function() { if (self[index]) iterationCallback.call(thisArg || self, self[index], index, self); index++; if (index < count) setTimeout(next, timeout); else completedCallback.call(thisArg || self, self); }; next(); }; if (!Array.prototype.any) { if (Array.prototype.some) { Array.prototype.any = Array.prototype.some; } else { Array.prototype.any = function(callback, thisArg) { var args; if (this == null) throw new TypeError("this is null or not defined"); const array = Object(this); const length = array.length >>> 0; // Convert array length to positive integer if (typeof callback !== "function") throw new TypeError(); if (arguments.length > 1) args = thisArg; else args = undefined; var index = 0; while (index < length) { if (index in array) { const element = array[index]; if (callback.call(args, element, index, array)) return true; } index++; } return false; } } } // Represents a dictionary style class with case-insensitive keys function Dictionary(source) { const self = this; if (!source) source = []; self._keys = []; self._values = []; self.count = function() { var size = 0; for (let property in self._values) { if (self._values.hasOwnProperty(property)) size++; } return size; } self.keys = function() { const keys = []; for (let property in self._keys) { if (self._keys.hasOwnProperty(property)) keys.push(self._keys[property]); } return keys; } self.values = function() { const values = []; for (let property in self._values) { if (self._keys.hasOwnProperty(property)) values.push(self._values[property]); } return values; } self.get = function(key) { return self._values[String(key).toLowerCase()]; } self.set = function(key, value) { const lkey = String(key).toLowerCase(); if (!self._keys[lkey] || self._keys[lkey].toLowerCase() !== key) self._keys[lkey] = key; if (isBool(value)) self._values[lkey] = getBool(value); else self._values[lkey] = value; } self.remove = function(key) { const lkey = String(key).toLowerCase(); delete self._keys[lkey]; delete self._values[lkey]; } self.containsKey = function(key) { const lkey = String(key).toLowerCase(); for (let property in self._values) { if (self._values.hasOwnProperty(property) && property === lkey) return true; } return false; } self.containsValue = function(value) { for (let property in self._values) { if (self._values.hasOwnProperty(property) && self._values[property] === value) return true; } return false; } self.clear = function() { self._keys = []; self._values = []; } self.joinKeyValuePairs = function(parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter) { const keyValuePairs = []; for (let property in self._values) { if (self._values.hasOwnProperty(property)) keyValuePairs[self._keys[property]] = self._values[property]; } return keyValuePairs.joinKeyValuePairs(parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter); } self.pushAll = function(source) { for (let property in source) if (source.hasOwnProperty(property)) self.set(property, source[property]); } self.toObservableDictionary = function(useLowerKeys) { // See ko.observableDictionary.js const observableDictionary = new ko.observableDictionary(); for (let property in self._values) { if (self._values.hasOwnProperty(property)) observableDictionary.push(useLowerKeys ? property : self._keys[property], self._values[property]); } return observableDictionary; } self.updateObservableDictionary = function(observableDictionary, useLowerKeys) { for (let property in self._values) { if (self._values.hasOwnProperty(property)) observableDictionary.set(useLowerKeys ? property : self._keys[property], self._values[property]); } } // Construction if (source instanceof Dictionary) { for (let property in source._values) if (source._values.hasOwnProperty(property)) self.set(source._keys[property], source._values[property]); } else { for (let property in source) { if (source.hasOwnProperty(property)) self.set(property, source[property]); } } } Dictionary.fromObservableDictionary = function(observableDictionary) { const dictionary = new Dictionary(); dictionary.pushAll(observableDictionary.toJSON()); return dictionary; } // String functions function isEmpty(str) { return !str || String(str).length === 0; } String.prototype.truncate = function(limit) { const text = this.trim(); if (text.length > limit) return text.substr(0, limit - 3) + "..."; return text; } String.prototype.replaceAll = function(findText, replaceWith, ignoreCase) { return this.replace( new RegExp(findText.replace(/([\/\,\!\\\^\$\{\}\[\]\(\)\.\*\+\?\|\<\>\-\&])/g, "\\$&"), (ignoreCase ? "gi" : "g")), (typeof replaceWith == "string") ? replaceWith.replace(/\$/g, "$$$$") : replaceWith); } if (!String.format) { String.format = function(format) { var text = format; for (let i = 1; i < arguments.length; i++) text = text.replaceAll("{" + (i - 1) + "}", arguments[i]); return text; }; } String.prototype.padLeft = function(totalWidth, paddingChar) { if (totalWidth > this.length) return Array(totalWidth - this.length + 1).join(paddingChar || " ") + this; return this; } String.prototype.padRight = function(totalWidth, paddingChar) { if (totalWidth > this.length) return this + Array(totalWidth - this.length + 1).join(paddingChar || " "); return this; } String.prototype.countOccurrences = function(searchString) { return (this.split(searchString).length - 1); } if (!String.prototype.startsWith) { String.prototype.startsWith = function(searchString, position) { position = position || 0; return this.substr(position, searchString.length) === searchString; } } String.prototype.regexEncode = function() { return "\\u" + this.charCodeAt(0).toString(16).padLeft(4, "0"); } // Returns a Dictionary of the parsed key/value pair expressions from a string. Parameter pairs are delimited by keyValueDelimiter // and multiple pairs separated by parameterDelimiter. Supports encapsulated nested expressions. String.prototype.parseKeyValuePairs = function(parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter, ignoreDuplicateKeys) { if (!parameterDelimiter) parameterDelimiter = ";"; if (!keyValueDelimiter) keyValueDelimiter = "="; if (!startValueDelimiter) startValueDelimiter = "{"; if (!endValueDelimiter) endValueDelimiter = "}"; if (ignoreDuplicateKeys === undefined) ignoreDuplicateKeys = true; if (parameterDelimiter === keyValueDelimiter || parameterDelimiter === startValueDelimiter || parameterDelimiter === endValueDelimiter || keyValueDelimiter === startValueDelimiter || keyValueDelimiter === endValueDelimiter || startValueDelimiter === endValueDelimiter) throw "All delimiters must be unique"; const escapedParameterDelimiter = parameterDelimiter.regexEncode(); const escapedKeyValueDelimiter = keyValueDelimiter.regexEncode(); const escapedStartValueDelimiter = startValueDelimiter.regexEncode(); const escapedEndValueDelimiter = endValueDelimiter.regexEncode(); const backslashDelimiter = "\\".regexEncode(); var keyValuePairs = new Dictionary(); var escapedValue = []; var valueEscaped = false; var delimiterDepth = 0; // Escape any parameter or key/value delimiters within tagged value sequences // For example, the following string: // "normalKVP=-1; nestedKVP={p1=true; p2=false}") // would be encoded as: // "normalKVP=-1; nestedKVP=p1\\u003dtrue\\u003b p2\\u003dfalse") for (let i = 0; i < this.length; i++) { var character = this[i]; if (character === startValueDelimiter) { if (!valueEscaped) { valueEscaped = true; continue; // Don't add tag start delimiter to final value } // Handle nested delimiters delimiterDepth++; } if (character === endValueDelimiter) { if (valueEscaped) { if (delimiterDepth > 0) { // Handle nested delimiters delimiterDepth--; } else { valueEscaped = false; continue; // Don't add tag stop delimiter to final value } } else { throw "Failed to parse key/value pairs: invalid delimiter mismatch. Encountered end value delimiter \"" + endValueDelimiter + "\" before start value delimiter \"" + startValueDelimiter + "\"."; } } if (valueEscaped) { // Escape any delimiter characters inside nested key/value pair if (character === parameterDelimiter) escapedValue.push(escapedParameterDelimiter); else if (character === keyValueDelimiter) escapedValue.push(escapedKeyValueDelimiter); else if (character === startValueDelimiter) escapedValue.push(escapedStartValueDelimiter); else if (character === endValueDelimiter) escapedValue.push(escapedEndValueDelimiter); else if (character === "\\") escapedValue.push(backslashDelimiter); else escapedValue.push(character); } else { if (character === "\\") escapedValue.push(backslashDelimiter); else escapedValue.push(character); } } if (delimiterDepth !== 0 || valueEscaped) { // If value is still escaped, tagged expression was not terminated if (valueEscaped) delimiterDepth = 1; throw "Failed to parse key/value pairs: invalid delimiter mismatch. Encountered more " + (delimiterDepth > 0 ? "start value delimiters \"" + startValueDelimiter + "\"" : "end value delimiters \"" + endValueDelimiter + "\"") + " than " + (delimiterDepth < 0 ? "start value delimiters \"" + startValueDelimiter + "\"" : "end value delimiters \"" + endValueDelimiter + "\"") + "."; } // Parse key/value pairs from escaped value var pairs = escapedValue.join("").split(parameterDelimiter); for (let i = 0; i < pairs.length; i++) { // Separate key from value var elements = pairs[i].split(keyValueDelimiter); if (elements.length === 2) { // Get key var key = elements[0].trim(); // Get unescaped value var unescapedValue = elements[1].trim(). replaceAll(escapedParameterDelimiter, parameterDelimiter). replaceAll(escapedKeyValueDelimiter, keyValueDelimiter). replaceAll(escapedStartValueDelimiter, startValueDelimiter). replaceAll(escapedEndValueDelimiter, endValueDelimiter). replaceAll(backslashDelimiter, "\\"); // Add key/value pair to dictionary if (ignoreDuplicateKeys) { // Add or replace key elements with unescaped value keyValuePairs.set(key, unescapedValue); } else { // Add key elements with unescaped value throwing an exception for encountered duplicate keys if (keyValuePairs.containsKey(key)) throw "Failed to parse key/value pairs: duplicate key encountered. Key \"" + key + "\" is not unique within the string: \"" + this + "\""; keyValuePairs.set(key, unescapedValue); } } } return keyValuePairs; } // Renders URLs and e-mail addresses as clickable links function renderHotLinks(sourceText, target) { if (target === undefined) target = "_blank"; sourceText = sourceText.toString(); var replacedText; // URLs starting with http://, https://, or ftp:// const replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim; replacedText = sourceText.replace(replacePattern1, "$1"); // URLs starting with "www." without // before it const replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim; replacedText = replacedText.replace(replacePattern2, "$1$2"); // Change e-mail addresses to mailto: links const replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim; replacedText = replacedText.replace(replacePattern3, "$1"); return replacedText; } // Date Functions Date.prototype.addDays = function(days) { return new Date(this.setDate(this.getDate() + days)); } Date.prototype.toUTC = function() { this.setMinutes(this.getMinutes() - this.getTimezoneOffset()); return this; } Date.prototype.daysBetween = function(startDate) { const millisecondsPerDay = 24 * 60 * 60 * 1000; return (this.toUTC() - startDate.toUTC()) / millisecondsPerDay; } String.prototype.toDate = function() { return new Date(Date.parse(this)); }; String.prototype.formatDate = function(format, utc) { return formatDate(this.toDate(), format, utc); }; Date.prototype.formatDate = function(format, utc) { return formatDate(this, format, utc); }; function formatDate(date, format, utc) { if (typeof date === "string") return formatDate(date.toDate(), format, utc); if (format === undefined && DateTimeFormat != undefined) format = DateTimeFormat; if (utc === undefined) utc = true; var MMMM = ["\x00", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; var MMM = ["\x01", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; var dddd = ["\x02", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; var ddd = ["\x03", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; function ii(i, len) { var ss = i + ""; len = len || 2; while (ss.length < len) ss = "0" + ss; return ss; } var y = utc ? date.getUTCFullYear() : date.getFullYear(); format = format.replace(/(^|[^\\])yyyy+/g, "$1" + y); format = format.replace(/(^|[^\\])yy/g, "$1" + y.toString().substr(2, 2)); format = format.replace(/(^|[^\\])y/g, "$1" + y); var M = (utc ? date.getUTCMonth() : date.getMonth()) + 1; format = format.replace(/(^|[^\\])MMMM+/g, "$1" + MMMM[0]); format = format.replace(/(^|[^\\])MMM/g, "$1" + MMM[0]); format = format.replace(/(^|[^\\])MM/g, "$1" + ii(M)); format = format.replace(/(^|[^\\])M/g, "$1" + M); var d = utc ? date.getUTCDate() : date.getDate(); format = format.replace(/(^|[^\\])dddd+/g, "$1" + dddd[0]); format = format.replace(/(^|[^\\])ddd/g, "$1" + ddd[0]); format = format.replace(/(^|[^\\])dd/g, "$1" + ii(d)); format = format.replace(/(^|[^\\])d/g, "$1" + d); var H = utc ? date.getUTCHours() : date.getHours(); format = format.replace(/(^|[^\\])HH+/g, "$1" + ii(H)); format = format.replace(/(^|[^\\])H/g, "$1" + H); var h = H > 12 ? H - 12 : H === 0 ? 12 : H; format = format.replace(/(^|[^\\])hh+/g, "$1" + ii(h)); format = format.replace(/(^|[^\\])h/g, "$1" + h); var m = utc ? date.getUTCMinutes() : date.getMinutes(); format = format.replace(/(^|[^\\])mm+/g, "$1" + ii(m)); format = format.replace(/(^|[^\\])m/g, "$1" + m); var s = utc ? date.getUTCSeconds() : date.getSeconds(); format = format.replace(/(^|[^\\])ss+/g, "$1" + ii(s)); format = format.replace(/(^|[^\\])s/g, "$1" + s); var f = utc ? date.getUTCMilliseconds() : date.getMilliseconds(); format = format.replace(/(^|[^\\])fff+/g, "$1" + ii(f, 3)); f = Math.round(f / 10); format = format.replace(/(^|[^\\])ff/g, "$1" + ii(f)); f = Math.round(f / 10); format = format.replace(/(^|[^\\])f/g, "$1" + f); var T = H < 12 ? "AM" : "PM"; format = format.replace(/(^|[^\\])TT+/g, "$1" + T); format = format.replace(/(^|[^\\])T/g, "$1" + T.charAt(0)); var t = T.toLowerCase(); format = format.replace(/(^|[^\\])tt+/g, "$1" + t); format = format.replace(/(^|[^\\])t/g, "$1" + t.charAt(0)); var tz = -date.getTimezoneOffset(); var K = utc || !tz ? "Z" : tz > 0 ? "+" : "-"; if (!utc) { tz = Math.abs(tz); var tzHrs = Math.floor(tz / 60); var tzMin = tz % 60; K += ii(tzHrs) + ":" + ii(tzMin); } format = format.replace(/(^|[^\\])K/g, "$1" + K); var day = (utc ? date.getUTCDay() : date.getDay()) + 1; format = format.replace(new RegExp(dddd[0], "g"), dddd[day]); format = format.replace(new RegExp(ddd[0], "g"), ddd[day]); format = format.replace(new RegExp(MMMM[0], "g"), MMMM[M]); format = format.replace(new RegExp(MMM[0], "g"), MMM[M]); format = format.replace(/\\(.)/g, "$1"); return format; }; // jQuery extensions $.fn.paddingHeight = function() { return this.outerHeight(true) - this.height(); } $.fn.paddingWidth = function() { return this.outerWidth(true) - this.width(); } // Call function once at start-up to auto-size drop-down to selected option contents $.fn.autoSizeSelect = function() { return this.each(function() { $(this).change(function() { const arrowWidth = 30; const $this = $(this); // Create test element const text = $this.find("option:selected").text(); const $test = $("").html(text); // Add to body, get width, and get out $test.appendTo("body"); const width = $test.width(); $test.remove(); // Set select width $this.width(width + arrowWidth); }).change(); }); } // Cell truncations should only be used with .table-cell-hard-wrap style $.fn.truncateToWidth = function(text, rows) { if (isEmpty(text)) return ""; if (!rows) rows = 1; textMetrics.font = this.css("font"); var targetWidth = this.innerWidth(); if (rows > 1) targetWidth *= ((isIE ? 0.45 : 0.75) * rows); if (targetWidth > 30) { let textWidth = textMetrics.measureText(text).width; let limit = Math.min(text.length, Math.ceil(targetWidth / (textWidth / text.length))); while (textWidth > targetWidth && limit > 3) { limit--; text = text.truncate(limit); textWidth = textMetrics.measureText(text).width; } } else { text = text.charAt(0) + "..."; } return text; } $.fn.visible = function() { return this.css("visibility", "visible"); } $.fn.invisible = function() { return this.css("visibility", "hidden"); } // The following target arrays of promises $.fn.whenAny = function() { var finish = $.Deferred(); if (this.length === 0) finish.resolve(); else $.each(this, function(index, deferred) { deferred.done(finish.resolve); }); return finish.promise(); } $.fn.whenAll = function() { if (this.length > 0) return $.when.apply($, this); return $.Deferred().resolve().promise(); }