Showing the changes to Source/Libraries/GSF.Web/Model/Scripts (2 of 32 files). Show all
Unified · Split
| @@ -0,0 +1,667 @@ | |||
| 1 | //****************************************************************************************************** | ||
| 2 | // gsf.web.client.js - Gbtc | ||
| 3 | // | ||
| 4 | // Copyright © 2016, Grid Protection Alliance. All Rights Reserved. | ||
| 5 | // | ||
| 6 | // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | ||
| 7 | // the NOTICE file distributed with this work for additional information regarding copyright ownership. | ||
| 8 | // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may | ||
| 9 | // not use this file except in compliance with the License. You may obtain a copy of the License at: | ||
| 10 | // | ||
| 11 | // http://opensource.org/licenses/MIT | ||
| 12 | // | ||
| 13 | // Unless agreed to in writing, the subject software distributed under the License is distributed on an | ||
| 14 | // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | ||
| 15 | // License for the specific language governing permissions and limitations. | ||
| 16 | // | ||
| 17 | // Code Modification History: | ||
| 18 | // ---------------------------------------------------------------------------------------------------- | ||
| 19 | // 01/26/2016 - J. Ritchie Carroll | ||
| 20 | // Generated original version of source code. | ||
| 21 | // | ||
| 22 | //****************************************************************************************************** | ||
| 23 | // ReSharper disable NativeTypePrototypeExtending | ||
| 24 | |||
| 25 | // Grid Solutions Framework Core Web Client Script Functions | ||
| 26 | "use strict"; | ||
| 27 | |||
| 28 | const isIE = detectIE(); | ||
| 29 | var textMetrics; | ||
| 30 | |||
| 31 | $(function () { | ||
| 32 | // Create a canvas object that will be used for text metrics calculations | ||
| 33 | $("<canvas id=\"textMetricsCanvas\" height=\"1px\" width=\"1px\" style=\"visibility: hidden\"></canvas>").appendTo("body"); | ||
| 34 | |||
| 35 | // Get text metrics canvas context | ||
| 36 | textMetrics = document.getElementById("textMetricsCanvas").getContext("2d"); | ||
| 37 | }); | ||
| 38 | |||
| 39 | // Miscellaneous functions | ||
| 40 | function notNull(value, nonNullValue) { | ||
| 41 | return value || (nonNullValue || ""); | ||
| 42 | } | ||
| 43 | |||
| 44 | function detectIE() { | ||
| 45 | const ua = window.navigator.userAgent; | ||
| 46 | const msie = ua.indexOf("MSIE "); | ||
| 47 | |||
| 48 | if (msie > 0) { | ||
| 49 | // IE 10 or older => return version number | ||
| 50 | return parseInt(ua.substring(msie + 5, ua.indexOf(".", msie)), 10); | ||
| 51 | } | ||
| 52 | |||
| 53 | const trident = ua.indexOf("Trident/"); | ||
| 54 | |||
| 55 | if (trident > 0) { | ||
| 56 | // IE 11 => return version number | ||
| 57 | const rv = ua.indexOf("rv:"); | ||
| 58 | return parseInt(ua.substring(rv + 3, ua.indexOf(".", rv)), 10); | ||
| 59 | } | ||
| 60 | |||
| 61 | const edge = ua.indexOf("Edge/"); | ||
| 62 | |||
| 63 | if (edge > 0) { | ||
| 64 | // Edge (IE 12+) => return version number | ||
| 65 | return parseInt(ua.substring(edge + 5, ua.indexOf(".", edge)), 10); | ||
| 66 | } | ||
| 67 | |||
| 68 | // Other browser | ||
| 69 | return false; | ||
| 70 | } | ||
| 71 | |||
| 72 | function isNumber(val) { | ||
| 73 | return !isNaN(parseFloat(val)) && isFinite(val); | ||
| 74 | } | ||
| 75 | |||
| 76 | function isBool(val) { | ||
| 77 | if (typeof value === "boolean") | ||
| 78 | return true; | ||
| 79 | |||
| 80 | const lval = val.toString().toLowerCase(); | ||
| 81 | return lval === "true" || lval === "false"; | ||
| 82 | } | ||
| 83 | |||
| 84 | function getBool(val) { | ||
| 85 | const num = +val; | ||
| 86 | return !isNaN(num) ? !!num : !!String(val).toLowerCase().replace(false, ""); | ||
| 87 | } | ||
| 88 | |||
| 89 | // Number functions | ||
| 90 | Number.prototype.truncate = function() { | ||
| 91 | if (typeof Math.trunc != "function") | ||
| 92 | return parseInt(this.toString()); | ||
| 93 | |||
| 94 | return Math.trunc(this); | ||
| 95 | } | ||
| 96 | |||
| 97 | Number.prototype.padLeft = function (totalWidth, paddingChar) { | ||
| 98 | return this.truncate().toString().padLeft(totalWidth, paddingChar || "0"); | ||
| 99 | } | ||
| 100 | |||
| 101 | Number.prototype.padRight = function (totalWidth, paddingChar) { | ||
| 102 | return this.truncate().toString().padRight(totalWidth, paddingChar || "0"); | ||
| 103 | } | ||
| 104 | |||
| 105 | // Array functions | ||
| 106 | |||
| 107 | // Combines a dictionary of key-value pairs into a string. Values will be escaped within startValueDelimiter and endValueDelimiter | ||
| 108 | // to contain nested key/value pair expressions like the following: "normalKVP=-1; nestedKVP={p1=true; p2=0.001}" when either the | ||
| 109 | // parameterDelimiter or the keyValueDelimiter are detected in the value of the key/value pair. | ||
| 110 | function joinKeyValuePairs (source, parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter) { | ||
| 111 | if (!parameterDelimiter) | ||
| 112 | parameterDelimiter = ";"; | ||
| 113 | |||
| 114 | if (!keyValueDelimiter) | ||
| 115 | keyValueDelimiter = "="; | ||
| 116 | |||
| 117 | if (!startValueDelimiter) | ||
| 118 | startValueDelimiter = "{"; | ||
| 119 | |||
| 120 | if (!endValueDelimiter) | ||
| 121 | endValueDelimiter = "}"; | ||
| 122 | |||
| 123 | const values = []; | ||
| 124 | |||
| 125 | for (var key in source) { | ||
| 126 | if (source.hasOwnProperty(key)) { | ||
| 127 | var value = source[key]; | ||
| 128 | |||
| 129 | if (isBool(value)) | ||
| 130 | value = value.toString().toLowerCase(); | ||
| 131 | else | ||
| 132 | value = value ? value.toString() : ""; | ||
| 133 | |||
| 134 | if (value.indexOf(parameterDelimiter) >= 0 || value.indexOf(keyValueDelimiter) >= 0) | ||
| 135 | value = startValueDelimiter + value + endValueDelimiter; | ||
| 136 | |||
| 137 | values.push(key + keyValueDelimiter + value); | ||
| 138 | } | ||
| 139 | } | ||
| 140 | |||
| 141 | return values.join(parameterDelimiter + " "); | ||
| 142 | }; | ||
| 143 | |||
| 144 | Array.prototype.joinKeyValuePairs = function (parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter) { | ||
| 145 | return joinKeyValuePairs(this, parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter); | ||
| 146 | }; | ||
| 147 | |||
| 148 | // Represents a dictionary style class with case-insensitive keys | ||
| 149 | function Dictionary(source) { | ||
| 150 | const self = this; | ||
| 151 | |||
| 152 | if (!source) | ||
| 153 | source = []; | ||
| 154 | |||
| 155 | self._keys = []; | ||
| 156 | self._values = []; | ||
| 157 | |||
| 158 | self.count = function () { | ||
| 159 | var size = 0; | ||
| 160 | |||
| 161 | for (var property in self._values) { | ||
| 162 | if (self._values.hasOwnProperty(property)) | ||
| 163 | size++; | ||
| 164 | } | ||
| 165 | |||
| 166 | return size; | ||
| 167 | } | ||
| 168 | |||
| 169 | self.keys = function () { | ||
| 170 | const keys = []; | ||
| 171 | |||
| 172 | for (var property in self._keys) { | ||
| 173 | if (self._keys.hasOwnProperty(property)) | ||
| 174 | keys.push(self._keys[property]); | ||
| 175 | } | ||
| 176 | |||
| 177 | return keys; | ||
| 178 | } | ||
| 179 | |||
| 180 | self.values = function () { | ||
| 181 | const values = []; | ||
| 182 | |||
| 183 | for (var property in self._values) { | ||
| 184 | if (self._keys.hasOwnProperty(property)) | ||
| 185 | values.push(self._values[property]); | ||
| 186 | } | ||
| 187 | |||
| 188 | return values; | ||
| 189 | } | ||
| 190 | |||
| 191 | self.get = function (key) { | ||
| 192 | return self._values[String(key).toLowerCase()]; | ||
| 193 | } | ||
| 194 | |||
| 195 | self.set = function (key, value) { | ||
| 196 | const lkey = String(key).toLowerCase(); | ||
| 197 | |||
| 198 | if (!self._keys[lkey] || self._keys[lkey].toLowerCase() !== key) | ||
| 199 | self._keys[lkey] = key; | ||
| 200 | |||
| 201 | if (isBool(value)) | ||
| 202 | self._values[lkey] = getBool(value); | ||
| 203 | else | ||
| 204 | self._values[lkey] = value; | ||
| 205 | } | ||
| 206 | |||
| 207 | self.remove = function (key) { | ||
| 208 | const lkey = String(key).toLowerCase(); | ||
| 209 | delete self._keys[lkey]; | ||
| 210 | delete self._values[lkey]; | ||
| 211 | } | ||
| 212 | |||
| 213 | self.containsKey = function (key) { | ||
| 214 | const lkey = String(key).toLowerCase(); | ||
| 215 | |||
| 216 | for (var property in self._values) { | ||
| 217 | if (self._values.hasOwnProperty(property) && property === lkey) | ||
| 218 | return true; | ||
| 219 | } | ||
| 220 | |||
| 221 | return false; | ||
| 222 | } | ||
| 223 | |||
| 224 | self.containsValue = function (value) { | ||
| 225 | for (var property in self._values) { | ||
| 226 | if (self._values.hasOwnProperty(property) && self._values[property] === value) | ||
| 227 | return true; | ||
| 228 | } | ||
| 229 | |||
| 230 | return false; | ||
| 231 | } | ||
| 232 | |||
| 233 | self.clear = function () { | ||
| 234 | self._keys = []; | ||
| 235 | self._values = []; | ||
| 236 | } | ||
| 237 | |||
| 238 | self.joinKeyValuePairs = function (parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter) { | ||
| 239 | const keyValuePairs = []; | ||
| 240 | |||
| 241 | for (var property in self._values) { | ||
| 242 | if (self._values.hasOwnProperty(property)) | ||
| 243 | keyValuePairs[self._keys[property]] = self._values[property]; | ||
| 244 | } | ||
| 245 | |||
| 246 | return keyValuePairs.joinKeyValuePairs(parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter); | ||
| 247 | } | ||
| 248 | |||
| 249 | self.pushAll = function (source) { | ||
| 250 | for (var property in source) | ||
| 251 | if (source.hasOwnProperty(property)) | ||
| 252 | self.set(property, source[property]); | ||
| 253 | } | ||
| 254 | |||
| 255 | self.toObservableDictionary = function (useLowerKeys) { | ||
| 256 | // See ko.observableDictionary.js | ||
| 257 | const observableDictionary = new ko.observableDictionary(); | ||
| 258 | |||
| 259 | for (var property in self._values) { | ||
| 260 | if (self._values.hasOwnProperty(property)) | ||
| 261 | observableDictionary.push(useLowerKeys ? property : self._keys[property], self._values[property]); | ||
| 262 | } | ||
| 263 | |||
| 264 | return observableDictionary; | ||
| 265 | } | ||
| 266 | |||
| 267 | self.updateObservableDictionary = function (observableDictionary, useLowerKeys) { | ||
| 268 | for (var property in self._values) { | ||
| 269 | if (self._values.hasOwnProperty(property)) | ||
| 270 | observableDictionary.set(useLowerKeys ? property : self._keys[property], self._values[property]); | ||
| 271 | } | ||
| 272 | } | ||
| 273 | |||
| 274 | // Construction | ||
| 275 | if (source instanceof Dictionary) { | ||
| 276 | for (var property in source._values) | ||
| 277 | if (source._values.hasOwnProperty(property)) | ||
| 278 | self.set(source._keys[property], source._values[property]); | ||
| 279 | } | ||
| 280 | else { | ||
| 281 | for (var property in source) { | ||
| 282 | if (source.hasOwnProperty(property)) | ||
| 283 | self.set(property, source[property]); | ||
| 284 | } | ||
| 285 | } | ||
| 286 | } | ||
| 287 | |||
| 288 | Dictionary.fromObservableDictionary = function (observableDictionary) { | ||
| 289 | var dictionary = new Dictionary(); | ||
| 290 | dictionary.pushAll(observableDictionary.toJSON()); | ||
| 291 | return dictionary; | ||
| 292 | } | ||
| 293 | |||
| 294 | // String functions | ||
| 295 | function isEmpty(str) { | ||
| 296 | return !str || String(str).length === 0; | ||
| 297 | } | ||
| 298 | |||
| 299 | String.prototype.truncate = function (limit) { | ||
| 300 | const text = this.trim(); | ||
| 301 | |||
| 302 | if (text.length > limit) | ||
| 303 | return text.substr(0, limit - 3) + "..."; | ||
| 304 | |||
| 305 | return text; | ||
| 306 | } | ||
| 307 | |||
| 308 | String.prototype.replaceAll = function (findText, replaceWith, ignoreCase) { | ||
| 309 | return this.replace( | ||
| 310 | new RegExp(findText.replace(/([\/\,\!\\\^\$\{\}\[\]\(\)\.\*\+\?\|\<\>\-\&])/g, "\\$&"), (ignoreCase ? "gi" : "g")), | ||
| 311 | (typeof replaceWith == "string") ? replaceWith.replace(/\$/g, "$$$$") : replaceWith); | ||
| 312 | } | ||
| 313 | |||
| 314 | String.prototype.padLeft = function (totalWidth, paddingChar) { | ||
| 315 | if (totalWidth > this.length) | ||
| 316 | return Array(totalWidth - this.length + 1).join(paddingChar || " ") + this; | ||
| 317 | |||
| 318 | return this; | ||
| 319 | } | ||
| 320 | |||
| 321 | String.prototype.padRight = function (totalWidth, paddingChar) { | ||
| 322 | if (totalWidth > this.length) | ||
| 323 | return this + Array(totalWidth - this.length + 1).join(paddingChar || " "); | ||
| 324 | |||
| 325 | return this; | ||
| 326 | } | ||
| 327 | |||
| 328 | String.prototype.countOccurrences = function (searchString) { | ||
| 329 | return (this.split(searchString).length - 1); | ||
| 330 | } | ||
| 331 | |||
| 332 | if (!String.prototype.startsWith) { | ||
| 333 | String.prototype.startsWith = function (searchString, position) { | ||
| 334 | position = position || 0; | ||
| 335 | return this.substr(position, searchString.length) === searchString; | ||
| 336 | } | ||
| 337 | } | ||
| 338 | |||
| 339 | String.prototype.regexEncode = function () { | ||
| 340 | return "\\u" + this.charCodeAt(0).toString(16).padLeft(4, "0"); | ||
| 341 | } | ||
| 342 | |||
| 343 | // Returns a Dictionary of the parsed key/value pair expressions from a string. Parameter pairs are delimited by keyValueDelimiter | ||
| 344 | // and multiple pairs separated by parameterDelimiter. Supports encapsulated nested expressions. | ||
| 345 | String.prototype.parseKeyValuePairs = function (parameterDelimiter, keyValueDelimiter, startValueDelimiter, endValueDelimiter, ignoreDuplicateKeys) { | ||
| 346 | if (!parameterDelimiter) | ||
| 347 | parameterDelimiter = ";"; | ||
| 348 | |||
| 349 | if (!keyValueDelimiter) | ||
| 350 | keyValueDelimiter = "="; | ||
| 351 | |||
| 352 | if (!startValueDelimiter) | ||
| 353 | startValueDelimiter = "{"; | ||
| 354 | |||
| 355 | if (!endValueDelimiter) | ||
| 356 | endValueDelimiter = "}"; | ||
| 357 | |||
| 358 | if (ignoreDuplicateKeys === undefined) | ||
| 359 | ignoreDuplicateKeys = true; | ||
| 360 | |||
| 361 | if (parameterDelimiter === keyValueDelimiter || | ||
| 362 | parameterDelimiter === startValueDelimiter || | ||
| 363 | parameterDelimiter === endValueDelimiter || | ||
| 364 | keyValueDelimiter === startValueDelimiter || | ||
| 365 | keyValueDelimiter === endValueDelimiter || | ||
| 366 | startValueDelimiter === endValueDelimiter) | ||
| 367 | throw "All delimiters must be unique"; | ||
| 368 | |||
| 369 | const escapedParameterDelimiter = parameterDelimiter.regexEncode(); | ||
| 370 | const escapedKeyValueDelimiter = keyValueDelimiter.regexEncode(); | ||
| 371 | const escapedStartValueDelimiter = startValueDelimiter.regexEncode(); | ||
| 372 | const escapedEndValueDelimiter = endValueDelimiter.regexEncode(); | ||
| 373 | const backslashDelimiter = "\\".regexEncode(); | ||
| 374 | |||
| 375 | var keyValuePairs = new Dictionary(); | ||
| 376 | var escapedValue = []; | ||
| 377 | var valueEscaped = false; | ||
| 378 | var delimiterDepth = 0; | ||
| 379 | |||
| 380 | // Escape any parameter or key/value delimiters within tagged value sequences | ||
| 381 | // For example, the following string: | ||
| 382 | // "normalKVP=-1; nestedKVP={p1=true; p2=false}") | ||
| 383 | // would be encoded as: | ||
| 384 | // "normalKVP=-1; nestedKVP=p1\\u003dtrue\\u003b p2\\u003dfalse") | ||
| 385 | for (var i = 0; i < this.length; i++) { | ||
| 386 | var character = this[i]; | ||
| 387 | |||
| 388 | if (character === startValueDelimiter) { | ||
| 389 | if (!valueEscaped) { | ||
| 390 | valueEscaped = true; | ||
| 391 | continue; // Don't add tag start delimiter to final value | ||
| 392 | } | ||
| 393 | |||
| 394 | // Handle nested delimiters | ||
| 395 | delimiterDepth++; | ||
| 396 | } | ||
| 397 | |||
| 398 | if (character === endValueDelimiter) { | ||
| 399 | if (valueEscaped) { | ||
| 400 | if (delimiterDepth > 0) { | ||
| 401 | // Handle nested delimiters | ||
| 402 | delimiterDepth--; | ||
| 403 | } | ||
| 404 | else { | ||
| 405 | valueEscaped = false; | ||
| 406 | continue; // Don't add tag stop delimiter to final value | ||
| 407 | } | ||
| 408 | } | ||
| 409 | else { | ||
| 410 | throw "Failed to parse key/value pairs: invalid delimiter mismatch. Encountered end value delimiter \"" + endValueDelimiter + "\" before start value delimiter \"" + startValueDelimiter + "\"."; | ||
| 411 | } | ||
| 412 | } | ||
| 413 | |||
| 414 | if (valueEscaped) { | ||
| 415 | // Escape any delimiter characters inside nested key/value pair | ||
| 416 | if (character === parameterDelimiter) | ||
| 417 | escapedValue.push(escapedParameterDelimiter); | ||
| 418 | else if (character === keyValueDelimiter) | ||
| 419 | escapedValue.push(escapedKeyValueDelimiter); | ||
| 420 | else if (character === startValueDelimiter) | ||
| 421 | escapedValue.push(escapedStartValueDelimiter); | ||
| 422 | else if (character === endValueDelimiter) | ||
| 423 | escapedValue.push(escapedEndValueDelimiter); | ||
| 424 | else if (character === "\\") | ||
| 425 | escapedValue.push(backslashDelimiter); | ||
| 426 | else | ||
| 427 | escapedValue.push(character); | ||
| 428 | } | ||
| 429 | else { | ||
| 430 | if (character === "\\") | ||
| 431 | escapedValue.push(backslashDelimiter); | ||
| 432 | else | ||
| 433 | escapedValue.push(character); | ||
| 434 | } | ||
| 435 | } | ||
| 436 | |||
| 437 | if (delimiterDepth !== 0 || valueEscaped) { | ||
| 438 | // If value is still escaped, tagged expression was not terminated | ||
| 439 | if (valueEscaped) | ||
| 440 | delimiterDepth = 1; | ||
| 441 | |||
| 442 | throw "Failed to parse key/value pairs: invalid delimiter mismatch. Encountered more " + | ||
| 443 | (delimiterDepth > 0 ? "start value delimiters \"" + startValueDelimiter + "\"" : "end value delimiters \"" + endValueDelimiter + "\"") + " than " + | ||
| 444 | (delimiterDepth < 0 ? "start value delimiters \"" + startValueDelimiter + "\"" : "end value delimiters \"" + endValueDelimiter + "\"") + "."; | ||
| 445 | } | ||
| 446 | |||
| 447 | // Parse key/value pairs from escaped value | ||
| 448 | var pairs = escapedValue.join("").split(parameterDelimiter); | ||
| 449 | |||
| 450 | for (var i = 0; i < pairs.length; i++) { | ||
| 451 | // Separate key from value | ||
| 452 | var elements = pairs[i].split(keyValueDelimiter); | ||
| 453 | |||
| 454 | if (elements.length === 2) { | ||
| 455 | // Get key | ||
| 456 | var key = elements[0].trim(); | ||
| 457 | |||
| 458 | // Get unescaped value | ||
| 459 | var unescapedValue = elements[1].trim(). | ||
| 460 | replaceAll(escapedParameterDelimiter, parameterDelimiter). | ||
| 461 | replaceAll(escapedKeyValueDelimiter, keyValueDelimiter). | ||
| 462 | replaceAll(escapedStartValueDelimiter, startValueDelimiter). | ||
| 463 | replaceAll(escapedEndValueDelimiter, endValueDelimiter). | ||
| 464 | replaceAll(backslashDelimiter, "\\"); | ||
| 465 | |||
| 466 | // Add key/value pair to dictionary | ||
| 467 | if (ignoreDuplicateKeys) { | ||
| 468 | // Add or replace key elements with unescaped value | ||
| 469 | keyValuePairs.set(key, unescapedValue); | ||
| 470 | } | ||
| 471 | else { | ||
| 472 | // Add key elements with unescaped value throwing an exception for encountered duplicate keys | ||
| 473 | if (keyValuePairs.containsKey(key)) | ||
| 474 | throw "Failed to parse key/value pairs: duplicate key encountered. Key \"" + key + "\" is not unique within the string: \"" + this + "\""; | ||
| 475 | |||
| 476 | keyValuePairs.set(key, unescapedValue); | ||
| 477 | } | ||
| 478 | } | ||
| 479 | } | ||
| 480 | |||
| 481 | return keyValuePairs; | ||
| 482 | } | ||
| 483 | |||
| 484 | // Date Functions | ||
| 485 | Date.prototype.addDays = function (days) { | ||
| 486 | return new Date(this.setDate(this.getDate() + days)); | ||
| 487 | } | ||
| 488 | |||
| 489 | Date.prototype.toUTC = function () { | ||
| 490 | this.setMinutes(this.getMinutes() - this.getTimezoneOffset()); | ||
| 491 | return this; | ||
| 492 | } | ||
| 493 | |||
| 494 | Date.prototype.daysBetween = function (startDate) { | ||
| 495 | const millisecondsPerDay = 24 * 60 * 60 * 1000; | ||
| 496 | return (this.toUTC() - startDate.toUTC()) / millisecondsPerDay; | ||
| 497 | } | ||
| 498 | |||
| 499 | String.prototype.toDate = function () { | ||
| 500 | return new Date(Date.parse(this)); | ||
| 501 | }; | ||
| 502 | |||
| 503 | String.prototype.formatDate = function (format, utc) { | ||
| 504 | return formatDate(this.toDate(), format, utc); | ||
| 505 | }; | ||
| 506 | |||
| 507 | Date.prototype.formatDate = function (format, utc) { | ||
| 508 | return formatDate(this, format, utc); | ||
| 509 | }; | ||
| 510 | |||
| 511 | function formatDate(date, format, utc) { | ||
| 512 | if (typeof date === "string") | ||
| 513 | return formatDate(date.toDate(), format, utc); | ||
| 514 | |||
| 515 | if (format === undefined && DateTimeFormat != undefined) | ||
| 516 | format = DateTimeFormat; | ||
| 517 | |||
| 518 | if (utc === undefined) | ||
| 519 | utc = true; | ||
| 520 | |||
| 521 | var MMMM = ["\x00", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; | ||
| 522 | var MMM = ["\x01", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; | ||
| 523 | var dddd = ["\x02", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; | ||
| 524 | var ddd = ["\x03", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; | ||
| 525 | |||
| 526 | function ii(i, len) { | ||
| 527 | var ss = i + ""; | ||
| 528 | len = len || 2; | ||
| 529 | while (ss.length < len) ss = "0" + ss; | ||
| 530 | return ss; | ||
| 531 | } | ||
| 532 | |||
| 533 | var y = utc ? date.getUTCFullYear() : date.getFullYear(); | ||
| 534 | format = format.replace(/(^|[^\\])yyyy+/g, "$1" + y); | ||
| 535 | format = format.replace(/(^|[^\\])yy/g, "$1" + y.toString().substr(2, 2)); | ||
| 536 | format = format.replace(/(^|[^\\])y/g, "$1" + y); | ||
| 537 | |||
| 538 | var M = (utc ? date.getUTCMonth() : date.getMonth()) + 1; | ||
| 539 | format = format.replace(/(^|[^\\])MMMM+/g, "$1" + MMMM[0]); | ||
| 540 | format = format.replace(/(^|[^\\])MMM/g, "$1" + MMM[0]); | ||
| 541 | format = format.replace(/(^|[^\\])MM/g, "$1" + ii(M)); | ||
| 542 | format = format.replace(/(^|[^\\])M/g, "$1" + M); | ||
| 543 | |||
| 544 | var d = utc ? date.getUTCDate() : date.getDate(); | ||
| 545 | format = format.replace(/(^|[^\\])dddd+/g, "$1" + dddd[0]); | ||
| 546 | format = format.replace(/(^|[^\\])ddd/g, "$1" + ddd[0]); | ||
| 547 | format = format.replace(/(^|[^\\])dd/g, "$1" + ii(d)); | ||
| 548 | format = format.replace(/(^|[^\\])d/g, "$1" + d); | ||
| 549 | |||
| 550 | var H = utc ? date.getUTCHours() : date.getHours(); | ||
| 551 | format = format.replace(/(^|[^\\])HH+/g, "$1" + ii(H)); | ||
| 552 | format = format.replace(/(^|[^\\])H/g, "$1" + H); | ||
| 553 | |||
| 554 | var h = H > 12 ? H - 12 : H === 0 ? 12 : H; | ||
| 555 | format = format.replace(/(^|[^\\])hh+/g, "$1" + ii(h)); | ||
| 556 | format = format.replace(/(^|[^\\])h/g, "$1" + h); | ||
| 557 | |||
| 558 | var m = utc ? date.getUTCMinutes() : date.getMinutes(); | ||
| 559 | format = format.replace(/(^|[^\\])mm+/g, "$1" + ii(m)); | ||
| 560 | format = format.replace(/(^|[^\\])m/g, "$1" + m); | ||
| 561 | |||
| 562 | var s = utc ? date.getUTCSeconds() : date.getSeconds(); | ||
| 563 | format = format.replace(/(^|[^\\])ss+/g, "$1" + ii(s)); | ||
| 564 | format = format.replace(/(^|[^\\])s/g, "$1" + s); | ||
| 565 | |||
| 566 | var f = utc ? date.getUTCMilliseconds() : date.getMilliseconds(); | ||
| 567 | format = format.replace(/(^|[^\\])fff+/g, "$1" + ii(f, 3)); | ||
| 568 | f = Math.round(f / 10); | ||
| 569 | format = format.replace(/(^|[^\\])ff/g, "$1" + ii(f)); | ||
| 570 | f = Math.round(f / 10); | ||
| 571 | format = format.replace(/(^|[^\\])f/g, "$1" + f); | ||
| 572 | |||
| 573 | var T = H < 12 ? "AM" : "PM"; | ||
| 574 | format = format.replace(/(^|[^\\])TT+/g, "$1" + T); | ||
| 575 | format = format.replace(/(^|[^\\])T/g, "$1" + T.charAt(0)); | ||
| 576 | |||
| 577 | var t = T.toLowerCase(); | ||
| 578 | format = format.replace(/(^|[^\\])tt+/g, "$1" + t); | ||
| 579 | format = format.replace(/(^|[^\\])t/g, "$1" + t.charAt(0)); | ||
| 580 | |||
| 581 | var tz = -date.getTimezoneOffset(); | ||
| 582 | var K = utc || !tz ? "Z" : tz > 0 ? "+" : "-"; | ||
| 583 | if (!utc) { | ||
| 584 | tz = Math.abs(tz); | ||
| 585 | var tzHrs = Math.floor(tz / 60); | ||
| 586 | var tzMin = tz % 60; | ||
| 587 | K += ii(tzHrs) + ":" + ii(tzMin); | ||
| 588 | } | ||
| 589 | |||
| 590 | format = format.replace(/(^|[^\\])K/g, "$1" + K); | ||
| 591 | var day = (utc ? date.getUTCDay() : date.getDay()) + 1; | ||
| 592 | |||
| 593 | format = format.replace(new RegExp(dddd[0], "g"), dddd[day]); | ||
| 594 | format = format.replace(new RegExp(ddd[0], "g"), ddd[day]); | ||
| 595 | |||
| 596 | format = format.replace(new RegExp(MMMM[0], "g"), MMMM[M]); | ||
| 597 | format = format.replace(new RegExp(MMM[0], "g"), MMM[M]); | ||
| 598 | |||
| 599 | format = format.replace(/\\(.)/g, "$1"); | ||
| 600 | |||
| 601 | return format; | ||
| 602 | }; | ||
| 603 | |||
| 604 | // jQuery extensions | ||
| 605 | $.fn.paddingHeight = function () { | ||
| 606 | return this.outerHeight(true) - this.height(); | ||
| 607 | } | ||
| 608 | |||
| 609 | $.fn.paddingWidth = function () { | ||
| 610 | return this.outerWidth(true) - this.width(); | ||
| 611 | } | ||
| 612 | |||
| 613 | // Cell truncations should only be used with .table-cell-hard-wrap style | ||
| 614 | $.fn.truncateToWidth = function (text, rows) { | ||
| 615 | if (isEmpty(text)) | ||
| 616 | return ""; | ||
| 617 | |||
| 618 | if (!rows) | ||
| 619 | rows = 1; | ||
| 620 | |||
| 621 | textMetrics.font = this.css("font"); | ||
| 622 | |||
| 623 | var targetWidth = this.innerWidth(); | ||
| 624 | var textWidth = textMetrics.measureText(text).width; | ||
| 625 | |||
| 626 | if (rows > 1) | ||
| 627 | targetWidth *= ((isIE ? 0.45 : 0.75) * rows); | ||
| 628 | |||
| 629 | var limit = Math.min(text.length, Math.ceil(targetWidth / (textWidth / text.length))); | ||
| 630 | |||
| 631 | while (textWidth > targetWidth && limit > 1) { | ||
| 632 | limit--; | ||
| 633 | text = text.truncate(limit); | ||
| 634 | textWidth = textMetrics.measureText(text).width; | ||
| 635 | } | ||
| 636 | |||
| 637 | return text; | ||
| 638 | } | ||
| 639 | |||
| 640 | $.fn.visible = function () { | ||
| 641 | return this.css("visibility", "visible"); | ||
| 642 | } | ||
| 643 | |||
| 644 | $.fn.invisible = function () { | ||
| 645 | return this.css("visibility", "hidden"); | ||
| 646 | } | ||
| 647 | |||
| 648 | // The following target arrays of promises | ||
| 649 | $.fn.whenAny = function () { | ||
| 650 | var finish = $.Deferred(); | ||
| 651 | |||
| 652 | if (this.length === 0) | ||
| 653 | finish.resolve(); | ||
| 654 | else | ||
| 655 | $.each(this, function (index, deferred) { | ||
| 656 | deferred.done(finish.resolve); | ||
| 657 | }); | ||
| 658 | |||
| 659 | return finish.promise(); | ||
| 660 | } | ||
| 661 | |||
| 662 | $.fn.whenAll = function () { | ||
| 663 | if (this.length > 0) | ||
| 664 | return $.when.apply($, this); | ||
| 665 | |||
| 666 | return $.Deferred().resolve().promise(); | ||
| 667 | } | ||
| @@ -0,0 +1,568 @@ | |||
| 1 | //****************************************************************************************************** | ||
| 2 | // pagedViewModel.js - Gbtc | ||
| 3 | // | ||
| 4 | // Copyright © 2016, Grid Protection Alliance. All Rights Reserved. | ||
| 5 | // | ||
| 6 | // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See | ||
| 7 | // the NOTICE file distributed with this work for additional information regarding copyright ownership. | ||
| 8 | // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may | ||
| 9 | // not use this file except in compliance with the License. You may obtain a copy of the License at: | ||
| 10 | // | ||
| 11 | // http://opensource.org/licenses/MIT | ||
| 12 | // | ||
| 13 | // Unless agreed to in writing, the subject software distributed under the License is distributed on an | ||
| 14 | // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the | ||
| 15 | // License for the specific language governing permissions and limitations. | ||
| 16 | // | ||
| 17 | // Code Modification History: | ||
| 18 | // ---------------------------------------------------------------------------------------------------- | ||
| 19 | // 01/23/2016 - J. Ritchie Carroll | ||
| 20 | // Generated original version of source code. | ||
| 21 | // | ||
| 22 | //****************************************************************************************************** | ||
| 23 | |||
| 24 | // Paged view model base class scripts | ||
| 25 | "use strict"; | ||
| 26 | |||
| 27 | var RecordMode = { | ||
| 28 | View: 0, | ||
| 29 | Edit: 1, | ||
| 30 | AddNew: 2 | ||
| 31 | } | ||
| 32 | |||
| 33 | // Define paged view model class | ||
| 34 | function PagedViewModel() { | ||
| 35 | const self = this; | ||
| 36 | |||
| 37 | // Configuration fields | ||
| 38 | self.labelField = "{name}"; // Field name that best represents row for delete confirmation | ||
| 39 | self.primaryKeyFields = ["{id}"]; // Primary key field names array | ||
| 40 | self.defaultSortField = "{id}"; // Default sort field | ||
| 41 | self.defaultSortAscending = true; // Default sort ascending flag | ||
| 42 | self.initialFocusField = ""; // Initial add/edit field with focus | ||
| 43 | self.modelName = "{name}"; // Name of model used for cookie names, defaults to page title | ||
| 44 | |||
| 45 | // Observable fields | ||
| 46 | self.pageRecords = ko.observableArray(); // Records queried for current page | ||
| 47 | self.recordCount = ko.observable(0); // Queried record count | ||
| 48 | self.sortField = ko.observable(self.defaultSortField); // Current sort field, persisted per model | ||
| 49 | self.sortAscending = ko.observable(self.defaultSortAscending); // Current sort ascending flag, persisted per model | ||
| 50 | self.unassignedFieldCount = ko.observable(0); // Number of bound fields with missing data | ||
| 51 | self.dataHubIsConnected = ko.observable(false); // Data hub connected observable flag - externally managed | ||
| 52 | self.errors = ko.validation.group([ko.observable(0)]); // Validation errors array | ||
| 53 | self.canEdit = ko.observable(true); // Can edit flag - normally controlled by external roles | ||
| 54 | self.canAddNew = ko.observable(true); // Can add new flag - normally controlled by external roles | ||
| 55 | self.canDelete = ko.observable(true); // Can delete flag - normally controlled by external roles | ||
| 56 | |||
| 57 | // Internal fields | ||
| 58 | self._currentPageSize = ko.observable(1); | ||
| 59 | self._currentPage = ko.observable(0); | ||
| 60 | self._currentRecord = ko.observable(); | ||
| 61 | self._recordMode = ko.observable(RecordMode.View); | ||
| 62 | self._isDirty = ko.observable(false); | ||
| 63 | self._columnWidths = []; | ||
| 64 | |||
| 65 | // Properties | ||
| 66 | |||
| 67 | // Gets or sets the number records to display on the page | ||
| 68 | self.currentPageSize = ko.pureComputed({ | ||
| 69 | read: self._currentPageSize, | ||
| 70 | write: function (value) { | ||
| 71 | if (value < 1) | ||
| 72 | value = 1; | ||
| 73 | |||
| 74 | if (value !== self._currentPageSize()) { | ||
| 75 | self._currentPageSize(value); | ||
| 76 | |||
| 77 | // Validate current page after page size change | ||
| 78 | if (self.currentPage()) | ||
| 79 | self.currentPage(self.currentPage()); | ||
| 80 | } | ||
| 81 | }, | ||
| 82 | owner: self | ||
| 83 | }); | ||
| 84 | |||
| 85 | // Gets or sets the current page number | ||
| 86 | self.currentPage = ko.pureComputed({ | ||
| 87 | read: self._currentPage, | ||
| 88 | write: function (value) { | ||
| 89 | if (value < 1) | ||
| 90 | value = 1; | ||
| 91 | else if (value > self.totalPages()) | ||
| 92 | value = self.totalPages(); | ||
| 93 | |||
| 94 | // Requery records for page when current page changes | ||
| 95 | if (value !== self._currentPage()) { | ||
| 96 | self._currentPage(value); | ||
| 97 | self.queryPageRecords(); | ||
| 98 | } | ||
| 99 | }, | ||
| 100 | owner: self | ||
| 101 | }); | ||
| 102 | |||
| 103 | // Gets or sets current record on page | ||
| 104 | self.currentRecord = ko.pureComputed({ | ||
| 105 | read: self._currentRecord, | ||
| 106 | write: function(value) { | ||
| 107 | self._currentRecord(value); | ||
| 108 | self.isDirty(false); | ||
| 109 | $(self).trigger("currentRecordChanged"); | ||
| 110 | self.applyValidationParameters(); | ||
| 111 | |||
| 112 | // Watch for changes to fields in current record | ||
| 113 | ko.watch(self._currentRecord(), function (parents, child, item) { | ||
| 114 | self.isDirty(true); | ||
| 115 | $(self).trigger("currentRecordUpdated", [child]); | ||
| 116 | |||
| 117 | // Recalculate unassigned field count after bindings may have been updated by field change | ||
| 118 | setTimeout(self.calculateUnassignedFieldCount, 10); | ||
| 119 | }); | ||
| 120 | }, | ||
| 121 | owner: self | ||
| 122 | }); | ||
| 123 | |||
| 124 | // Gets or sets the record mode, i.e., view, edit or add new | ||
| 125 | self.recordMode = ko.pureComputed({ | ||
| 126 | read: self._recordMode, | ||
| 127 | write: function (newMode) { | ||
| 128 | const oldMode = self._recordMode(); | ||
| 129 | |||
| 130 | if (newMode !== oldMode) { | ||
| 131 | self._recordMode(newMode); | ||
| 132 | |||
| 133 | if (oldMode === RecordMode.View && newMode === RecordMode.Edit) | ||
| 134 | self.setFocusOnInitialField(); | ||
| 135 | |||
| 136 | $(self).trigger("recordModeChanged", [oldMode, newMode]); | ||
| 137 | } | ||
| 138 | }, | ||
| 139 | owner: self | ||
| 140 | }); | ||
| 141 | |||
| 142 | // Gets or sets current record changed flag | ||
| 143 | self.isDirty = ko.pureComputed({ | ||
| 144 | read: self._isDirty, | ||
| 145 | write: function (value) { | ||
| 146 | if (value === undefined) | ||
| 147 | value = true; | ||
| 148 | |||
| 149 | self._isDirty(value); | ||
| 150 | self.calculateUnassignedFieldCount(); | ||
| 151 | }, | ||
| 152 | owner: self | ||
| 153 | }); | ||
| 154 | |||
| 155 | // Gets total number pages based on total record count and current page size | ||
| 156 | self.totalPages = ko.pureComputed(function () { | ||
| 157 | return Math.max(Math.ceil(self.recordCount() / self.currentPageSize()), 1); | ||
| 158 | }); | ||
| 159 | |||
| 160 | // Gets flag that determines if current page is first page | ||
| 161 | self.onFirstPage = ko.pureComputed(function () { | ||
| 162 | return self.currentPage() <= 1; | ||
| 163 | }); | ||
| 164 | |||
| 165 | // Gets flag that determines if current page is last page | ||
| 166 | self.onLastPage = ko.pureComputed(function () { | ||
| 167 | return self.currentPage() >= self.totalPages(); | ||
| 168 | }); | ||
| 169 | |||
| 170 | // Return validation errors, i.e., non-required field errors | ||
| 171 | self.validationErrors = ko.pureComputed(function() { | ||
| 172 | return self.errors().length - self.unassignedFieldCount(); | ||
| 173 | }).extend({ notify: "always" }); | ||
| 174 | |||
| 175 | // Delegates | ||
| 176 | self.queryRecordCount = function () { }; | ||
| 177 | self.queryRecords = function (/* sortField, ascending, page, pageSize */) { }; | ||
| 178 | self.deleteRecord = function (/* keyValues[] */) { }; | ||
| 179 | self.newRecord = function () { }; | ||
| 180 | self.addNewRecord = function (/* record */) { }; | ||
| 181 | self.updateRecord = function (/* record */) { }; | ||
| 182 | self.applyValidationParameters = function () { }; | ||
| 183 | |||
| 184 | // Setters needed to assign delegate properties, 'cause Javascript | ||
| 185 | self.setQueryRecordCount = function (queryRecordCountFunction) { | ||
| 186 | self.queryRecordCount = queryRecordCountFunction; | ||
| 187 | } | ||
| 188 | |||
| 189 | self.setQueryRecords = function (queryRecordsFunction) { | ||
| 190 | self.queryRecords = queryRecordsFunction; | ||
| 191 | } | ||
| 192 | |||
| 193 | self.setDeleteRecord = function (deleteRecordFunction) { | ||
| 194 | self.deleteRecord = deleteRecordFunction; | ||
| 195 | } | ||
| 196 | |||
| 197 | self.setNewRecord = function (newRecordFunction) { | ||
| 198 | self.newRecord = newRecordFunction; | ||
| 199 | } | ||
| 200 | |||
| 201 | self.setAddNewRecord = function (addNewRecordFunction) { | ||
| 202 | self.addNewRecord = addNewRecordFunction; | ||
| 203 | } | ||
| 204 | |||
| 205 | self.setUpdateRecord = function (updateRecordFunction) { | ||
| 206 | self.updateRecord = updateRecordFunction; | ||
| 207 | } | ||
| 208 | |||
| 209 | self.setApplyValidationParameters = function (applyValidationParametersFunction) { | ||
| 210 | self.applyValidationParameters = applyValidationParametersFunction; | ||
| 211 | } | ||
| 212 | |||
| 213 | // Methods | ||
| 214 | self.initialize = function () { | ||
| 215 | // Restore any previous sort order | ||
| 216 | const lastSortField = Cookies.get(self.modelName + "!LastSortField"); | ||
| 217 | const lastSortAscending = Cookies.get(self.modelName + "!LastSortAscending"); | ||
| 218 | |||
| 219 | if (lastSortField === undefined) | ||
| 220 | self.sortField(self.defaultSortField); | ||
| 221 | else | ||
| 222 | self.sortField(lastSortField); | ||
| 223 | |||
| 224 | if (lastSortAscending === undefined) | ||
| 225 | self.sortAscending(self.defaultSortAscending); | ||
| 226 | else | ||
| 227 | self.sortAscending(lastSortAscending === "true"); | ||
| 228 | |||
| 229 | if (self.dataHubIsConnected()) { | ||
| 230 | // Query total record count | ||
| 231 | self.queryRecordCount().done(function (count) { | ||
| 232 | // Update record count observable | ||
| 233 | self.recordCount(count); | ||
| 234 | |||
| 235 | // Force page refresh when record count has been updated | ||
| 236 | const currentPage = self.currentPage(); | ||
| 237 | self._currentPage(0); | ||
| 238 | self.currentPage(currentPage); | ||
| 239 | }); | ||
| 240 | |||
| 241 | // Initialize current record with an empty row | ||
| 242 | self.newRecord().done(function (emptyRecord) { | ||
| 243 | self.deriveObservableRecord(emptyRecord).done(function (observableRecord) { | ||
| 244 | self.currentRecord(observableRecord); | ||
| 245 | }); | ||
| 246 | }); | ||
| 247 | } | ||
| 248 | |||
| 249 | // Initialize column widths array | ||
| 250 | self._columnWidths = []; | ||
| 251 | const columns = $("#recordRow").find("td"); | ||
| 252 | |||
| 253 | for (let i = 0; i < columns.length; i++) { | ||
| 254 | self._columnWidths.push($(columns[i]).width()); | ||
| 255 | } | ||
| 256 | } | ||
| 257 | |||
| 258 | self.calculatePageSize = function (forceRefresh) { | ||
| 259 | // Calculate total number of table rows that will fit within current page height | ||
| 260 | const remainingHeight = calculateRemainingBodyHeight() - | ||
| 261 | $("#contentWell").paddingHeight() - | ||
| 262 | $("#responsiveTableDiv").paddingHeight() - | ||
| 263 | $("#recordsTable").paddingHeight() - | ||
| 264 | $("#pageControlsRow").outerHeight(true); | ||
| 265 | |||
| 266 | // Estimate page size based on height of first record row | ||
| 267 | const firstRow = $("#recordRow"); | ||
| 268 | var pageSize = (remainingHeight / firstRow.outerHeight(true)).truncate(); | ||
| 269 | |||
| 270 | if (!pageSize || isNaN(pageSize) || !isFinite(pageSize) || pageSize < 1) | ||
| 271 | pageSize = 1; | ||
| 272 | |||
| 273 | if (forceRefresh === undefined) | ||
| 274 | forceRefresh = false; | ||
| 275 | |||
| 276 | // Check for dynamic Bootstrap column resizing, in which case we need to refresh data | ||
| 277 | // for cases where binding may be truncating data lengths, see $.fn.truncateToWidth | ||
| 278 | const columns = firstRow.find("td"); | ||
| 279 | var columnWidth; | ||
| 280 | |||
| 281 | for (let i = 0; i < columns.length; i++) { | ||
| 282 | columnWidth = $(columns[i]).width(); | ||
| 283 | |||
| 284 | if (self._columnWidths[i] !== columnWidth) { | ||
| 285 | self._columnWidths[i] = columnWidth; | ||
| 286 | forceRefresh = true; | ||
| 287 | } | ||
| 288 | } | ||
| 289 | |||
| 290 | if (pageSize !== self.currentPageSize() || forceRefresh) { | ||
| 291 | const currentPage = self.currentPage(); | ||
| 292 | |||
| 293 | // Updating page size will validate current page number | ||
| 294 | self.currentPageSize(pageSize); | ||
| 295 | |||
| 296 | // Requery data for page unless current page was dynamically changed | ||
| 297 | // by page size update and has reloaded data already | ||
| 298 | if (currentPage === self.currentPage()) | ||
| 299 | self.queryPageRecords(); | ||
| 300 | } | ||
| 301 | } | ||
| 302 | |||
| 303 | self.calculateUnassignedFieldCount = function () { | ||
| 304 | // Derive unassigned field count based on existence of Bootstrap "has-error" class | ||
| 305 | self.unassignedFieldCount($("#addNewEditDialog div.form-group.has-error").length); | ||
| 306 | } | ||
| 307 | |||
| 308 | self.refreshValidationErrors = function () { | ||
| 309 | // Force re-evaluation of validation errors property | ||
| 310 | self.unassignedFieldCount.valueHasMutated(); | ||
| 311 | |||
| 312 | // Make sure any initial validation error messages are visible | ||
| 313 | self.errors.showAllMessages(); | ||
| 314 | } | ||
| 315 | |||
| 316 | // Convert observable object to a simple Javascript record | ||
| 317 | self.deriveJSRecord = function () { | ||
| 318 | return $.Deferred(function (deferred) { | ||
| 319 | const observableRecord = self.currentRecord(); | ||
| 320 | const promises = []; | ||
| 321 | |||
| 322 | // Allow customization of observable record before conversion | ||
| 323 | $(self).trigger("derivingJSRecord", [observableRecord, promises]); | ||
| 324 | |||
| 325 | $(promises).whenAll().done(function () { | ||
| 326 | deferred.resolve(ko.mapping.toJS(observableRecord)); | ||
| 327 | }); | ||
| 328 | }).promise(); | ||
| 329 | } | ||
| 330 | |||
| 331 | // Convert simple Javascript record to an observable object | ||
| 332 | self.deriveObservableRecord = function (record) { | ||
| 333 | return $.Deferred(function (deferred) { | ||
| 334 | const observableRecord = ko.mapping.fromJS(record); | ||
| 335 | const promises = []; | ||
| 336 | |||
| 337 | // Allow customization of new observable record | ||
| 338 | $(self).trigger("derivingObservableRecord", [observableRecord, promises]); | ||
| 339 | |||
| 340 | $(promises).whenAll().done(function () { | ||
| 341 | // Apply validation binding to current observable record | ||
| 342 | self.errors = ko.validation.group(observableRecord); | ||
| 343 | self.refreshValidationErrors(); | ||
| 344 | deferred.resolve(observableRecord); | ||
| 345 | }); | ||
| 346 | }).promise(); | ||
| 347 | } | ||
| 348 | |||
| 349 | self.setFocusOnInitialField = function () { | ||
| 350 | if (!isEmpty(self.initialFocusField)) | ||
| 351 | $("#" + self.initialFocusField).focus(); | ||
| 352 | } | ||
| 353 | |||
| 354 | self.nextPage = function () { | ||
| 355 | if (self.currentPage() < self.totalPages()) | ||
| 356 | self.currentPage(self.currentPage() + 1); | ||
| 357 | } | ||
| 358 | |||
| 359 | self.previousPage = function () { | ||
| 360 | if (self.currentPage() > 1) | ||
| 361 | self.currentPage(self.currentPage() - 1); | ||
| 362 | } | ||
| 363 | |||
| 364 | self.updateSortOrder = function (fieldName, ascending) { | ||
| 365 | self.sortField(fieldName); | ||
| 366 | self.sortAscending(ascending); | ||
| 367 | self.queryPageRecords(); | ||
| 368 | |||
| 369 | // Save last sort order | ||
| 370 | Cookies.set(self.modelName + "!LastSortField", self.sortField(), { expires: 365 }); | ||
| 371 | Cookies.set(self.modelName + "!LastSortAscending", self.sortAscending().toString(), { expires: 365 }); | ||
| 372 | } | ||
| 373 | |||
| 374 | self.isSortOrder = function (fieldName, ascending) { | ||
| 375 | return self.sortField().toUpperCase() === fieldName.toUpperCase() && self.sortAscending() === ascending; | ||
| 376 | } | ||
| 377 | |||
| 378 | self.queryPageRecords = function () { | ||
| 379 | if (self.dataHubIsConnected()) | ||
| 380 | self.queryRecords(self.sortField(), self.sortAscending(), self.currentPage(), self.currentPageSize()).done(function (records) { | ||
| 381 | $(self).trigger("pageRecordsQueried", [records]); | ||
| 382 | self.pageRecords.removeAll(); | ||
| 383 | self.pageRecords(records); | ||
| 384 | refreshHubDependentControlState(); | ||
| 385 | $("[id='recordRow']").css("visibility", "visible"); | ||
| 386 | $("#loadingDataLabel").hide(); | ||
| 387 | |||
| 388 | // Validate proper page size after any record refresh | ||
| 389 | setTimeout(self.calculatePageSize, 150); | ||
| 390 | }).fail(function (error) { | ||
| 391 | showErrorMessage(error); | ||
| 392 | }); | ||
| 393 | } | ||
| 394 | |||
| 395 | self.removePageRecord = function (record) { | ||
| 396 | if (!self.canDelete()) | ||
| 397 | return; | ||
| 398 | |||
| 399 | if (self.dataHubIsConnected() && confirm("Are you sure you want to delete \"" + record[self.labelField] + "\"?")) { | ||
| 400 | const keyValues = []; | ||
| 401 | |||
| 402 | for (var i = 0; i < self.primaryKeyFields.length; i++) { | ||
| 403 | keyValues.push(record[self.primaryKeyFields[i]]); | ||
| 404 | } | ||
| 405 | |||
| 406 | self.deleteRecord(keyValues).done(function () { | ||
| 407 | self.pageRecords.remove(record); | ||
| 408 | self.initialize(); | ||
| 409 | $(self).trigger("recordDeleted", [record]); | ||
| 410 | showInfoMessage("Deleted record..."); | ||
| 411 | }).fail(function (error) { | ||
| 412 | showErrorMessage(error); | ||
| 413 | }); | ||
| 414 | } | ||
| 415 | } | ||
| 416 | |||
| 417 | self.savePageRecord = function () { | ||
| 418 | switch (self.recordMode()) { | ||
| 419 | case RecordMode.Edit: | ||
| 420 | self.saveEditedRecord(); | ||
| 421 | break; | ||
| 422 | case RecordMode.AddNew: | ||
| 423 | self.saveNewRecord(); | ||
| 424 | break; | ||
| 425 | } | ||
| 426 | } | ||
| 427 | |||
| 428 | self.saveEditedRecord = function () { | ||
| 429 | if (!self.canEdit()) | ||
| 430 | return; | ||
| 431 | |||
| 432 | if (self.dataHubIsConnected()) { | ||
| 433 | self.deriveJSRecord().done(function (record) { | ||
| 434 | self.updateRecord(record).done(function () { | ||
| 435 | self.initialize(); | ||
| 436 | $(self).trigger("recordSaved", [record, false]); | ||
| 437 | showInfoMessage("Saved updated record..."); | ||
| 438 | }).fail(function (error) { | ||
| 439 | showErrorMessage(error); | ||
| 440 | }); | ||
| 441 | }); | ||
| 442 | } | ||
| 443 | } | ||
| 444 | |||
| 445 | self.saveNewRecord = function () { | ||
| 446 | if (!self.canAddNew()) | ||
| 447 | return; | ||
| 448 | |||
| 449 | if (self.dataHubIsConnected()) { | ||
| 450 | self.deriveJSRecord().done(function (record) { | ||
| 451 | self.addNewRecord(record).done(function () { | ||
| 452 | self.initialize(); | ||
| 453 | $(self).trigger("recordSaved", [record, true]); | ||
| 454 | showInfoMessage("Saved new record..."); | ||
| 455 | }).fail(function (error) { | ||
| 456 | showErrorMessage(error); | ||
| 457 | }); | ||
| 458 | }); | ||
| 459 | } | ||
| 460 | } | ||
| 461 | |||
| 462 | self.viewPageRecord = function (record) { | ||
| 463 | self.deriveObservableRecord(record).done(function (observableRecord) { | ||
| 464 | self.recordMode(RecordMode.View); | ||
| 465 | self.currentRecord(observableRecord); | ||
| 466 | $("#addNewEditDialog").modal("show"); | ||
| 467 | }); | ||
| 468 | } | ||
| 469 | |||
| 470 | self.editPageRecord = function (record) { | ||
| 471 | if (!self.canEdit()) | ||
| 472 | return; | ||
| 473 | |||
| 474 | self.deriveObservableRecord(record).done(function (observableRecord) { | ||
| 475 | self.recordMode(RecordMode.Edit); | ||
| 476 | self.currentRecord(observableRecord); | ||
| 477 | $("#addNewEditDialog").modal("show"); | ||
| 478 | }); | ||
| 479 | } | ||
| 480 | |||
| 481 | self.addPageRecord = function () { | ||
| 482 | if (!self.canAddNew()) | ||
| 483 | return; | ||
| 484 | |||
| 485 | if (self.dataHubIsConnected()) { | ||
| 486 | self.newRecord().done(function (emptyRecord) { | ||
| 487 | // Raise event to allow any new record initialization | ||
| 488 | $(self).trigger("newRecord", [emptyRecord]); | ||
| 489 | |||
| 490 | self.deriveObservableRecord(emptyRecord).done(function (observableRecord) { | ||
| 491 | self.recordMode(RecordMode.AddNew); | ||
| 492 | self.currentRecord(observableRecord); | ||
| 493 | $("#addNewEditDialog").modal("show"); | ||
| 494 | }); | ||
| 495 | }); | ||
| 496 | } | ||
| 497 | } | ||
| 498 | |||
| 499 | self.cancelPageRecord = function () { | ||
| 500 | if (!self.isDirty() || confirm("Are you sure you want to discard unsaved changes?")) | ||
| 501 | $("#addNewEditDialog").modal("hide"); | ||
| 502 | } | ||
| 503 | }; | ||
| 504 | |||
| 505 | // Define page scoped view model instance | ||
| 506 | var viewModel = new PagedViewModel(); | ||
| 507 | |||
| 508 | (function ($, viewPort) { | ||
| 509 | $("#bodyContainer").addClass("fill-height"); | ||
| 510 | |||
| 511 | $("#titleText").html("Records: <span data-bind='text: recordCount'>calculating...</span>"); | ||
| 512 | |||
| 513 | $("#firstPageButton").click(function () { | ||
| 514 | viewModel.currentPage(1); | ||
| 515 | }); | ||
| 516 | |||
| 517 | $("#previousPageButton").click(function () { | ||
| 518 | viewModel.previousPage(); | ||
| 519 | }); | ||
| 520 | |||
| 521 | $("#nextPageButton").click(function () { | ||
| 522 | viewModel.nextPage(); | ||
| 523 | }); | ||
| 524 | |||
| 525 | $("#lastPageButton").click(function () { | ||
| 526 | viewModel.currentPage(viewModel.totalPages()); | ||
| 527 | }); | ||
| 528 | |||
| 529 | $(window).on("hubConnected", function (event) { | ||
| 530 | viewModel.dataHubIsConnected(true); | ||
| 531 | viewModel.initialize(); | ||
| 532 | }); | ||
| 533 | |||
| 534 | $(window).on("hubDisconnected", function (event) { | ||
| 535 | viewModel.dataHubIsConnected(false); | ||
| 536 | }); | ||
| 537 | |||
| 538 | $(window).on("messageVisibiltyChanged", function (event) { | ||
| 539 | viewModel.calculatePageSize(); | ||
| 540 | }); | ||
| 541 | |||
| 542 | $("#addNewEditDialog").on("shown.bs.modal", function () { | ||
| 543 | viewModel.setFocusOnInitialField(); | ||
| 544 | $("[data-toggle='tooltip']").tooltip(); | ||
| 545 | }); | ||
| 546 | |||
| 547 | $(window).resize( | ||
| 548 | viewPort.changed(function () { | ||
| 549 | viewModel.calculatePageSize(); | ||
| 550 | })); | ||
| 551 | |||
| 552 | viewModel.calculatePageSize(); | ||
| 553 | |||
| 554 | ko.validation.rules.pattern.message = "Invalid"; | ||
| 555 | |||
| 556 | ko.validation.init({ | ||
| 557 | registerExtenders: true, | ||
| 558 | messagesOnModified: true, | ||
| 559 | insertMessages: true, | ||
| 560 | parseInputAttributes: true, | ||
| 561 | allowHtmlMessages: true, | ||
| 562 | messageTemplate: null, | ||
| 563 | grouping: { deep: true, observable: true, live: true } | ||
| 564 | }, true); | ||
| 565 | |||
| 566 | ko.applyBindings(viewModel); | ||
| 567 | |||
| 568 | })(jQuery, ResponsiveBootstrapToolkit); | ||