History

(1) GSF.Security: Added modeled security tables. (2) GSF.Web: Added security, data context, view template and scripts related to modeled table operations

Ritchie Carroll committed 10 years ago

Commit a02edca1 parent 46782aa32 files · +5897 −0Browse files

Showing the changes to Source/Libraries/GSF.Web/Model/Scripts (2 of 32 files). Show all

Unified · Split

2 files
  1. Source/Libraries/GSF.Web/Model/Scripts/gsf.web.client.js +667 −0
  2. Source/Libraries/GSF.Web/Model/Scripts/pagedViewModel.js +568 −0
Source/Libraries/GSF.Web/Model/Scripts/gsf.web.client.js+667−0
@@ -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
28const isIE = detectIE();
29var 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
40function notNull(value, nonNullValue) {
41 return value || (nonNullValue || "");
42}
43
44function 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
72function isNumber(val) {
73 return !isNaN(parseFloat(val)) && isFinite(val);
74}
75
76function 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
84function getBool(val) {
85 const num = +val;
86 return !isNaN(num) ? !!num : !!String(val).toLowerCase().replace(false, "");
87}
88
89// Number functions
90Number.prototype.truncate = function() {
91 if (typeof Math.trunc != "function")
92 return parseInt(this.toString());
93
94 return Math.trunc(this);
95}
96
97Number.prototype.padLeft = function (totalWidth, paddingChar) {
98 return this.truncate().toString().padLeft(totalWidth, paddingChar || "0");
99}
100
101Number.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.
110function 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
144Array.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
149function 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
288Dictionary.fromObservableDictionary = function (observableDictionary) {
289 var dictionary = new Dictionary();
290 dictionary.pushAll(observableDictionary.toJSON());
291 return dictionary;
292}
293
294// String functions
295function isEmpty(str) {
296 return !str || String(str).length === 0;
297}
298
299String.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
308String.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
314String.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
321String.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
328String.prototype.countOccurrences = function (searchString) {
329 return (this.split(searchString).length - 1);
330}
331
332if (!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
339String.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.
345String.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
485Date.prototype.addDays = function (days) {
486 return new Date(this.setDate(this.getDate() + days));
487}
488
489Date.prototype.toUTC = function () {
490 this.setMinutes(this.getMinutes() - this.getTimezoneOffset());
491 return this;
492}
493
494Date.prototype.daysBetween = function (startDate) {
495 const millisecondsPerDay = 24 * 60 * 60 * 1000;
496 return (this.toUTC() - startDate.toUTC()) / millisecondsPerDay;
497}
498
499String.prototype.toDate = function () {
500 return new Date(Date.parse(this));
501};
502
503String.prototype.formatDate = function (format, utc) {
504 return formatDate(this.toDate(), format, utc);
505};
506
507Date.prototype.formatDate = function (format, utc) {
508 return formatDate(this, format, utc);
509};
510
511function 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}
Source/Libraries/GSF.Web/Model/Scripts/pagedViewModel.js+568−0
@@ -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
27var RecordMode = {
28 View: 0,
29 Edit: 1,
30 AddNew: 2
31}
32
33// Define paged view model class
34function 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
506var 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);

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t