PageSourceSearch

https://projectr.tv/js/pagemanager.js?v00136

js projectr.tv collected 2026-10-02 05:08:09 UTC 119,344 bytes, 2,779 lines download raw bytes

1"use strict";
2
3class PageManager {
4    constructor(model, anonUserId) {
5        this.ApplicationId = model.applicationId;
6        this.PageId = model.pageId;
7        this.Url = encodeURI(window.location.href);
8        this.ObjectId = model.objectId;
9        this.OriginalObjectId = model.originalObjectId;
10        this.OriginalParentObjectId = model.originalParentObjectId;
11        this.LoginProfileId = model.loginProfileId;
12        this.IpAddress = model.ipAddress;
13        this.Unpub = model.unpub;
14        this.SectionToLoadCount = 0;
15        this.SectionLoadedCount = 0;
16        this.WidgetsToLoadCount = 0;
17        this.WidgetsLoadedCount = 0;
18        this.AnonUserId = anonUserId;
19        this.CartCount = -1;
20        this.CartTotal = -1.0;
21        this.GetCartCount(); //This will be populated the first time cart count is retrieved by something and available thereafter.
22        this.AutoSuggestTitles = new Array();
23        this.Institutions = new Array();
24        this.eaPropToggles = new Array();
25        if (model.model) {
26            this.RegisteredOrganizationId = model.model.registeredOrganizationId;
27            this.RegisteredOrg = model.model.registeredOrg;
28            this.AppName = model.model.appName;
29            if (model.model.object != null) {
30                this.HasAssociatedObject = true;
31                this.AssociatedObjectPrimaryTitle = model.model.primaryTitle;
32                this.DataType = model.model.dataType;
33                this.YOP = model.model.yop;
34            }
35            else {
36                this.HasAssociatedObject = false;
37                this.AssociatedObjectPrimaryTitle = "";
38                this.DataType = model.model.dataType;
39                this.YOP = model.model.yop;
40            }
41        }
42        //This gets and stores url parameters that are on the page for use in whatever code you need.
43        this.UrlParams = new URLSearchParams(window.location.search);
44        //Execute any on load TOAST.
45        //New feature where if query string parameters for TOAST exist, we can display the toast.
46        if (this.UrlParams.get("TOAST") != null) {
47            //parse the toast message.
48            //_pageManager.Toast("Link copied to clipboard", "success", 3000);
49            //_pageManager.Toast("Info test", "info", 3000);
50            //_pageManager.Toast("Warning test", "warning", 3000);
51            //_pageManager.Toast("Error test", "error", 3000);
52            var toastParamSplit = this.UrlParams.get("TOAST").split('|');
53            //0 = Message
54            //1 = Level
55            //2 time to display
56            this.Toast(toastParamSplit[0], toastParamSplit[1], toastParamSplit[2]);
57        }
58
59    }
60
61    ////Page Scaffolding
62    ScaffoldPage() {
63        let obj = this;
64
65        //To load synchronously, first we need to get the count of sections on page to load.
66        //obj.SectionToLoadCount = $('.sectionPlaceHolder').length;
67        const $sections = $('.sectionPlaceHolder').not('[data-ssr="1"]');
68        obj.SectionToLoadCount = $sections.length;
69
70        if (obj.SectionToLoadCount == 0) {
71            obj.SectionFinished();
72        }
73        else {
74            //Sections the server already rendered (data-ssr="1") are left as they are - their request has nothing the server didn't have.
75            //(Server-rendered widgets are still re-fetched by LoadWidget: the browser sends things the server render doesn't have,
76            //such as the page's objectId, AnonUserId and data-properties, so its version can differ.)
77            $sections.each(function () {
78
79                var $section = $(this);
80                var sectionId = $section.attr('data-sectionId');
81                obj.AjaxWithRetry({
82                    type: "GET",
83                    url: "/Page/SectionGet?sectionId=" + sectionId + "&applicationId=" + obj.ApplicationId + "&unpub=" + obj.Unpub,
84                    async: true,
85                    dataType: 'json',
86                    success: function (data) {
87
88                     
89
90                        $section.html(data.sectionModel.html);
91                        //Increment section loaded count, then call routine to see if all sections loaded yet or not.
92
93                        obj.SectionLoadedCount++;
94                        obj.SectionFinished();
95                    },
96                    error: function () {
97                        $section.html("Error loading section");
98                        obj.SectionLoadedCount++;
99                        obj.SectionFinished();
100                    }
101                });
102            });
103        }
104
105        obj.AllLoadActions();
106
107        obj.PostLoadBindings();
108
109        return true;
110    }
111
112    //This routine gets called each time a section finishes loading.  When all have finished loading, it will load widgets
113    SectionFinished() {
114        let obj = this;
115        if (obj.SectionLoadedCount >= obj.SectionToLoadCount) {
116
117            //All sections loaded, so load any widgets
118
119            obj.WidgetsToLoadCount = $('.widgetPlaceHolder').length;
120            if (obj.WidgetsToLoadCount == 0) {
121                //Got no widgets
122                obj.WidgetLoaded();
123            }
124            else {
125
126                /*Load Widgets    ASYNC*/
127
128                if ($('.widgetPlaceHolder[data-priority]').length == 0) {
129                    //No priority widgets, so do them all
130                    $('.widgetPlaceHolder').each(function () {
131                        var $widgetPlaceHolder = $(this);
132                        $widgetPlaceHolder.attr('data-objectId', obj.ObjectId);
133                        obj.LoadWidget($widgetPlaceHolder);
134                    });
135                }
136                else {
137
138                    var widgetDelay = 750;
139
140                    $('.widgetPlaceHolder[data-priority="1"]').each(function () {
141                        var $widgetPlaceHolder = $(this);
142                        $widgetPlaceHolder.attr('data-objectId', obj.ObjectId);
143                        obj.LoadWidget($widgetPlaceHolder);
144                    });
145
146                    setTimeout(function () {
147                        $('.widgetPlaceHolder[data-priority="2"]').each(function () {
148                            var $widgetPlaceHolder = $(this);
149                            $widgetPlaceHolder.attr('data-objectId', obj.ObjectId);
150                            obj.LoadWidget($widgetPlaceHolder);
151                        });
152
153                        setTimeout(function () {
154                            $('.widgetPlaceHolder[data-priority="3"]').each(function () {
155                                var $widgetPlaceHolder = $(this);
156                                $widgetPlaceHolder.attr('data-objectId', obj.ObjectId);
157                                obj.LoadWidget($widgetPlaceHolder);
158                            });
159
160                            setTimeout(function () {
161                                $('.widgetPlaceHolder:not([data-priority="1"]):not([data-priority="2"]):not([data-priority="3"])').each(function () {
162                                    var $widgetPlaceHolder = $(this);
163                                    $widgetPlaceHolder.attr('data-objectId', obj.ObjectId);
164                                    obj.LoadWidget($widgetPlaceHolder);
165                                });
166
167                                try {
168                                    PostLoadActions();
169                                }
170                                catch (err) {
171                                    //ignore
172                                }
173
174                            }, widgetDelay);
175
176                        }, widgetDelay);
177
178                    }, widgetDelay);
179
180                }
181
182            }
183
184            //Make skiplinks if needed
185            obj.MakeSkipLinks();
186        }
187
188        obj.PostLoadBindings();
189    }
190
191    LoadWidget($widgetPlaceHolder) {
192        let obj = this;
193
194
195
196        //Get HTML attributes
197        var widgetId = $widgetPlaceHolder.attr('data-widgetId');
198
199        var widgetObjectId = obj.ObjectId;
200
201        //On occasion, we may want the widget to operate on a different ObjectId than the one on the page, for example Quizzes, where the page deals with the LA/LAS Objects etc
202        var overridingObjectId = $widgetPlaceHolder.attr('data-objectId');
203        if (typeof overridingObjectId !== 'undefined' && overridingObjectId != null) {
204            widgetObjectId = overridingObjectId;
205        }
206
207
208        var widgetAutoLoad = $widgetPlaceHolder.attr('data-autoLoad');
209        var widgetObjectIdsFilter = $widgetPlaceHolder.attr('data-objectIdsFilter');
210        var widgetGalleryIdFilter = $widgetPlaceHolder.attr('data-galleryIdFilter');
211        var widgetObjectDefinitionIdsFilter = $widgetPlaceHolder.attr('data-objectDefinitionIdsFilter');
212        var widgetHardTypesFilter = $widgetPlaceHolder.attr('data-hardTypesFilter');
213        var widgetCategoryObjectIdsFilter = $widgetPlaceHolder.attr('data-categoryObjectIdsFilter');
214        var widgetTagIdsFilter = $widgetPlaceHolder.attr('data-tagIdsFilter');
215        var widgetListIdsFilter = $widgetPlaceHolder.attr('data-listIdsFilter');
216        var widgetSearchTermFilter = $widgetPlaceHolder.attr('data-searchTermFilter') ?? "";
217        var widgetCustomFilter = $widgetPlaceHolder.attr('data-customFilter') ?? "";
218        var widgetOrderBy = $widgetPlaceHolder.attr('data-orderBy');
219        var widgetFavorites = $widgetPlaceHolder.attr('data-favorites');
220        var widgetRecentlyWatched = $widgetPlaceHolder.attr('data-RecentlyWatched');
221        var widgetWatchedNotFinished = $widgetPlaceHolder.attr('data-WatchedNotFinished');
222        if (!widgetWatchedNotFinished) {
223            //Backward compatibaility for typo that may exist in existing widgets
224            widgetWatchedNotFinished = $widgetPlaceHolder.attr('data-WatchedNotFinshed');
225        }
226
227        var widgetLearningNotComplete = $widgetPlaceHolder.attr('data-learningNotComplete');
228        var widgetLearningComplete = $widgetPlaceHolder.attr('data-learningComplete');
229        var widgetTake = $widgetPlaceHolder.attr('data-Take');
230        var widgetMeta = $widgetPlaceHolder.attr('data-meta');
231        var widgetSearchResultCallBack = $widgetPlaceHolder.attr('data-SearchResultCallBack'); //This gets set as the call back for a search results widget which typically is calling back to the paging parent widget to set search results.
232        var widgetTargetWidgetId = $widgetPlaceHolder.attr('data-TargetWidgetId');
233        var widgetHeaderTitle = ($widgetPlaceHolder.attr('data-widgetheadertitle') ?? $widgetPlaceHolder.attr('data-title')) ?? "";
234        var widgetHeaderUrl = $widgetPlaceHolder.attr('data-WidgetHeaderUrl') ?? "";
235        var overrideSafeId = $widgetPlaceHolder.attr('data-overridesafeid') ?? widgetId;
236        var relatedObjectDefinitionsFilter = $widgetPlaceHolder.attr('data-relatedObjectDefinitionsFilter');
237        var widgetAuthedOnly = $widgetPlaceHolder.attr('data-authedonly');
238        var widgetPublicOnly = $widgetPlaceHolder.attr('data-publiconly');
239        var widgetMostWatched = $widgetPlaceHolder.attr('data-MostWatched');
240        var widgetSearchType = $widgetPlaceHolder.attr('data-SearchType') ?? "";
241        var widgetFilterOptions = $widgetPlaceHolder.attr('data-FilterOptions') ?? "";
242        var widgetSpecificListSearch = $widgetPlaceHolder.attr('data-SpecificListSearch') ?? "";
243        var widgetCarouselAutoScroll = $widgetPlaceHolder.attr('data-carouselautoscroll') ?? "";
244        var widgetProperties = $widgetPlaceHolder.attr('data-properties') ?? "";
245        var widgetLoad = $widgetPlaceHolder.attr('data-Load') ?? "";
246
247
248        var widgetRequestModel = {
249            WidgetId: widgetId,
250            ApplicationId: obj.ApplicationId,
251            ObjectId: widgetObjectId,
252            EntitySummary: '',
253            AutoLoad: widgetAutoLoad,
254            ObjectIdsFilter: widgetObjectIdsFilter,
255            GalleryIdFilter: widgetGalleryIdFilter,
256            ObjectDefinitionIdsFilter: widgetObjectDefinitionIdsFilter,
257            HardTypesFilter: widgetHardTypesFilter,
258            CategoryObjectIdsFilter: widgetCategoryObjectIdsFilter,
259            ListIdsFilter: widgetListIdsFilter,
260            TagIdsFilter: widgetTagIdsFilter,
261            SearchTermFilter: widgetSearchTermFilter,
262            CustomFilter: widgetCustomFilter,
263            OrderBy: widgetOrderBy,
264            Favorites: widgetFavorites,
265            RecentlyWatched: widgetRecentlyWatched,
266            WatchedNotFinished: widgetWatchedNotFinished,
267            LearningNotComplete: widgetLearningNotComplete,
268            LearningComplete: widgetLearningComplete,
269            Take: widgetTake,
270            Unpub: obj.Unpub,
271            AnonUserId: obj.AnonUserId,
272            Meta: widgetMeta,
273            SearchResultCallBack: widgetSearchResultCallBack,
274            TargetWidgetId: widgetTargetWidgetId,
275            WidgetHeaderTitle: widgetHeaderTitle,
276            WidgetHeaderUrl: widgetHeaderUrl,
277            OverrideSafeId: overrideSafeId,
278            RelatedObjectDefinitionsFilter: relatedObjectDefinitionsFilter,
279            AuthedOnly: widgetAuthedOnly,
280            PublicOnly: widgetPublicOnly,
281            MostWatched: widgetMostWatched,
282            SearchType: widgetSearchType,
283            FilterOptions: widgetFilterOptions,
284            SpecificListSearch: widgetSpecificListSearch,
285            CarouselAutoScroll: widgetCarouselAutoScroll,
286            Properties: widgetProperties,
287            Load: widgetLoad
288        };
289
290        var safeId = obj.GetSafeId(overrideSafeId);
291        var thisWidget = $widgetPlaceHolder;
292
293        //Routine to prevent accidental double loading.
294        if ($(thisWidget).hasClass('widget-loaded')) {
295            return;
296        }
297        else {
298            $(thisWidget).addClass('widget-loaded');
299        }
300
301        obj.AjaxWithRetry({
302            type: "Post",
303            url: "/Page/WidgetGet",
304            async: true,
305            data: widgetRequestModel,
306            dataType: 'json',
307            success: function (data) {
308
309                if ($(thisWidget)[0].hasAttribute("data-removewidget")) {
310                    $(thisWidget).before(data.widgetModel.html);
311                }
312                else {
313                    $widgetPlaceHolder.html(data.widgetModel.html);
314                }
315
316           
317                try {
318                    //Call the widget specific Javascript function
319                    //Function name will be PostLoadActions_{safeId} - this avoids duplication of events ie if more than 1 per page
320                    //safeId will be the converted widgetId; Javascript Function names don't allow "-", so they're replaced with "_"
321                    setTimeout(function () {
322                        try {
323                           //Check if function exists before calling.
324                           if (typeof window["PostLoadActions_" + safeId] === "function") {
325                               window["PostLoadActions_" + safeId](widgetId);
326                           }
327                        }
328                        catch (err) {
329                            //ignore - as its not mandatory
330                           console.log("Post Load Actions Error from Page Manager");
331                           console.log("Calling Widget");
332                           console.log(widgetId);
333                            console.log(err);
334                        }
335
336                        //update next, we want to add any child widgets to the queue.  These are widgets within widgets
337                        //that need to load after the parent loads.
338                        if ($(thisWidget)[0].hasAttribute("data-removewidget")) {
339                            //We need to do the parent.
340                            var thisWidgetParent = $(thisWidget).parent();
341                            $(thisWidget).remove();
342                            $(thisWidgetParent).find('.widgetPlaceHolder').each(function () {
343                                obj.LoadWidget($(this));
344                            });
345                        }
346                        else {
347                            $(thisWidget).find('.widgetPlaceHolder').each(function () {
348                                obj.LoadWidget($(this));
349                            });
350                        }
351
352                        obj.PostLoadBindings();
353
354                    }, 10);
355                }
356                catch (err) {
357                    //ignore
358                    console.log("Page Manager Widget Load Error")
359                    console.log(err);
360                }
361
362                obj.WidgetLoaded();
363            },
364            error: function () {
365                $widgetPlaceHolder.html("Error Loading Widget");
366                obj.WidgetLoaded();
367            }
368        });
369
370    }
371
372    //This routine gets called each time a widget gets loaded.  When all have finished loading, it will run accessibility
373    WidgetLoaded() {
374        let obj = this;
375
376        obj.WidgetsLoadedCount++;
377
378        if (obj.WidgetsLoadedCount >= obj.WidgetsToLoadCount) {
379            setTimeout(function () {
380                obj.MakePageAccessible();
381            }, 100);
382        }
383    }
384
385    PostLoadBindings() {
386        if (typeof SiteWideBinding !== 'undefined' && SiteWideBinding != null) {
387            SiteWideBinding();
388        }
389
390
391        if (typeof BindSiteWideMessaging != 'undefined' && BindSiteWideMessaging != null) {
392            BindSiteWideMessaging();
393        }
394    }
395
396    GetSafeId(id) {
397        //Used to append WidgetId (for example) to Javascript functions, to avoid duplication.
398        //Javascript Function names don't allow "-", so they're replaced with "_"
399        return id.replaceAll("-", "_").toLowerCase();
400    }
401
402    //Sends a $.ajax request, and if the server says it's busy (429/503 from MediaSite's rate limiting), tries it again - up to 2 more times.
403    //Waits for the server's Retry-After time plus a random extra, so a page's widgets don't all retry at the same moment.
404    //Any other error, or still busy after the retries, goes to the caller's error handler as before.
405    AjaxWithRetry(ajaxOptions, retriesLeft = 2) {
406        let obj = this;
407        let callerError = ajaxOptions.error;
408
409        $.ajax($.extend({}, ajaxOptions, {
410            error: function (xhr) {
411                if (retriesLeft > 0 && (xhr.status === 429 || xhr.status === 503)) {
412                    var retryAfterSeconds = parseInt(xhr.getResponseHeader("Retry-After"), 10);
413                    if (isNaN(retryAfterSeconds) || retryAfterSeconds < 1) {
414                        retryAfterSeconds = 2;
415                    }
416                    retryAfterSeconds = Math.min(retryAfterSeconds, 10);
417
418                    var delayMs = (retryAfterSeconds + Math.random() * retryAfterSeconds) * 1000;
419                    setTimeout(function () {
420                        obj.AjaxWithRetry(ajaxOptions, retriesLeft - 1);
421                    }, delayMs);
422                    return;
423                }
424
425                if (typeof callerError === "function") {
426                    callerError.apply(this, arguments);
427                }
428            }
429        }));
430    }
431
432    //Widget Events
433    RefreshWidget(targetWidgetId) {
434        var obj = this;
435
436        var $widgetPlaceHolder = $('.widgetPlaceHolder[data-widgetId=' + targetWidgetId + ']');
437
438        obj.LoadWidget($widgetPlaceHolder);
439    }
440
441    TriggerWidgetEvent(targetWidgetId, event, params) {
442        var obj = this;
443
444        try {
445
446            //Make sure we've got a widget with this Id on the page'
447            var $widgetPlaceHolder = $('.widgetPlaceHolder[data-widgetId=' + targetWidgetId + ']');
448
449            //Now call the Widget-specific functions, passing on any params
450            if ($widgetPlaceHolder) {
451                //ExternalTrigger(targetWidgetId, event.toLowerCase(), params);
452                //Call the widget specific Javascript function
453                //Function name will be ExternalTrigger_{safeId} - this avoids duplication of events ie if more than 1 per page
454                //safeId will be the converted widgetId; Javascript Function names don't allow "-", so they're replaced with "_"
455                //That event will locally respond to the actual event name (event - which will always be expected to be lowercase)
456                window['ExternalTrigger_' + obj.GetSafeId(targetWidgetId)](targetWidgetId, event.toLowerCase(), params);
457            }
458        }
459        catch (err) { }
460
461    }
462
463    //Login (Cadmore Login)
464    Login() {
465        let obj = this;
466
467        window.location = location.origin + "/Authenticate/ConnectToAuth?applicationId=" + obj.ApplicationId + "&loginProfileId=" + obj.LoginProfileId + "&ipAddress=" + obj.IpAddress + "&returnUrl=" + obj.Url;
468    }
469
470    //Register (Cadmore Login) - similar to Login, but auto goes to 'User Register' mode
471    Register() {
472        let obj = this;
473
474        window.location = location.origin + "/Authenticate/ConnectToAuthRegister?applicationId=" + obj.ApplicationId + "&loginProfileId=" + obj.LoginProfileId + "&ipAddress=" + obj.IpAddress + "&returnUrl=" + obj.Url;
475    }
476
477    //Login OAuth (Client Specific)
478    LoginOAuth() {
479        let obj = this;
480
481        $.post("/Authenticate/RedirectToOAuthUrl", { loginProfileId: obj.LoginProfileId }, function (data) {
482            window.location = data.url;
483        });
484    }
485
486    //Login OIDC (Client Specific)
487    LoginOidc() {
488        let obj = this;
489
490        $.post("/Authenticate/RedirectToOidcUrl", { loginProfileId: obj.LoginProfileId }, function (data) {
491            window.location = data.url;
492        });
493    }
494
495    //Login SSO
496    //creates the idp url and redirects the user to the SAML provider, who should then Authenticate user (if not already authenticated) and send SAML Response to {appUrl}/SAML/AssertionService which'll get redirected to {login.web}/SAML/AssertionService
497    LoginSSO() {
498        let obj = this;
499
500        $.post("/Authenticate/RedirectToSSOUrl", { loginProfileId: obj.LoginProfileId }, function (data) {
501            window.location = data.url;
502        });
503    }
504
505    Logout(entityId) {
506        //if entity = null, logs everything out
507
508        let obj = this;
509
510        window.location = location.origin + "/Authenticate/ConnectToAuthLogout?applicationId=" + obj.ApplicationId + "&loginProfileId=" + obj.LoginProfileId + "&entityId=" + (entityId ?? "") + "&returnUrl=" + obj.Url;
511    }
512
513    //LoginIp
514    LoginIp() {
515        //Basically, clears out lastIp checked, and reloads page - which should ultimately trigger IP check on next page load (PageController)
516        let obj = this;
517        $.post("/Authenticate/ResetLastIp", null, function (data) {
518            obj.ReloadPage("Your IP Authentication has been refreshed|info|3000");
519        });
520    }
521
522    ReloadPage(toast = "") {
523        let obj = this;
524        $("#iframeLogin").remove();
525
526
527        if (toast !== "") {
528            var url = new URL(location.href);
529            url.searchParams.set('TOAST', toast);
530            location.assign(url.search);
531        }
532        else {
533            window.location.reload();
534        }
535    }
536
537    ReloadPageWithoutQueryStringParameters() {
538        var url = window.location.origin + window.location.pathname;
539        window.location.href = url;
540    }
541
542    //RefreshLoginContext
543    RefreshLoginContext() {
544        $.post("/Page/RefreshLoginContext", null, null);
545    }
546
547    Authed() {
548        let obj = this;
549        //Kill the iframe.  Firefox bug.
550        $("#iframeLogin").remove();
551        $("#pageAuthModal").remove();
552        window.location.reload();
553    }
554
555    //Toasts/Alerts
556    Alert(message) {
557        alert('***pagemanager*** ' + message);
558    }
559
560    Toast(text, type, durationMilliSeconds) {
561
562     
563        //type = "success", "info, "warning or "error
564        //type determines the color and icon, typically: success=green, info=blue, warning=yellow, error=red, but ultimately set in client-specific css
565
566        //Examples;
567        //_pageManager.Toast("Link copied to clipboard", "success", 3000);
568        //_pageManager.Toast("Info test", "info", 3000);
569        //_pageManager.Toast("Warning test", "warning", 3000);
570        //_pageManager.Toast("Error test", "error", 3000);
571
572        var fadeTime = 1000;    //How long it takes to fade out after being removed
573
574        //Get, or create the toast container - doesn't really matter where it is, as client-specific css will control it
575        var $toastContainer = $('.toast-container');
576        if ($toastContainer.length === 0) {
577            $('body').append($('<div>').addClass('toast-container'));
578        }
579
580        //Configure the closing/fade-out of the toast
581        var closeToast = function ($toast) {
582            //Add the fading class and remove the 'click' binding
583            $toast
584                .addClass('fading')
585                .off('click');
586
587            // Fade the toast message out and remove it
588            $toast.fadeOut(fadeTime, function () {
589                $toast.remove();
590            });
591        }
592
593        //Create the type-specific icon
594        var iconName = '';
595        switch (type) {
596            case 'success':
597                iconName = 'fa-circle-check';
598                break;
599            case 'info':
600                iconName = 'fa-circle-info';
601                break;
602            case 'warning':
603                iconName = 'fa-triangle-exclamation';
604                break;
605            case 'error':
606                iconName = 'fa-circle-exclamation';
607                break;
608        }
609        var $icon = "";
610        if (iconName.length > 0) { $icon = $('<div>').addClass('toast-icon').append($('<i>').addClass('fa ' + iconName)) };
611
612        //Build the toast message
613        var $toast = $('<div>')
614            .addClass('toast ' + type)
615            .append($('<div>')
616                .addClass('toast-inner')
617                .append($icon)
618                .append($('<div>')
619                    .addClass('text')
620                    .text(text)));
621
622        //Add message to toast-container
623        $toastContainer.append($toast);
624
625        //Add event listener to close toast if clicked
626        $toast.on('click', function () { closeToast($toast); });
627
628        // Automatic close after durationMilliSeconds
629        setTimeout(function () { closeToast($toast); }, durationMilliSeconds);
630
631    }
632
633
634    // Generic Dialog Modal
635    // Example use
636    // _pageManager.genericDialogModal_Open('Confirm Delete', 'Are you sure you want to DELETE this?', 'No', '', 'Yes', 'deleteSomething(`id`)');
637    // _pageManager.genericDialogModal_Open("Confirm Delete", "Are you sure you want to DELETE this clip?", "No", "", "Yes", "$('#clipDeleteForm').submit();");
638    genericDialogModal_Open(title, prompt, button1Text, button1OnClick, button2Text, button2OnClick) {
639        $("#genericDialogModalTitle").html(title);
640        $("#genericDialogModalPrompt").html(prompt);
641
642        if (button1Text && button1Text.length) {
643            $("#genericDialogModalButton1").html(button1Text);
644            $("#genericDialogModalButton1").attr("aria-label", button1Text);
645            $("#genericDialogModalButton1").attr("onclick", "_pageManager.genericDialogModal_Close();" + (button1OnClick ?? ""));
646            $("#genericDialogModalButton1").show();
647        }
648        else {
649            $("#genericDialogModalButton1").html("");
650            $("#genericDialogModalButton1").attr("aria-label", "");
651            $("#genericDialogModalButton1").attr("onclick", "");
652            $("#genericDialogModalButton1").hide();
653        }
654
655        if (button2Text && button2Text.length) {
656            $("#genericDialogModalButton2").html(button2Text);
657            $("#genericDialogModalButton2").attr("aria-label", button1Text);
658            $("#genericDialogModalButton2").attr("onclick", "_pageManager.genericDialogModal_Close();" + (button2OnClick ?? ""));
659            $("#genericDialogModalButton2").show();
660        }
661        else {
662            $("#genericDialogModalButton2").html("");
663            $("#genericDialogModalButton2").attr("aria-label", "");
664            $("#genericDialogModalButton2").attr("onclick", "");
665            $("#genericDialogModalButton2").hide();
666        }
667
668        //Show Overlay
669        $(".genericDialogModal_overlay").css("opacity", "0.5");
670        $(".genericDialogModal_overlay").css("visibility", "visible");
671
672        //Show Dialog
673        $("#genericDialogModal").show();
674    }
675
676    genericDialogModal_Close() {
677        //Reset Values
678        $("#genericDialogModalTitle").html("");
679        $("#genericDialogModalPrompt").html("");
680        $("#genericDialogModalButton1").html("");
681        $("#genericDialogModalButton1").attr("aria-label", "");
682        $("#genericDialogModalButton1").attr("onclick", "");
683        $("#genericDialogModalButton1").hide();
684        $("#genericDialogModalButton2").html("");
685        $("#genericDialogModalButton2").attr("aria-label", "");
686        $("#genericDialogModalButton2").attr("onclick", "");
687        $("#genericDialogModalButton2").hide();
688
689        //Show Overlay
690        $(".genericDialogModal_overlay").css("opacity", "0");
691        $(".genericDialogModal_overlay").css("visibility", "hidden");
692
693        //Hide Dialog
694        $("#genericDialogModal").hide();
695    }
696
697
698    //Cadmore Password Reset
699    PasswordReset() {
700        $("#modalPasswordReset").show();
701    }
702
703    PasswordResetRedirect() {
704        var url = "/Authenticate/PasswordReset";
705        window.location.href = url;
706
707    }
708
709
710
711    //Favorites
712    ///Callback is  a functon to call after the favorite or unfavorite is done.  it will pass True if just favorited and False if not.
713    //RegisterForEventObjectId is the evnet object id when favoriting is also tied to Reminders/RSVP feature.
714    FavoriteToggle(element, entityId, objectId, toggle = false, registerForEventObjectId = "", callback = null) {
715
716        var obj = this;
717        try {
718            var $element = $(element);
719            if ($element) {
720
721                //This function can be called by any element with toggle true, and if so, will set the toggle status to the opposite of whatever it is now.
722                //This is useful to just call this function favorite without more jscript needed to set status first.  Not always applicable.
723                if (toggle) {
724                    if ($element.hasClass('isfavorite')) {
725                        $element.removeClass('isfavorite');
726                    }
727                    else {
728                        $element.addClass('isfavorite');
729                    }
730                }
731
732
733                var fav = false;
734                //Determine desired Favorite status (ie value after the current toggle action)
735                //Different widgets use different methods, ie ACAMH uses checkbox, Endocrine use buttons (using a class isfavorite)
736
737                if ($element.is(":checkbox")) {
738                    fav = ($element.prop('checked'));
739                } else {
740                    fav = $element.hasClass("isfavorite");
741                }
742
743                //Save
744                $.post("/Page/FavoriteSet", { ApplicationId: obj.ApplicationId, EntityId: entityId, ObjectId: objectId, IsFavorite: fav, RegisterForEventObjectId: registerForEventObjectId }, function (data) {
745
746                    if (data) {
747
748                        //Saved ok, so update UI attribute of other instances of this object
749                        if (fav) {
750                            //Add favorite to
751                            //$element.attr("data-isfavorite", "true");
752                            $('.check-fav-' + objectId).prop('checked', true);
753                            $('.fav-tooltiptext-' + objectId).html('Remove from favorites');
754                            $('.object-favorite-' + objectId).attr('data-tip', 'Remove from favorites');
755
756                        }
757                        else {
758                            //Remove Favorite
759                            //$element.attr("data-isfavorite", "false");
760                            $('.check-fav-' + objectId).prop('checked', false);
761                            $('.fav-tooltiptext-' + objectId).html('Add to favorites');
762                            $('.object-favorite-' + objectId).attr('data-tip', 'Add to favorites');
763
764                        }
765
766                        if (callback != null) {
767                            try {
768                                callback(fav);
769                            }
770                            catch (ex) {
771                                console.log(ex);
772                            }
773                        }
774                    }
775                });
776
777            }
778        }
779        catch (err) { }
780    }
781
782    VoteBestToggle(element, entityId, objectId, toggle = false) {
783
784        var obj = this;
785        try {
786            var $element = $(element);
787            if ($element) {
788
789                //This function can be called by any element with toggle true, and if so, will set the toggle status to the opposite of whatever it is now.
790                //This is useful to just call this function favorite without more jscript needed to set status first.  Not always applicable.
791                if (toggle) {
792                    if ($element.hasClass('isvotebest')) {
793                        $element.removeClass('isvotebest');
794                    }
795                    else {
796                        $element.addClass('isvotebest');
797                    }
798                }
799
800
801                var votebest = false;
802                //Determine desired isvotebest status (ie value after the current toggle action)
803                //Different widgets use different methods, ie ACAMH uses checkbox, Endocrine use buttons (using a class isfavorite)
804
805                if ($element.is(":checkbox")) {
806                    votebest = ($element.prop('checked'));
807                } else {
808                    votebest = $element.hasClass("isvotebest");
809                }
810
811
812
813                //Save
814                $.post("/Page/VoteBestSet", { ApplicationId: obj.ApplicationId, EntityId: entityId, ObjectId: objectId, VoteBest: votebest }, function (data) {
815
816                    if (data) {
817                        if (data.resultsModel == true) {
818                            //Saved ok, so update UI attribute of other instances of this object
819                            if (votebest) {
820                                //Add favorite to
821                                //$element.attr("data-isfavorite", "true");
822                                $('.check-votebest-' + objectId).prop('checked', true);
823                                $('.votebest-tooltiptext-' + objectId).html('Remove Vote for Best');
824                                $('.object-votebest-' + objectId).attr('data-tip', 'Remove from Vote for Best');
825
826                            }
827                            else {
828                                //Remove Favorite
829                                //$element.attr("data-isfavorite", "false");
830                                $('.check-votebest-' + objectId).prop('checked', false);
831                                $('.votebest-tooltiptext-' + objectId).html('Vote for the Best');
832                                $('.object-votebest-' + objectId).attr('data-tip', 'Vote for the Best');
833                            }
834                        }
835                    }
836                });
837
838            }
839        }
840        catch (err) { }
841    }
842
843    //Lists
844    //Relies on MediaSite-specific modal (see ProjectR:Film for example)
845    ShowListModal(objectId, objectName, userClip = false) {
846        //Opens a MediaSite-specific AddToList modal
847        //The following javascript funtion "showListModal(objectId)" is required in a client specific widget - see Projector
848        //objectId is the chosen object, so the lists will show whether this objectId is already in the lists - ok to send nothing
849        showListModal(objectId, objectName, userClip);
850    }
851
852    //Relies on MediaSite-specific modal
853    ShowAddListModal() {
854        //Opens a MediaSite-specific modal
855        showAddListModal();
856    }
857
858    //Relies on MediaSite-specific modal
859    ShowDeleteListModal(listId, listName) {
860        //Opens a MediaSite-specific modal
861        showDeleteListModal(listId, listName);
862    }
863
864    //Relies on MediaSite-specific modal
865    ShowRenameListModal(listId, listName) {
866        //Opens a MediaSite-specific modal
867        showRenameListModal(listId, listName);
868    }
869
870    LoadCadmoreEntityLists(entityId, listContainer, template, objectId, onCompleteFunction) {
871        //objectId is the object being added to a list (if required - otherwise pass null)
872        //onCompleteFunction - name of js function (eg on widget) that needs to run on completion - eg a carousel splide mount
873        let obj = this;
874        var listCount = 0;
875
876        try {
877            if (entityId) {
878                //Get Entity's Lists
879                $.post("/Page/EntityLists", { entityId: entityId, applicationId: obj.ApplicationId }, function (data) {
880                    if (data) {
881                        $(listContainer).empty();
882
883                        if (data.resultsModel && data.resultsModel.length > 0) {
884                            listCount = data.resultsModel.length;
885                            var templateMarkup = $(template).html();
886
887                            //Loop thru results, adding to UI
888                            for (let i = 0; i < data.resultsModel.length; i++) {
889                                try {
890                                    var $itemMarkup = obj.BuildCadmoreListItemMarkup(data.resultsModel[i], templateMarkup, i, objectId);
891                                    $(listContainer).append($itemMarkup);
892
893                                    //Set Checkbox, if objectId is in the list
894                                    if (objectId && data.resultsModel[i].objectList && data.resultsModel[i].objectList.length) {
895                                        var objectIsInList = false;
896                                        var listIncludesObject = data.resultsModel[i].objectList.find(t => t.objectId === objectId);
897                                        if (listIncludesObject) { objectIsInList = true; }
898                                        $('#list_' + data.resultsModel[i].id).prop('checked', objectIsInList);
899                                    }
900
901
902                                }
903                                catch (err) { }
904                            }
905                        }
906
907                        //onCompleteFunction
908                        if (onCompleteFunction && onCompleteFunction.length) {
909                            window[onCompleteFunction](listCount);
910                        }
911
912                        return data;
913                    }
914
915                    //onCompleteFunction
916                    if (onCompleteFunction && onCompleteFunction.length) {
917                        window[onCompleteFunction](listCount);
918                    }
919                });
920
921            }
922        }
923        catch (err) { }
924
925    }
926
927    LoadCadmoreEntityListsLimited(entityId, listContainer, template, objectId, take, onCompleteFunction) {
928        //objectId is the object being added to a list (if required - otherwise pass null)
929        //onCompleteFunction - name of js function (eg on widget) that needs to run on completion - eg a carousel splide mount
930        let obj = this;
931        var listCount = 0;
932
933        try {
934            if (entityId) {
935                //Get Entity's Lists
936                $.post("/Page/EntityLists", { entityId: entityId, applicationId: obj.ApplicationId }, function (data) {
937                    if (data) {
938                        $(listContainer).empty();
939
940                        if (data.resultsModel && data.resultsModel.length > 0) {
941                            listCount = data.resultsModel.length;
942                            if (take > listCount) {
943                                take = listCount;
944                            }
945
946                            var templateMarkup = $(template).html();
947
948                            //Loop thru results, adding to UI
949                            for (let i = 0; i < take; i++) {
950                                try {
951                                    var $itemMarkup = obj.BuildCadmoreListItemMarkup(data.resultsModel[i], templateMarkup, i, objectId);
952                                    $(listContainer).append($itemMarkup);
953
954                                    //Set Checkbox, if objectId is in the list
955                                    if (objectId && data.resultsModel[i].objectList && data.resultsModel[i].objectList.length) {
956                                        var objectIsInList = false;
957                                        var listIncludesObject = data.resultsModel[i].objectList.find(t => t.objectId === objectId);
958                                        if (listIncludesObject) { objectIsInList = true; }
959                                        $('#list_' + data.resultsModel[i].id).prop('checked', objectIsInList);
960                                    }
961                                }
962                                catch (err) { }
963                            }
964                        }
965
966                        //onCompleteFunction
967                        if (onCompleteFunction && onCompleteFunction.length) {
968                            window[onCompleteFunction](listCount);
969                        }
970
971                        return data;
972                    }
973
974                    //onCompleteFunction
975                    if (onCompleteFunction && onCompleteFunction.length) {
976                        window[onCompleteFunction](listCount);
977                    }
978                });
979
980            }
981        }
982        catch (err) { }
983
984    }
985
986    BuildCadmoreListItemMarkup(item, template, itemIndex, objectId) {
987        var obj = this;
988        //if (item.imageUrl == '') { item.imageUrl = 'https://placehold.co/960x540/46647A/fff?text=Image&nbsp;not&nbsp;available'; }
989
990        var $itemMarkup = template.replaceAll('{{ListId}}', item.id)
991            .replaceAll('{{EntityId}}', item.entityId)
992            .replaceAll('{{ApplicationId}}', item.applicationId)
993            .replaceAll('{{Name}}', item.name)
994            .replaceAll('{{NameNoQuotes}}', obj.RemoveQuotes(item.name))    //Used when passing as a JS parameter, so quotes don't break syntax
995            .replaceAll('{{NameFriendly}}', obj.UrlFriendlyText(item.name)) //Used for url, eg to List page
996            .replaceAll('{{ImageUrl}}', '')
997            .replaceAll('{{ItemCount}}', item.itemCount);
998
999        //Remove any redundant placeholders, eg {{anything}}
1000        var placeholderPattern = /\{\{.*?\}\}/g;
1001        $itemMarkup = $itemMarkup.replace(placeholderPattern, "");
1002
1003        return $itemMarkup;
1004    }
1005
1006    ListAdd(entityId, newListName, listContainer, template, objectId) {
1007        var obj = this;
1008        try {
1009            if (entityId && newListName) {
1010                //Save
1011                $.post("/Page/ListAdd", { EntityId: entityId, ApplicationId: obj.ApplicationId, Name: newListName }, function (data) {
1012
1013                    if (data) {
1014                        if (data.resultsModel) {
1015                            _pageManager.Toast("New list created : " + newListName, "success", 3000);
1016                            //Added, so refresh whole list of lists
1017                            if (listContainer) {
1018                                obj.LoadCadmoreEntityLists(entityId, listContainer, template, objectId);
1019                            }
1020                        }
1021                        else {
1022                            _pageManager.Toast("Error creating list : " + newListName, "error", 3000);
1023                        }
1024                    }
1025                    else {
1026                        _pageManager.Toast("Error creating list : " + newListName, "error", 3000);
1027                    }
1028                }
1029                );
1030
1031            }
1032            else {
1033                _pageManager.Toast("Error creating list", "error", 3000);
1034            }
1035        }
1036        catch (err) { }
1037    }
1038
1039    ListAddObject(entityId, listId, objectId, objectName, listName, itemCountLabel, userClip = false) {
1040        var obj = this;
1041       try {
1042            if (listId) {
1043                var origCount = -1;
1044                if (itemCountLabel) { origCount = parseInt($(itemCountLabel).html()); }
1045
1046                //Add Object to List
1047                $.post("/Page/ListAddObject", { EntityId: entityId, ApplicationId: obj.ApplicationId, EntityMediaLibraryListId: listId, ObjectId: objectId, UserClip: userClip }, function (data) {
1048
1049                    if (data) {
1050                        if (data.resultsModel == true) {
1051                            if (origCount >= 0 && itemCountLabel) {
1052                                var newCount = origCount + 1;
1053                                $(itemCountLabel).html(newCount);
1054                                var toastMessage = "'" + objectName + "' added to '" + listName + "'<br/>View your lists <a href='/Lists' class='toast-link'>here</a>";
1055                                obj.Toast(toastMessage, "success", 3000);
1056                            }
1057                        }
1058                        else {
1059                            obj.Toast("Error adding '" + objectName + "' to '" + listName + "'", "error", 3000);
1060                        }
1061                    }
1062                    else {
1063                        obj.Toast("Error adding '" + objectName + "' to '" + listName + "'", "error", 3000);
1064                    }
1065                });
1066
1067            }
1068        }
1069        catch (err) { }
1070    }
1071
1072    ListRemoveObject(entityId, listId, objectId, objectName, listName, itemCountLabel) {
1073        var obj = this;
1074        try {
1075            if (listId) {
1076                var origCount = -1;
1077                if (itemCountLabel) { origCount = parseInt($(itemCountLabel).html()); }
1078
1079                //remove Item from List
1080                $.post("/Page/ListRemoveObject", { EntityId: entityId, ApplicationId: obj.ApplicationId, EntityMediaLibraryListId: listId, ObjectId: objectId }, function (data) {
1081
1082                    if (data) {
1083                        if (data.resultsModel == true) {
1084                            if (origCount > 0 && itemCountLabel) {
1085                                var newCount = origCount - 1;
1086                                $(itemCountLabel).html(newCount);
1087                                _pageManager.Toast("'" + objectName + "' removed from '" + listName + "'", "success", 3000);
1088                            }
1089                        }
1090                        else {
1091                            _pageManager.Toast("Error removing '" + objectName + "' from '" + listName + "'", "error", 3000);
1092                        }
1093                    }
1094                    else {
1095                        _pageManager.Toast("Error removing '" + objectName + "' from '" + listName + "'", "error", 3000);
1096                    }
1097                });
1098
1099            }
1100        }
1101        catch (err) { }
1102    }
1103
1104    ListRename(entityId, listId, newListName, listContainer, template, objectId) {
1105        var obj = this;
1106        try {
1107            if (entityId && listId && newListName) {
1108                //Rename
1109                $.post("/Page/ListRename", { Id: listId, EntityId: entityId, ApplicationId: obj.ApplicationId, Name: newListName }, function (data) {
1110
1111                    if (data) {
1112                        if (data.resultsModel) {
1113                            _pageManager.Toast("List renamed", "success", 3000);
1114                            //Renamed, so refresh whole list of lists
1115                            if (listContainer) {
1116                                obj.LoadCadmoreEntityLists(entityId, listContainer, template, objectId);
1117                            }
1118                        }
1119                        else {
1120                            _pageManager.Toast("Error renaming list", "error", 3000);
1121                        }
1122                    }
1123                    else {
1124                        _pageManager.Toast("Error renaming list", "error", 3000);
1125                    }
1126                });
1127
1128            }
1129            else {
1130                _pageManager.Toast("Error renaming list", "error", 3000);
1131            }
1132        }
1133        catch (err) { }
1134    }
1135
1136    ListDelete(entityId, listId, listContainer, template, objectId) {
1137        var obj = this;
1138        try {
1139            if (entityId && listId) {
1140                //Delete
1141                $.post("/Page/ListDelete", { Id: listId, EntityId: entityId, ApplicationId: obj.ApplicationId, Name: "delete" }, function (data) {
1142
1143                    if (data) {
1144                        if (data.resultsModel == true) {
1145                            _pageManager.Toast("List deleted", "success", 3000);
1146                            //Deleted, so refresh whole list of lists
1147                            if (listContainer) {
1148                                obj.LoadCadmoreEntityLists(entityId, listContainer, template, objectId);
1149                            }
1150                        }
1151                        else {
1152                            _pageManager.Toast("Error deleting list", "error", 3000);
1153                        }
1154                    }
1155                    else {
1156                        _pageManager.Toast("Error deleting list", "error", 3000);
1157                    }
1158                });
1159
1160            }
1161            else {
1162                _pageManager.Toast("Error deleting list", "error", 3000);
1163            }
1164        }
1165        catch (err) { }
1166    }
1167
1168
1169    //Misc Functions
1170    HtmlEncodeSimple(html) {
1171        return html.replaceAll(/</g, "&lt;").replaceAll(/>/g, "&gt;");
1172    }
1173
1174    HtmlDecodeSimple(html) {
1175        return html.replaceAll("&lt;", /</g).replaceAll("&gt;", />/g);
1176    }
1177
1178    RemoveQuotes(text) {
1179        return text.replaceAll("'", "");
1180    }
1181
1182    UrlFriendlyText(text) {
1183        return text.replaceAll("'", "").replaceAll(" ", "-").replaceAll("/", "-");
1184    }
1185
1186    TruncateString(text, length, ellipse) {
1187        if (text.length > length) {
1188            text = text.substring(0, length);
1189
1190            if (ellipse) {
1191                text = text + '...';
1192            }
1193
1194        }
1195
1196        return text;
1197    }
1198
1199    Cart() {
1200        let obj = this;
1201        $("#modalCart").show();
1202    }
1203
1204
1205    //Intended to be called when any element on a page finishes loading.  Considered a global loading rutine.
1206    AllLoadActions() {
1207        let obj = this;
1208        obj.ReplaceAnonUserId();
1209        obj.ReplaceCartCount();
1210        obj.ReplaceCartTotal();
1211
1212        setTimeout(function () {
1213            obj.MakePageAccessible();
1214        }, 100);
1215
1216    }
1217
1218    //Function used to saturate dynamic elements for forms and such with Anon User Id
1219    ReplaceAnonUserId() {
1220        let obj = this;
1221        $('.AnonUserId').each(function () {
1222            $(this).val(obj.AnonUserId);
1223        });
1224    }
1225
1226    ReplaceCartCount() {
1227        let obj = this;
1228        $('.cart_item_count').each(function () {
1229            if (!$(this).find('.cart-count').length && obj.CartCount > 0)
1230            {
1231                $(this).append(`<div class="cart-count">${obj.CartCount}</div>`);
1232            }
1233        });
1234        $('.cart_item_count_span').each(function () {
1235            if (!$(this).find('.cart-count-span').length) {
1236                $(this).append(`<span class="cart-count-span">${obj.CartCount}</span>`);
1237            }
1238        });
1239        $('.cart_item_count_hidezero').each(function () {
1240            $(this).html(`<span class="cart-count-span cart-link__count">${obj.CartCount}</span>`);
1241
1242            if (obj.CartCount > 0) {
1243                $(this).show();
1244            }
1245            else {
1246                $(this).hide();
1247            }
1248        });
1249    }
1250
1251    ReplaceCartTotal() {
1252        let obj = this;
1253        $('.cart_total_inject').each(function () {
1254            if (!$(this).find('.cart-total').length) {
1255                $(this).append(`<span class="cart-total">${obj.CartTotal}</span>`);
1256            }
1257        });
1258    }
1259
1260    GetCartCount() {
1261        let obj = this;
1262
1263        $.ajax({
1264            url: `/Commerce/GetCartItemCount?AnonUserId=${obj.AnonUserId}`,
1265            type: "GET",
1266            async: true,
1267            success: function (data) {
1268                obj.CartCount = data.cartItemCount;
1269                obj.CartTotal = data.cartTotal;
1270                obj.ReplaceCartCount();
1271            },
1272            error: function (x, y, z) {
1273                console.log("Failed to get cart count.");
1274                console.log(x.responseText);
1275
1276                obj.CartCount = 0;
1277                obj.CartTotal = 0;
1278                obj.ReplaceCartCount();
1279            }
1280        });
1281    }
1282
1283    GetCartItemsList(element) {
1284        let obj = this;
1285
1286        $.ajax({
1287            url: `/Commerce/GetCartItemsList?AnonUserId=${obj.AnonUserId}`,
1288            type: "GET",
1289            async: false,
1290            success: function (data) {
1291                $("#" + element).val(data.cartItemsList);
1292            },
1293            error: function (x, y, z) {
1294                console.log("Failed to get cart count.");
1295                console.log(x.responseText);
1296            }
1297        });
1298    }
1299
1300    //AutoSuggest
1301    AutoSuggestGetTitles(objectDefinitionIds) {
1302        let obj = this;
1303        obj.AutoSuggestTitles = new Array();
1304
1305        try {
1306            $.post("/Page/AutoSuggestGetTitles", { applicationId: obj.ApplicationId, objectDefinitionIds: objectDefinitionIds }, function (data) {
1307                obj.AutoSuggestTitles = data.results;
1308            });
1309        }
1310        catch (err) { }
1311    }
1312
1313    AutoSuggest(searchElement, autosuggestContainerId, take) {
1314        let obj = this;
1315        var $autosuggestContainer = $("#" + autosuggestContainerId);
1316
1317        try {
1318            var searchTerm = $(searchElement).val();
1319
1320            if (searchTerm.length > 1 && obj.AutoSuggestTitles.length > 0) {
1321
1322                var filteredList = obj.AutoSuggestTitles.filter(function (obj) {
1323                    // Convert field value to lowercase for case-insensitive search
1324                    return obj["title"].toLowerCase().includes(searchTerm.toLowerCase());
1325                });
1326
1327                filteredList = filteredList.slice(0, take);
1328
1329                if (filteredList.length > 0) {
1330                    //Got results, so build html
1331                    var asHtml = "";
1332                    filteredList.forEach(function (result) {
1333                        asHtml += "<div class='autosuggest-listitem' title='" + result.title + "'><a href='" + result.navToUrl + "'><span class='autosuggest-listitem-type'>" + result.objectType + "</span> : <span class='autosuggest-listitem-title'>" + obj.AutoSuggestHighlightText(result.title, searchTerm) + "</span></a></div>";
1334                    });
1335                    $autosuggestContainer.empty();
1336                    $autosuggestContainer.append(asHtml);
1337                    $autosuggestContainer.show();
1338
1339                    $(searchElement)
1340                        .on("focusout", function () {
1341                            setTimeout(function () {
1342                                $autosuggestContainer.hide();
1343                            }, 500); //Give it half a second before hiding, so it can process any auto-suggest link clicks
1344                        })
1345                        .on("blur", function () {
1346                            setTimeout(function () {
1347                                $autosuggestContainer.hide();
1348                            }, 500); //Give half a second before hiding, so it can process any auto-suggest link clicks
1349                        });
1350                }
1351                else {
1352                    $autosuggestContainer.hide();
1353                    $autosuggestContainer.empty();
1354                }
1355            }
1356            else {
1357                $autosuggestContainer.hide();
1358                $autosuggestContainer.empty();
1359            }
1360        }
1361        catch (err) { }
1362    }
1363
1364    AutoSuggestHighlightText(text, searchTerm) {
1365        var regex = new RegExp('(' + searchTerm + ')', 'gi');
1366        return text.replace(regex, '<span class="autosuggest-highlight">$1</span>');
1367    }
1368
1369    /************ COUNTER *************************/
1370
1371    RecordSearch(searchTerm) {
1372       $.ajax({
1373            url: `/COUNTER/RecordSearch?searchTerm=${encodeURIComponent(searchTerm)}`,
1374            async: true,
1375            type: "POST",
1376            success: function () { },
1377            error: function (x, y, z) { console.log(x.responseText); console.log("Failed to record SEARCH for COUNTER."); }
1378        });
1379    }
1380
1381    RecordRequest() {
1382        if (this.HasAssociatedObject) {
1383            //Build a form submission.
1384            //string platform, string item, Guid objectId, Data_Type dataType, string regOrgName, int ? YOP, string URI
1385            var recordRequestModel = {
1386                platform: this.AppName,
1387                regOrgName: this.RegisteredOrg,
1388                item: this.AssociatedObjectPrimaryTitle,
1389                objectId: this.ObjectId,
1390                dataType: this.DataType,
1391                YOP: this.YOP,
1392                URI: window.location.href
1393            };
1394
1395            $.ajax({
1396                url: `/COUNTER/RecordRequest`,
1397                async: true,
1398                data: recordRequestModel,
1399                dataType: 'json',
1400                type: "POST",
1401                success: function () { },
1402                error: function (x, y, z) { console.log(x.responseText); console.log("Failed to record REQUEST for COUNTER."); }
1403            });
1404        }
1405    }
1406
1407    /************************************************/
1408
1409
1410
1411    /************ LEARNING *************************/
1412
1413    LearningActivityStart(laObjectId) {
1414        let obj = this;
1415
1416        try {
1417            $.post("/Page/LearningActivityStart", { laObjectId: laObjectId, regOrgId: obj.RegisteredOrganizationId }, function (data) {
1418            });
1419        }
1420        catch (err) { }
1421    }
1422
1423    LearningActivitySectionComplete(lasObjectId, refresh = true) {
1424        let obj = this;
1425
1426        try {
1427            $.post("/Page/LearningActivitySectionComplete", { lasObjectId: lasObjectId, regOrgId: obj.RegisteredOrganizationId }, function (data) {
1428                obj.Toast('Completed', 'succes', 1000);
1429
1430                if (refresh) {
1431                    window.location.reload();
1432                }
1433            });
1434        }
1435        catch (err) { }
1436
1437        if (refresh) {
1438            window.location.reload();
1439        }
1440    }
1441
1442
1443    LearningActivitySectionCompleteReset(lasObjectId) {
1444        let obj = this;
1445
1446        try {
1447            $.post("/Page/LearningActivitySectionCompleteReset", { lasObjectId: lasObjectId, regOrgId: obj.RegisteredOrganizationId }, function (data) {
1448                obj.Toast('Completed', 'succes', 1000);
1449                window.location.reload();
1450            });
1451        }
1452        catch (err) { }
1453        window.location.reload();
1454    }
1455
1456    ArrayToParameterString(array) {
1457        var s = "";
1458        for (var i = 0; i < array.length; i++) {
1459            if (s == "") {
1460                s = array[i];
1461            }
1462            else {
1463                s += "|" + array[i];
1464            }
1465        }
1466        return s;
1467    }
1468
1469    /************************************************/
1470
1471    ///This gets used by the search results widget paradigm to append a widget onto the screen and then load it
1472    ///to execute a search and return.
1473    AppendSearchResultsWidget(searchModel) {
1474        let obj = this;
1475        //Clear any previous results.
1476        $('#' + searchModel.SearchResultsContainerId).empty();
1477
1478        if (!searchModel.Take) {
1479            searchModel.Take = 24;
1480        }
1481
1482        //Create search results widget.
1483        const $widget = $('<div>', { 'class': 'widgetPlaceHolder search-results-widget' });
1484
1485
1486        console.log("SEARCH WIDGET");
1487        console.log(obj.ArrayToParameterString(searchModel.RelatedObjectDefinitionFilter) ?? '');
1488
1489        $widget.attr('data-widgetName', 'SearchResults');
1490        $widget.attr('data-widgetId', searchModel.TargetWidgetId ?? '');
1491        $widget.attr('data-objectIdsFilter', obj.ArrayToParameterString(searchModel.ObjectIdsFilter) ?? '');
1492        $widget.attr('data-objectDefinitionIdsFilter', obj.ArrayToParameterString(searchModel.ObjectDefinitionIdsFilter) ?? '');
1493        $widget.attr('data-tagIdsFilter', obj.ArrayToParameterString(searchModel.TagIdsFilter) ?? '');
1494        $widget.attr('data-hardTypesFilter', obj.ArrayToParameterString(searchModel.HardTypesFilter) ?? '');
1495        $widget.attr('data-categoryObjectIdsFilter', obj.ArrayToParameterString(searchModel.CategoryObjectIdsFilter) ?? '');
1496        $widget.attr('data-relatedObjectDefinitionsFilter', obj.ArrayToParameterString(searchModel.RelatedObjectDefinitionFilter) ?? '');
1497        $widget.attr('data-listIdsFilter', obj.ArrayToParameterString(searchModel.ListIdsFilter) ?? '');
1498        $widget.attr('data-searchTermFilter', searchModel.SearchTermFilter ?? '');
1499        $widget.attr('data-meta', `${searchModel.ViewMode ?? ''}|${searchModel.PageSize ?? ''}|${searchModel.Page ?? ''}`);
1500        $widget.attr('data-SearchResultCallBack', searchModel.SearchResultCallBack ?? '');
1501        $widget.attr('data-orderBy', searchModel.OrderBy ?? '');
1502        $widget.attr('data-favorites', String(searchModel.Favorites ?? false));
1503        $widget.attr('data-RecentlyWatched', String(searchModel.RecentlyWatched ?? false));
1504        $widget.attr('data-WatchedNotFinished', String(searchModel.WatchedNotFinished ?? false));
1505        $widget.attr('data-LearningNotComplete', String(searchModel.LearningNotComplete ?? false));
1506        $widget.attr('data-LearningComplete', String(searchModel.LearningComplete ?? false));
1507        $widget.attr('data-MostWatched', String(searchModel.MostWatched ?? false));
1508        $widget.attr('data-SearchType', searchModel.SearchType ?? '');
1509        $widget.attr('data-FilterOptions', searchModel.FilterOptions ?? '');
1510        $widget.attr('data-customFilter', searchModel.CustomFilter ?? '');
1511        $widget.attr('data-SpecificListSearch', searchModel.SpecificListSearch ?? '');
1512        $widget.attr('data-authedonly', String(searchModel.AuthedOnly ?? false));
1513        $widget.attr('data-publiconly', String(searchModel.PublicOnly ?? false));
1514        $widget.attr('data-Take', searchModel.Take ?? '');
1515        $widget.attr('data-galleryIdFilter', searchModel.GalleryIdFilter ?? '');
1516
1517        //Append search results widget.
1518        $('#' + searchModel.SearchResultsContainerId).append($widget);
1519
1520        console.log("LOAD");
1521        console.log(searchModel.ObjectDefinitionIdsFilter);
1522
1523        //load this widget.
1524        $('.search-results-widget').each(function () {
1525            obj.LoadWidget($(this));
1526        });
1527    }
1528
1529
1530
1531    /************ ACCESSIBILITY *************************/
1532
1533    MakePageAccessible() {
1534        let obj = this;
1535
1536        //Set TabIndex for all elements to 0, unless already set to -1
1537        /*$("[aria-label][tabindex!='-1']").each(function ()*/
1538        $("input[tabindex!='-1'], select[tabindex!='-1'], textarea[tabindex!='-1'], button[tabindex!='-1'], a[tabindex!='-1']").each(function () {
1539            $(this).attr("tabindex", "0");
1540        });
1541
1542        //Set aria-expanded to false on popup triggers
1543        $('[aria-haspopup="true"], [aria-haspopup="menu"]').attr("aria-expanded", "false");
1544
1545        //Set tabindex="-1" on all menu/popup items
1546        $('[aria-haspopup="true"], [aria-haspopup="menu"]').each(function () {
1547            var ownsTarget = $(this).attr("aria-owns");
1548            if (ownsTarget && ownsTarget.length) {
1549                var target = $("#" + ownsTarget);
1550                if (!target.length) {
1551                    target = $("." + ownsTarget);
1552                }
1553                if (target.length) {
1554                    $(target).find("input, select, textarea, button, a").attr("tabindex", "-1");
1555                }
1556            }
1557        });
1558
1559        //EnterKey open Popups, after removing any existing keydown listeners
1560        $('[aria-haspopup="true"], [aria-haspopup="menu"]').off("keydown");
1561        $('[aria-haspopup="true"], [aria-haspopup="menu"]').on("keydown", function (event) {
1562
1563            /*Enter Key = 13, Space Key = 32, Escape Key = 27 */
1564            //if (event.which == 13 || event.which == 32 || event.which == 27) {
1565            if (event.which == 13 || event.which == 27) {
1566
1567                var parentElement = $(this);
1568                var affectVisibilityStr = $(parentElement).attr("affect-visibility") ?? "";
1569                var openFunctionStr = $(parentElement).attr("post-open-function");
1570                var closeFunctionStr = $(parentElement).attr("post-close-function");
1571                var displayStyle = $(parentElement).attr("display-style");
1572
1573                if (!displayStyle || !displayStyle.length) {
1574                    displayStyle = "unset";
1575                }
1576
1577                var affectVisibility = !(affectVisibilityStr.length && affectVisibilityStr.toLowerCase() == "false");
1578                var openFunction = null;
1579                var closeFunction = null;
1580
1581                //Check the functions are actually functions, otherwise leave as null
1582                if (typeof window[openFunctionStr] === 'function') {
1583                    openFunction = openFunctionStr;
1584                }
1585
1586                if (typeof window[closeFunctionStr] === 'function') {
1587                    closeFunction = closeFunctionStr;
1588                }
1589
1590                var target = $(this).attr("aria-owns");
1591
1592                if (target && target.length) {
1593                    var popup = $("#" + target);
1594                    if (!popup.length) {
1595                        popup = $("." + target).first();
1596                    }
1597
1598                    if (popup.length) {
1599                        // Check if Enter or Space key is pressed
1600                        //if (event.which == 13 || event.which == 32) {
1601                        if (event.which == 13) {
1602
1603                            //Set aria-expanded to true
1604                            parentElement.attr('aria-expanded', 'true');
1605
1606                            //Display the popup/menu
1607                            if (affectVisibility) {
1608                                popup.css("visibility", "visible");
1609                                popup.css("opacity", "1.0");
1610                                popup.css("display", displayStyle);
1611                            }
1612
1613                            if (openFunction) { window[openFunction](parentElement); }
1614
1615                            // Get focusable items
1616                            var focusableItems = popup.find("input, select, textarea, button, a");
1617                            if (focusableItems && focusableItems.length) {
1618                                focusableItems.attr("tabindex", "0");// Make items tabable
1619                                var firstItem = focusableItems.first();
1620                                var lastItem = focusableItems.last();
1621                                event.preventDefault();
1622
1623                                //Focus on first item, after a slight delay before setting focus to ensure visibility
1624                                setTimeout(function () {
1625                                    //Focus on first item
1626                                    firstItem.focus();  // Set focus to the first item
1627                                }, 100);  // 100ms delay to ensure popup is fully rendered
1628
1629                                // Tab Trap within the popup
1630                                popup.off("keydown");
1631
1632                                popup.on('keydown', function (e) {
1633                                    // If the user presses the "Tab" key
1634                                    if (e.which == 9) {
1635                                        if (e.shiftKey) {  // Shift+Tab (backward)
1636                                            if (document.activeElement === firstItem[0]) {
1637                                                e.preventDefault();
1638                                                lastItem.focus();  // Loop back to the last item
1639                                            }
1640                                        }
1641                                        else {  // Tab (forward)
1642                                            if (document.activeElement === lastItem[0]) {
1643                                                e.preventDefault();
1644                                                firstItem.focus();  // Loop back to the first item
1645                                            }
1646                                        }
1647                                    }
1648
1649                                    // Close the popup on "Escape"
1650                                    if (e.which == 27) {
1651                                        parentElement.attr('aria-expanded', 'false');
1652
1653                                        if (affectVisibility) {
1654                                            popup.css("visibility", "hidden");
1655                                            popup.css("display", "none");
1656                                        }
1657                                        if (closeFunction) { window[closeFunction](parentElement); }
1658
1659                                        popup.off('keydown');  // Remove keydown handler
1660                                        popup.find("input, select, textarea, button, a").attr("tabindex", "-1");// Make items un-tabable
1661                                        parentElement.focus();  // Set focus back to the triggering element
1662                                    }
1663
1664                                    //// Handle "Enter or Space" key press
1665                                    //if (e.which == 13 || e.which == 32) {
1666                                    //    parentElement.attr('aria-expanded', 'false');
1667
1668                                    //    if (affectVisibility) { popup.css("visibility", "hidden"); }
1669                                    //    if (closeFunction) { window[closeFunction](parentElement); }
1670
1671                                    //    popup.find("input, select, textarea, button, a");
1672                                    //    popup.off('keydown');  // Remove keydown handler
1673                                    //}
1674
1675                                });
1676                            }
1677
1678                        }
1679
1680                        // Check if Escape key is pressed (keyCode 27), if so hide it
1681                        if (event.which == 27) {
1682                            parentElement.attr('aria-expanded', 'false');
1683
1684                            if (affectVisibility) {
1685                                popup.css("visibility", "hidden");
1686                                popup.css("display", "none");
1687                            }
1688
1689                            if (closeFunction) {
1690                                window[closeFunction]($(this));
1691                            }
1692
1693                            popup.off('keydown');  // Remove keydown handler
1694                            popup.find("input, select, textarea, button, a").attr("tabindex", "-1");// Make items un-tabable
1695                            parentElement.focus();
1696                        }
1697                    }
1698                }
1699            }
1700        });
1701
1702        //Focus to top of page, unless any containers (eg widgetPlaceHolders) have the attribute auto-focus="true"
1703        var $autoFocusElements = $("[auto-focus='true']");
1704        if (!$autoFocusElements.length) {
1705            setTimeout(function () {
1706                $('#topOfPage').focus();
1707            }, 100);  // 100ms delay to ensure skiplink is fully rendered
1708        }
1709        else {
1710            //Focus on first tabable item in the first container with the attribute auto-focus="true"
1711            setTimeout(function () {
1712                var $firstAutoFocusContainer = $autoFocusElements.first();
1713                if ($firstAutoFocusContainer.length) {
1714                    var tabableItem = $firstAutoFocusContainer.find(".auto-focus-item").first();
1715                    if (tabableItem.length) {
1716                        tabableItem.focus();
1717
1718                        //Reset auto-focus to false
1719                        //This allows for initial page load to NOT select first, then any subsequent searches/pagination etc DO select 1st item
1720                        //$autoFocusElements.attr("auto-focus", "false");
1721                    }
1722                }
1723            }, 100);  // 1000ms delay to ensure items are rendered
1724        }
1725    }
1726
1727    MakeSkipLinks() {
1728
1729
1730        //Add SkipLinks, if none found (allows for custom ones to be added)
1731        var skipLinks = $("#skiplinks");
1732        if (!skipLinks.length) {
1733
1734            var linkMainContent = "";
1735            var linkMainFooter = "";
1736
1737            //See if a #maincontent or .maincontent exists
1738            //Some <main> tags already have id's other than 'maincontent', so in that case we can use classes (then get it's id)
1739            var mainContent = $("#maincontent[id], .maincontent[id], #main-content, .main-content").first();
1740            if (mainContent.length) {
1741                //Get id of mainContent Elemen tId
1742                var mainContentElementId = mainContent.attr('id');
1743
1744                if (mainContentElementId && mainContentElementId.length) {
1745                    //Yes, found a '#maincontent' element, so create a skiplinks div with a link to the main content
1746                    linkMainContent = "<a href='#" + mainContentElementId + "' aria-label='skip to main content'>skip to main content</a>";
1747                }
1748            }
1749
1750            //See if a #mainfooter or .mainfooter exists
1751            //Some <main> tags already have id's other than 'mainfooter', so in that case we can use classes (then get it's id)
1752            var mainFooter = $("#mainfooter[id], .mainfooter[id]").first();
1753            if (mainFooter.length) {
1754                //Get id of mainFooter Elemen tId
1755                var mainFooterElementId = mainFooter.attr('id');
1756
1757                if (mainFooterElementId && mainFooterElementId.length) {
1758                    //Yes, found a '#mainfooter' element, so create a skiplinks div with a link to the footer
1759                    linkMainFooter = "<a href='#" + mainFooterElementId + "' aria-label='skip to footer'>skip to footer</a>";
1760                }
1761            }
1762
1763            //If we have a Content and/or Footer link, create a skiplink 
1764            if (linkMainContent.length || linkMainFooter.length) {
1765                var genericSkipLinkHtml = "<div id='skiplinks'>" + linkMainContent + linkMainFooter + "</div >";
1766                $("body").prepend(genericSkipLinkHtml);
1767            }
1768        }
1769
1770        //Add top of page, off-screen input... if not already there
1771        var $topOfPage = $('#topOfPage');
1772        if (!$topOfPage.length) {
1773            var currentPageTitle = $(document).find("title").text().replace(/['"]/g, '');
1774            var topOfPageHtml = "<input id='topOfPage' aria-label='top of page:" + currentPageTitle + "' value='' style='position:absolute; left:-9999px;'/>";
1775            $("body").prepend(topOfPageHtml);
1776        }
1777
1778        /* HomeKey sets focus to TopOfPage, one tab away from skiplink */
1779        $(document).on("keydown", function (event) {
1780
1781            /*Home Key = 36 */
1782            if (event.which === 36 || event.key === "Home") {
1783                $("#topOfPage").focus();
1784            }
1785
1786        });
1787    }
1788
1789    MakeUnTabable(element) {
1790        // Get all the element's child tabable items and set tabindex's to -1
1791        setTimeout(function () {
1792            $(element).find("input, select, textarea, button, a").attr("tabindex", "-1");
1793            //Just in case element is tabable itself, set that too
1794            $(element).attr("tabindex", "-1");
1795        }, 1000);  // 1000ms delay to ensure popup is fully rendered
1796
1797
1798    }
1799
1800    TabTrapPopup(popupId, closeFunction) {
1801        var popup = $("#" + popupId);
1802
1803        // Get focusable items
1804        var focusableItems = popup.find("[aria-label]");
1805        if (focusableItems && focusableItems.length) {
1806            focusableItems.attr("tabindex", "0");// Make items tabable
1807            var firstItem = focusableItems.first();
1808            var lastItem = focusableItems.last();
1809
1810            //Focus on first item, after a slight delay before setting focus to ensure visibility
1811            setTimeout(function () {
1812                //Focus on first item
1813                firstItem.focus();  // Set focus to the first item
1814            }, 100);  // 100ms delay to ensure popup is fully rendered
1815
1816            // Tab Trap within the popup
1817            popup.off("keydown");
1818
1819            popup.on('keydown', function (e) {
1820                // If the user presses the "Tab" key
1821                if (e.which == 9) {
1822                    if (e.shiftKey) {  // Shift+Tab (backward)
1823                        if (document.activeElement === firstItem[0]) {
1824                            e.preventDefault();
1825                            lastItem.focus();  // Loop back to the last item
1826                        }
1827                    }
1828                    else {  // Tab (forward)
1829                        if (document.activeElement === lastItem[0]) {
1830                            e.preventDefault();
1831                            firstItem.focus();  // Loop back to the first item
1832                        }
1833                    }
1834                }
1835
1836                // Close the popup on "Escape"
1837                if (e.which == 27) {
1838                    if (closeFunction) { window[closeFunction](); }
1839
1840                    popup.off('keydown');  // Remove keydown handler
1841                    //popup.find("[aria-label]").attr("tabindex", "-1");// Make items un-tabable
1842                }
1843
1844            });
1845        }
1846    }
1847
1848    /************************************************/
1849
1850
1851    /************ Application Institutions *************************/
1852
1853    ApplicationInstitutionsGet() {
1854        let obj = this;
1855        obj.Institutions = new Array();
1856
1857        try {
1858            $.post("/Page/ApplicationInstitutionsGet", { applicationId: obj.ApplicationId }, function (data) {
1859                obj.Institutions = data.institutions;
1860            });
1861        }
1862        catch (err) { }
1863    }
1864
1865    ApplicationInstitutionsSearch(take) {
1866        let obj = this;
1867        var $institutionContainer = $("#institutionsContainer");
1868
1869        try {
1870            var searchTerm = $("#institutionSearch").val();
1871
1872            if (searchTerm.length > 0 && obj.Institutions.length > 0) {
1873
1874                var filteredList = obj.Institutions.filter(function (obj) {
1875                    // Convert field value to lowercase for case-insensitive search
1876                    return obj["name"].toLowerCase().includes(searchTerm.toLowerCase());
1877                });
1878
1879                filteredList = filteredList.slice(0, take);
1880
1881                if (filteredList.length > 0) {
1882                    //Got results, so build html
1883                    var asHtml = "";
1884                    filteredList.forEach(function (result) {
1885                        asHtml += "<a href='javascript:void(0);' class='institution-listitem' title='" + result.cleanName + "' aria-label='select " + result.cleanName + "' data-entityId='" + result.id + "' onclick='_pageManager.SelectInstitutionForLogin(`" + result.id + "`);'>" + result.name + "</a>";
1886                    });
1887                    $institutionContainer.empty();
1888                    $institutionContainer.append(asHtml);
1889                    $institutionContainer.addClass("show");
1890
1891                    $(searchElement)
1892                        .on("focusout", function () {
1893                            setTimeout(function () {
1894                                $institutionContainer.removeClass("show");
1895                            }, 500); //Give it half a second before hiding, so it can process any auto-suggest link clicks
1896                        })
1897                        .on("blur", function () {
1898                            setTimeout(function () {
1899                                $institutionContainer.removeClass("show");
1900                            }, 500); //Give half a second before hiding, so it can process any auto-suggest link clicks
1901                        });
1902                }
1903                else {
1904                    $institutionContainer.removeClass("show");
1905                    $institutionContainer.empty();
1906                }
1907            }
1908            else {
1909                $institutionContainer.removeClass("show");
1910                $institutionContainer.empty();
1911            }
1912        }
1913        catch (err) { }
1914    }
1915
1916    SelectInstitutionForLogin(entityId) {
1917        let obj = this;
1918
1919        // Find the object with the unique ID
1920        var institute = obj.Institutions.find(function (item) {
1921            return item.id === entityId;
1922        });
1923
1924        if (institute) {
1925            $("#institutionSearch").val(institute.name);
1926            $("#institutionsContainer").removeClass("show");
1927            $("#institutionsContainer").empty();
1928
1929            if (institute.hasOpenAthensIdentifier) {
1930                var openAthensUrl = $("#openAthensUrlWithoutEntityValue").val() + institute.openAthensIdentifier;
1931                $("#institution-login-button-openathens .button-login").attr("href", openAthensUrl);
1932                $("#institution-login-button-openathens").addClass("show");
1933            }
1934            else {
1935                $("#institution-login-button-openathens").removeClass("show");
1936            }
1937
1938            if (institute.hasProxyEntryUrl) {
1939                $("#institution-login-button-proxy .button-login").attr("href", institute.proxyEntryUrl);
1940                $("#institution-login-button-proxy").addClass("show");
1941            }
1942            else {
1943                $("#institution-login-button-proxy").removeClass("show");
1944            }
1945
1946            if (institute.hasIPs) {
1947                $("#institution-login-button-ip").addClass("show");
1948            }
1949            else {
1950                $("#institution-login-button-ip").removeClass("show");
1951            }
1952
1953        }
1954        else {
1955            $("#institution-login-button-openathens").removeClass("show");
1956            $("#institution-login-button-proxy").removeClass("show");
1957            $("#institution-login-button-ip").removeClass("show");
1958
1959            $("#institution-login-button-openathens .button-login").attr("href", "");
1960            $("#institution-login-button-proxy .button-login").attr("href", "");
1961        }
1962
1963
1964    }
1965
1966    /************************************************/
1967
1968
1969    /************ Access Codes *************************/
1970
1971    async RedeemAccessCode(accessCodeElement, accessCodeButton, accessCodeResultElement) {
1972        let obj = this;
1973        const accessCode = $("#" + accessCodeElement).val()?.trim();
1974
1975        if (!accessCode) {
1976            $("#" + accessCodeResultElement)
1977                .text("Invalid access code")
1978                .addClass("error")
1979                .removeClass("success");
1980            return;
1981        }
1982
1983        const $btn = $("#" + accessCodeButton);
1984
1985        // Prevent double-clicks
1986        if ($btn.data("busy")) return;
1987        $btn.data("busy", true).prop("disabled", true);
1988
1989        // Add spinner only if not already present
1990        if ($btn.find("i.fa-spinner").length === 0) {
1991            $btn.prepend('<i class="fa-solid fa-spinner fa-spin me-2" aria-hidden="true"></i>');
1992        }
1993
1994        $("#" + accessCodeResultElement)
1995            .text("")
1996            .removeClass("error")
1997            .removeClass("success");
1998
1999        try {
2000
2001            await new Promise(r => setTimeout(r, 1000));
2002
2003            const data = await $.ajax({
2004                url: "/Page/RedeemAccessCode",
2005                method: "POST",
2006                data: { regOrgId: obj.RegisteredOrganizationId, applicationId: obj.ApplicationId, loginProfileId: obj.LoginProfileId, accessCode: accessCode }
2007            });
2008
2009            if (data && data?.success) {
2010                $("#" + accessCodeResultElement)
2011                    .text(data?.message || "Access code redeemed successfully")
2012                    .addClass("success")
2013                    .removeClass("error");
2014            } else {
2015                $("#" + accessCodeResultElement)
2016                    .text(data?.message || "There was an issue redeeming the access code")
2017                    .addClass("error")
2018                    .removeClass("success");
2019            }
2020        } catch (err) {
2021            $("#" + accessCodeResultElement)
2022                .text("There was an issue redeeming the access code")
2023                .addClass("error")
2024                .removeClass("success");
2025        } finally {
2026            $btn.find("i.fa-spinner").remove();
2027            $btn.data("busy", false).prop("disabled", false);
2028        }
2029    }
2030
2031    /************************************************/
2032
2033
2034    /************ Email Form *************************/
2035
2036    SendForm(formName, emailAddress, subject, intro, formContainer, successFunction) {
2037        let obj = this;
2038
2039        var emailBody = intro + "<br/><br/>";
2040
2041        try {
2042
2043            // Find all form-field elements
2044            $('#' + formContainer).find('.form-field').each(function () {
2045                // Check for text inputs
2046                var textInput = $(this).find('input[type="text"]');
2047                if (textInput.length > 0) {
2048                    var fieldName = textInput.attr('name');
2049                    var fieldValue = textInput.val();
2050                    emailBody += fieldName + ": " + fieldValue + "<br/>";
2051                }
2052
2053                // Check for hidden inputs
2054                var textInput = $(this).find('input[type="hidden"]');
2055                if (textInput.length > 0) {
2056                    var fieldName = textInput.attr('name');
2057                    var fieldValue = textInput.val();
2058                    emailBody += fieldName + ": " + fieldValue + "<br/>";
2059                }
2060
2061                // Check for checkbox groups
2062                var groupName = $(this).attr('data-group');
2063                if (groupName) {
2064                    var selectedValues = [];
2065
2066                    // Find all checked checkboxes within this group
2067                    $(this).find('input[type="checkbox"]:checked').each(function () {
2068                        selectedValues.push($(this).val());
2069                    });
2070
2071                    if (selectedValues.length > 0) {
2072                        emailBody += groupName + ": " + selectedValues.join(', ') + "<br/>";
2073                    }
2074                }
2075            });
2076
2077            $.post("/Page/SendFormByEmail", { emailAddress: emailAddress, subject: subject, body: emailBody }, function (data) {
2078                if (data) {
2079                    var toastSuccess = formName + ' sent';
2080                    obj.Toast(toastSuccess, 'success', 2000);
2081                    $('#' + formContainer + 'SentWrapper').show();
2082                    $('#' + formContainer + 'Wrapper').hide();
2083                    if (successFunction && successFunction.length) {
2084                        let functionObj = window[successFunction];
2085                        functionObj();
2086                    }
2087                }
2088                else {
2089                    var toastError = 'There was an issue sending ' + formName;
2090                    obj.Toast(toastError, 'error', 2000);
2091                }
2092
2093                return;
2094            });
2095
2096        }
2097        catch (err) {
2098            var toastError = 'There was an issue sending ' + formName;
2099            obj.Toast(toastError, 'error', 2000);
2100        }
2101
2102
2103    }
2104
2105    /************************************************/
2106
2107
2108
2109    /************ Search Within Transcripts *************************/
2110    /* Typically called by LoadData function in cadmoregallery.js */
2111    /*Primary url field is the clickthrough field if it is present on the media object whose transcript is being search.  This is to power click throughs.*/
2112    SearchWithinTranscripts(safeId, searchTerm, primaryUrlField) {
2113        let obj = this;
2114
2115        try {
2116            if (searchTerm.trim().length === 0 || searchTerm.trim() === "*") {
2117                return;
2118            }
2119
2120            var $searchWithinTranscriptContainers = new Array();
2121
2122            if (safeId.length) {
2123                //Find all search-within-transcript-containers within a given widget
2124                $searchWithinTranscriptContainers = $(".search-within-transcript-container_" + safeId + "[data-processed=false]");
2125            }
2126
2127            //Get template
2128            var $templateTranscriptSearchWithin = $("#templateTranscriptSearchWithin_" + safeId);
2129
2130            //Got any containers to process - and do we have a search result template?
2131            if ($searchWithinTranscriptContainers.length > 0 && $templateTranscriptSearchWithin && $templateTranscriptSearchWithin.length > 0) {
2132
2133                //Got some search-within-transcript-containers, so process them
2134                $searchWithinTranscriptContainers.each(function () {
2135                    var $container = $(this);
2136
2137                    //Find a results container
2138                    var $resultsContainer = $container.find(".search-within-transcript-results");
2139
2140                    //Clear results container
2141                    if ($resultsContainer && $resultsContainer.length > 0) {
2142                        $resultsContainer.empty();
2143                    }
2144
2145                    //Only do a search if we have a searchTerm
2146                    if ($resultsContainer && $resultsContainer.length > 0 && searchTerm.length) {
2147                        //Get MediaObjectId & navToUrl
2148                        var mediaObjectId = $container.attr('data-mediaObjectId');
2149                        var origNavToUrl = $container.attr('data-navToUrl');
2150                        var primaryUrlField = $container.attr('data-primaryUrlField');
2151                        var take = $container.attr('data-transcriptSearchResultsTake');
2152
2153                        //Only continue if we have an ObjectId & navToUrl
2154                        if (mediaObjectId.length > 0 && origNavToUrl.length) {
2155
2156                            $.post("/Page/SearchWithinTranscript", { objectId: mediaObjectId, searchTerm: searchTerm, take: take, applicationId: obj.ApplicationId }, function (data) {
2157                                if (data && data.resultsModel) {
2158                                    var result = data.resultsModel;
2159
2160                                    //Mark container as processed - to stop any repeat checks
2161                                    $container.attr('data-processed', 'true');
2162
2163                                    var template = $templateTranscriptSearchWithin.html();
2164
2165                                    //Add transcript results to container
2166                                    for (let i = 0; i < result.transcriptHits.length; i++) {
2167                                        try {
2168                                            var item = result.transcriptHits[i];
2169
2170                                            if (item.timeStamp.length && item.line.length) {
2171                                                var navToUrl = origNavToUrl + "?cue=" + item.timeStamp + ":000";
2172
2173                                                //var $itemMarkup = obj.BuildTranscriptSearchWithResultItemMarkup(navToUrl, item.timeStamp, item.line, $templateTranscriptSearchWithin);
2174                                                var $itemMarkup = template.replaceAll("{{NavToUrl}}", navToUrl)
2175                                                    .replaceAll("{{Timestamp}}", item.timeStamp + ":000")
2176                                                    .replaceAll("{{Line}}", item.line)
2177                                                    .replaceAll("{{ObjectId}}", mediaObjectId)
2178                                                    .replaceAll("{{PrimaryUrlField}}", primaryUrlField)
2179                                                    ;
2180
2181                                                if ($itemMarkup && $itemMarkup.length > 0) {
2182                                                    $resultsContainer.append($itemMarkup);
2183                                                }
2184                                            }
2185                                        }
2186                                        catch (err) { }
2187                                    }
2188                                }
2189                            });
2190                        }
2191                    }
2192                });
2193            }
2194        }
2195        catch (err) { }
2196
2197    }
2198
2199
2200    SearchWithinTranscriptsAndSegments(safeId, searchTerm) {
2201        let obj = this;
2202
2203        try {
2204            if (searchTerm.trim().length === 0 || searchTerm.trim() === "*") {
2205                return;
2206            }
2207
2208            var $searchWithinTranscriptContainers = new Array();
2209
2210            if (safeId.length) {
2211                //Find all search-within-transcript-containers within a given widget
2212                $searchWithinTranscriptContainers = $(".search-within-transcript-container_" + safeId + "[data-processed=false]");
2213            }
2214
2215            //Get template
2216            var $templateTranscriptSearchWithin = $("#templateTranscriptSearchWithin_" + safeId);
2217
2218            //Got any containers to process - and do we have a search result template?
2219            if ($searchWithinTranscriptContainers.length > 0 && $templateTranscriptSearchWithin && $templateTranscriptSearchWithin.length > 0) {
2220
2221                //Got some search-within-transcript-containers, so process them
2222                $searchWithinTranscriptContainers.each(function () {
2223                    var $container = $(this);
2224
2225                    //Find a results container
2226                    var $resultsContainer = $container.find(".search-within-transcript-results");
2227
2228                    //Clear results container
2229                    if ($resultsContainer && $resultsContainer.length > 0) {
2230                        $resultsContainer.empty();
2231                    }
2232
2233                    //Only do a search if we have a searchTerm
2234                    if ($resultsContainer && $resultsContainer.length > 0 && searchTerm.length) {
2235                        //Get MediaObjectId & navToUrl
2236                        var mediaObjectId = $container.attr('data-mediaObjectId');
2237                        var origNavToUrl = $container.attr('data-navToUrl');
2238                        var take = $container.attr('data-transcriptSearchResultsTake');
2239
2240                        //Only continue if we have an ObjectId & navToUrl
2241                        if (mediaObjectId.length > 0 && origNavToUrl.length) {
2242
2243                            $.post("/Page/SearchWithinTranscript", { objectId: mediaObjectId, searchTerm: searchTerm, take: take, applicationId: obj.ApplicationId }, function (data) {
2244                                if (data && data.resultsModel) {
2245                                    var result = data.resultsModel;
2246
2247                                    result.transcriptHits = result.transcriptHits || [];
2248                                    result.segmentHits = result.segmentHits || [];
2249
2250                                    //Mark container as processed - to stop any repeat checks
2251                                    $container.attr('data-processed', 'true');
2252
2253                                    var template = $templateTranscriptSearchWithin.html();
2254
2255                                    var hits = [];
2256
2257                                    // Transcript hits
2258                                    if (result.transcriptHits && result.transcriptHits.length > 0) {
2259                                        for (let i = 0; i < result.transcriptHits.length; i++) {
2260                                            hits.push({
2261                                                hitTypeLabel: "Transcript",
2262                                                hitTypeClass: "transcript",
2263                                                item: result.transcriptHits[i]
2264                                            });
2265                                        }
2266                                    }
2267
2268                                    // Segment hits
2269                                    if (result.segmentHits && result.segmentHits.length > 0) {
2270                                        for (let i = 0; i < result.segmentHits.length; i++) {
2271                                            hits.push({
2272                                                hitTypeLabel: "Segment",
2273                                                hitTypeClass: "segment",
2274                                                item: result.segmentHits[i]
2275                                            });
2276                                        }
2277                                    }
2278
2279                                    // Optional: sort by timestamp if both types use the same timestamp format
2280                                    hits.sort(function (a, b) {
2281                                        var aTime = a.item.timeStamp || a.item.timestamp || "";
2282                                        var bTime = b.item.timeStamp || b.item.timestamp || "";
2283                                        return aTime.localeCompare(bTime);
2284                                    });
2285
2286                                    // Add combined results to container
2287                                    for (let i = 0; i < hits.length; i++) {
2288                                        try {
2289                                            var hit = hits[i];
2290                                            var item = hit.item;
2291
2292                                            var timeStamp = item.timeStamp || "";
2293                                            var line = item.line || item.segmentTitle || "";
2294
2295                                            if (timeStamp.length && line.length) {
2296                                                var cueTimeStamp = timeStamp;
2297
2298                                                if (hit.hitTypeClass === "transcript") {
2299                                                    cueTimeStamp = timeStamp + ":000";
2300                                                }
2301
2302                                                var navToUrl = origNavToUrl + "?cue=" + cueTimeStamp;
2303
2304                                                var $itemMarkup = template
2305                                                    .replaceAll("{{NavToUrl}}", navToUrl)
2306                                                    .replaceAll("{{Timestamp}}", timeStamp)
2307                                                    .replaceAll("{{Line}}", line)
2308                                                    .replaceAll("{{HitTypeLabel}}", hit.hitTypeLabel)
2309                                                    .replaceAll("{{HitTypeClass}}", hit.hitTypeClass);
2310
2311                                                if ($itemMarkup && $itemMarkup.length > 0) {
2312                                                    $resultsContainer.append($itemMarkup);
2313                                                }
2314                                            }
2315                                        }
2316                                        catch (err) { }
2317                                    }
2318                                }
2319                            });
2320                        }
2321                    }
2322                });
2323            }
2324        }
2325        catch (err) { }
2326
2327    }
2328
2329    /************************************************/
2330
2331
2332    /************ Share *************************/
2333    /* Opens a MediaSite-specific modal (see ProjectR:Film for example) */
2334
2335    ShowShareModal(objectName) {
2336        let obj = this;
2337        //var thisUrl = window.location.href; //obj.Url
2338        var thisUrl = obj.Url
2339
2340        var truncatedName = obj.TruncateString(objectName, 30, true);
2341
2342        //Title
2343        $("#shareModal .shareTitle .objectName").html(truncatedName);
2344
2345        //Share Items
2346        var facebookURL = "https://facebook.com/sharer/sharer.php?u=" + thisUrl;
2347        $("#shareModal #facebook-share-tab-button").attr("href", facebookURL);
2348
2349        var twitterURL = "https://twitter.com/intent/tweet/?text=" + objectName + "&url=" + thisUrl;
2350        $("#shareModal #twitter-share-tab-button").attr("href", twitterURL);
2351
2352        var linkedInURL = "https://www.linkedin.com/shareArticle?mini=true&title=" + objectName + "&url=" + thisUrl;
2353        $("#shareModal #linkedin-share-tab-button").attr("href", linkedInURL);
2354
2355        var emailUrl = "mailto:?subject=" + objectName + "&body=" + thisUrl;
2356        $("#shareModal #email-share-tab-button").attr("href", emailUrl);
2357
2358        //Open Modal
2359        $("#shareModal").fadeIn();
2360
2361    }
2362
2363    /************************************************/
2364
2365    /************ Saved Search *************************/
2366    /* see NISO Video (widget: Search Gallery) for example */
2367
2368    SavedSearches_UpdateList() {
2369        let obj = this;
2370        try {
2371
2372            //Clear any containers
2373            $(".savedSearch-dropdown-container").empty();
2374
2375            $.post("/Page/SearchesGetByEntityIdAndApplicationId", {}, function (data) {
2376                $('.savedSearch-dropdown-container').each(function (x, item) {
2377                    if (data) {
2378                        var isOnSearchPage = ($(item).attr("data-onsearchpage") == "true");
2378 //eg in Header will be "false"", on search filter etc will be "true"
2379                        var searchpagename = $(item).attr("data-searchpagename"); //name of search page, for redirect purposes
2380                        var showEdit = ($(item).attr("data-showedit") == "true"); //display edit/delete
2381                        var showDescription = ($(item).attr("data-showdescription") == "true"); //display description
2382
2383                        //Loop thru results, adding to UI
2384                        for (let i = 0; i < data.length; i++) {
2385                            try {
2386                                var editElement = "";
2387                                if (showEdit) {
2388                                    editElement = "<i class='fas fa-edit savedSearches-listitem-edit' onclick='event.stopPropagation();savedSearch_Edit(`" + data[i].id + "`);'></i>";
2389                                }
2390
2391                                var description = "";
2392                                if (showDescription && data[i].description && data[i].description.length) {
2393                                    description = "<div class='savedSearches-listitem-description'>" + obj.escapeHTML(data[i].description) + "</div>";
2394                                }
2395
2396
2397                                var $itemMarkup = "";
2398                                if (isOnSearchPage) {
2399                                    $itemMarkup = "<div class='savedSearches-listitem'><span class='savedSearches-listitem-name' onclick='savedSearch_Run(`" + data[i].id + "`);'>" + data[i].name + "</span>" + editElement + description + "</div>";
2400                                }
2401                                else {
2402                                    $itemMarkup = "<div class='savedSearches-listitem'><a class=' savedSearches-listitem-name' href='/" + searchpagename + "?searchId=" + data[i].id + "'>" + data[i].name + "</a>" + editElement + description + "</div>";
2403                                }
2404                                $(item).append($itemMarkup);
2405                            }
2406                            catch (err) { }
2407                        }
2408                    }
2409
2410                    if (isOnSearchPage) {
2411                        var $addNewMarkup = "<div class='savedSearches-addnewitem' onclick='savedSearch_New();'>add new</div>";
2412                        $(item).append($addNewMarkup);
2413                    }
2414                });
2415
2416            });
2417
2418            return;
2419
2420        }
2421        catch (err) {
2422            var toastError = 'There was an issue retrieving saved searches';
2423            _pageManager.Toast(toastError, 'error', 2000);
2424        }
2425
2426        return;
2427
2428    }
2429
2430    SearchSave(searchModel, successFunction) {
2431        let obj = this;
2432        try {
2433
2434            $.post("/Page/SearchSave", { model: searchModel }, function (data) {
2435                if (data) {
2436                    var toastSuccess = 'Search saved';
2437                    obj.Toast(toastSuccess, 'success', 2000);
2438                    if (successFunction && successFunction.length) {
2439                        let functionObj = window[successFunction];
2440                        functionObj();
2441                    }
2442                }
2443                else {
2444                    var toastError = 'There was an issue saving this search ';
2445                    obj.Toast(toastError, 'error', 2000);
2446                }
2447
2448                return;
2449            });
2450
2451        }
2452        catch (err) {
2453            var toastError = 'There was an issue saving this search';
2454            obj.Toast(toastError, 'error', 2000);
2455        }
2456    }
2457
2458    SearchDelete(searchId, successFunction) {
2459        let obj = this;
2460        try {
2461
2462            $.post("/Page/SearchDelete", { searchId: searchId }, function (data) {
2463                if (data) {
2464                    var toastSuccess = 'Search deleted';
2465                    obj.Toast(toastSuccess, 'success', 2000);
2466                    if (successFunction && successFunction.length) {
2467                        let functionObj = window[successFunction];
2468                        functionObj();
2469                    }
2470                }
2471                else {
2472                    var toastError = 'There was an issue deleting this search ';
2473                    obj.Toast(toastError, 'error', 2000);
2474                }
2475
2476                return;
2477            });
2478
2479        }
2480        catch (err) {
2481            var toastError = 'There was an issue deleting this search';
2482            obj.Toast(toastError, 'error', 2000);
2483        }
2484    }
2485
2486    /************************************************/
2487
2488
2489    /************ Clips *************************/
2490    /* see NISO Video (widget: Dashboard - Clips - Carousel) for example */
2491
2492    LoadCadmoreEntityClips(entityId, clipContainer, template, onCompleteFunction) {
2493        //objectId is the object being added to a clip (if required - otherwise pass null)
2494        //onCompleteFunction - name of js function (eg on widget) that needs to run on completion - eg a carousel splide mount
2495        let obj = this;
2496        var clipCount = 0;
2497
2498        try {
2499            if (entityId) {
2500                //Get Entity's Lists
2501                $.post("/Page/ClipsGetByEntityIdAndApplicationId", { entityId: entityId, applicationId: obj.ApplicationId }, function (data) {
2502                    if (data) {
2503                        $(clipContainer).empty();
2504
2505                        if (data.length > 0) {
2506                            clipCount = data.length;
2507                            var templateMarkup = $(template).html();
2508
2509                            //Loop thru results, adding to UI
2510                            for (let i = 0; i < data.length; i++) {
2511                                try {
2512                                    var $itemMarkup = obj.BuildCadmoreClipItemMarkup(data[i], templateMarkup);
2513                                    $(clipContainer).append($itemMarkup);
2514                                }
2515                                catch (err) { }
2516                            }
2517                        }
2518
2519                        //onCompleteFunction
2520                        if (onCompleteFunction && onCompleteFunction.length) {
2521                            window[onCompleteFunction](clipCount);
2522                        }
2523
2524                        return data;
2525                    }
2526
2527                    //onCompleteFunction
2528                    if (onCompleteFunction && onCompleteFunction.length) {
2529                        window[onCompleteFunction](clipCount);
2530                    }
2531                });
2532
2533            }
2534        }
2535        catch (err) { }
2536    }
2537
2538    BuildCadmoreClipItemMarkup(item, template) {
2539        var obj = this;
2540        //if (item.imageUrl == '') { item.imageUrl = 'https://placehold.co/960x540/46647A/fff?text=Image&nbsp;not&nbsp;available'; }
2541
2542        var $itemMarkup = template.replaceAll('{{ClipId}}', item.id)
2543            .replaceAll('{{EntityId}}', item.entityId)
2544            .replaceAll('{{ApplicationId}}', item.applicationId)
2545            .replaceAll('{{Name}}', item.name ?? "")
2546            .replaceAll('{{NameNoQuotes}}', obj.RemoveQuotes(item.name) ?? "")    //Used when passing as a JS parameter, so quotes don't break syntax
2547            .replaceAll('{{NameFriendly}}', obj.UrlFriendlyText(item.name ?? "")) //Used for url, eg to List page
2548            .replaceAll('{{NavToUrl}}', item.fullUrl ?? "")
2549            .replaceAll('{{ImageUrl}}', item.imageUrl ?? "")
2550            .replaceAll('{{ObjectTitle}}', item.objectTitle ?? "");
2551
2552        //Remove any redundant placeholders, eg {{anything}}
2553        var placeholderPattern = /\{\{.*?\}\}/g;
2554        $itemMarkup = $itemMarkup.replace(placeholderPattern, "");
2555
2556        return $itemMarkup;
2557    }
2558
2559    /************************************************/
2560
2561
2562    /************ Get ObjectListItem *************************/
2563
2564    RedirectToObjectFullPath(objectId, urlPrefix) {
2565        //By default 'urlPrefix' will be empty, but to nav to another site, urlPrefix would be the host
2566        let obj = this;
2567        try {
2568
2569            $.post("/Page/GetObjectListItem", { objectId: objectId, applicationId: obj.ApplicationId }, function (data) {
2570                if (data) {
2571                    var url = urlPrefix + data.navToUrl;
2572                    window.location = url;
2573                }
2574                else {
2575                    window.location = '/PageNotFound-UnableToRedirect';
2576                }
2577            });
2578
2579        }
2580        catch (err) {
2581            window.location = '/PageNotFound-UnableToRedirect';
2582        }
2583
2584    }
2585
2586    /************************************************/
2587
2588
2589    escapeHTML(str) {
2590        return str
2591            .replace(/&/g, '&amp;')
2592            .replace(/</g, '&lt;')
2593            .replace(/>/g, '&gt;')
2594            .replace(/"/g, '&quot;')
2595            .replace(/'/g, '&#39;');
2596    }
2597
2598    /************* Entity Application Properties Interface *********/
2599    //This is for saving a specific property type.  For custom, need something else, but currently have no customs.
2600    //Property Type Int is the literaly int from the app property enum you want to save.
2601    //If boolean, val shoudl be True or False.  (This was left open in case we ever need string values.)
2602    SaveEntityApplicationProperty(propertyTypeInt, val) {
2603        //Make a simple call to add or set.
2604        let obj = this;
2605        var fdata = new FormData();
2606        fdata.append("type", propertyTypeInt);
2607        fdata.append("value", val);
2608        $.ajax({
2609            url: '/Page/AddOrSetEntityApplicationProperty',
2610            async: true,
2611            type: "POST",
2612            data: fdata,
2613            processData: false,
2614            contentType: false,
2615            success: function (data) {
2616                //Nothing to report.  Data will true if it thinks it worked, false if not.
2617                //We have to refresh login context because this is changing a property for the underyling user.
2618                obj.RefreshLoginContext();
2619                return data;
2620            },
2621            error: function (x, y, z) {
2622                console.log(x.responseText);
2623            }
2624        });
2625    }
2626
2627    ///For use with simpel bool variables on entity application properties to toggle them.
2628    EntityApplicationPropertyBoolUpdater(startingVal, identifier) {
2629        var found = false;
2630        let obj = this;
2631        for (var i = 0; i < obj.eaPropToggles.length; i++) {
2632            var propToggle = obj.eaPropToggles[i];
2633
2634            if (propToggle.identifier == identifier) {
2635                found = true;
2636                if (propToggle.val == "True") {
2637                    return "False";
2638                }
2639                else {
2640                    return "True";
2641                }
2642            }
2643        }
2644
2645        if (!found) {
2646            var flipVal = startingVal;
2647            if (flipVal == "True") {
2648                flipVal = "False";
2649            }
2650            else {
2651                flipVal = "True";
2652            }
2653            obj.eaPropToggles.push({
2654                identifier: identifier,
2655                val: flipVal
2656            });
2657
2658            return flipVal;
2659        }
2660    }
2661
2662
2663    /************* RunScriptIfExists *********/
2664    //This can be called from anywhere (eg window.addEventListener below), and will attempt to ru na script if found on current page if it exists
2665    //Typical usecase includes 'PlayerEventOnStart' triggered via PlayerWeb eventListener which caould then enable a 'Mark as Complete' button
2666    //Known uses;
2667    // PlayerEventOnStart, PlayerEventOnEnd, PlayerEventOnJoinEvent, PlayerEventOnLoaded
2668    RunScriptIfExists(functionName, data) {
2669        try {
2670            if (data) {
2671                window[functionName](data);
2672            }
2673            else {
2674                window[functionName]();
2675            }
2676        }
2677        catch (err) { }
2678    };
2679
2680    ///This is for sending feedback on a specific prompt id.
2681    AI_RAG_Feedback(feedback, promptId) {
2682        let obj = this;
2683        var fdata = new FormData();
2684        fdata.append("feedback", feedback);
2685        fdata.append("promptId", promptId);
2686        $('#RAG_feedback_prompt_box').hide();
2687        $('#RAG_feedback_response').show();
2688        $.ajax({
2689            url: '/Page/RAG_Feedback',
2690            async: true,
2691            type: "POST",
2692            data: fdata,
2693            processData: false,
2694            contentType: false,
2695            success: function () {
2696            },
2697            error: function (x, y, z) {
2698                console.log('Error on RAG feedback.');
2699                console.log(x.responseText);
2700            }
2701        });
2702    }
2703
2704    AI_RAG(prompt, replacement_area, spinner, resetHistory = true) {
2705
2706       let obj = this;
2707        var fdata = new FormData();
2708        fdata.append("prompt", prompt);
2709        fdata.append("AnonUserId", obj.AnonUserId);
2710        fdata.append("resetHistory", resetHistory);
2711        $(`#${replacement_area}`).empty();
2712
2713        //Prevent prompt overrun.
2714        if (prompt.length > 500) {
2715            $(`#${replacement_area}`).html(`<div class="prompt_box"><strong>You asked</strong> - "${prompt}"</div><p>Unfortunately, our AI assistant can only respond to queries that are 500 characters or less.  Please try again with a shorter question.</p>`);
2716            return;
2717        }
2718        $(`#${spinner}`).show();
2719
2720        $.ajax({
2721            url: '/Page/RAG',
2722            async: true,
2723            type: "POST",
2724            data: fdata,
2725            processData: false,
2726            contentType: false,
2727            success: function (data) {
2728                $(`#${spinner}`).hide();
2729                $(`#${replacement_area}`).html(data);
2730
2731                $('#ask_more_box').data('spinner', spinner);
2732                $('#ask_more_box').data('replacement', replacement_area);
2733            },
2734            error: function (x, y, z) {
2735                if (x.status === 429) {
2736                    // Handle rate limit specifically
2737                    $(`#${spinner}`).hide();
2738                    $(`#${replacement_area}`).html(`<div class="prompt_box"><strong>You asked</strong> - "${prompt}"</div><p>Unfortunately, our AI assistant is a little busy right now and can't respond.  Only 10 questions per minute are allowed per logged in user, or per IP address if you are not logged in.  This is to prevent abuse of the AI assistant.  Sorry for the inconvenience.  Try again in a few moments.</p>`);
2739
2740                }
2741                console.log(x.responseText);
2742            }
2743        });
2744    }
2745}
2746
2747/****************  PlayerWeb EventListener **************************************/
2748
2749window.addEventListener('message', function (e) {
2750    try {
2751
2752        if (e.origin !== "https://play.cadmore.media" && e.origin != "https://samoyed-player-dev.azurewebsites.net" && e.origin !== "https://localhost:16301") {
2753            //bad domain.
2754            return;
2755        }
2756
2757        var obj = JSON.parse(e.data);
2758
2759     
2760        if (obj.method === "playStart") {
2761
2762            //Post a record request.
2763            _pageManager.RecordRequest();
2764            _pageManager.RunScriptIfExists('PlayerEventOnStart');
2765        }
2766        else if (obj.method === "playEnd") {
2767            _pageManager.RunScriptIfExists('PlayerEventOnEnd');
2768        }
2769        else if (obj.method === "event_joined") {
2770            _pageManager.RunScriptIfExists('PlayerEventOnJoinEvent');
2771        }
2772        else if (obj.method === "loaded") {
2773            _pageManager.RunScriptIfExists('PlayerEventOnLoaded', obj);
2774        }
2775    }
2776    catch (ex) { }
2777});
2778
2779

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.