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+
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+}
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+
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);

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