PageSourceSearch

https://jazeffect.com.au/scripts/common.js?v=20260927.2

js jazeffect.com.au collected 2026-09-27 05:13:17 UTC 61,282 bytes, 1,733 lines download raw bytes

1/// Utility functions
2function getClientHeight(elementID) {
3    return document.getElementById(elementID).clientHeight;
4}
5
6function checkClassOnElement(elementID, classToCheck) {
7    var element = document.getElementById(elementID);
8    if (element != null) {
9        return element.classList.contains(classToCheck);
10    }
11
12    return false;
13}
14
15function addClassToElement(elementID, classToAdd) {
16    var element = document.getElementById(elementID);
17    if (element != null) {
18        element.classList.add(classToAdd);
19    }    
20}
21
22function addClassToClass(selector, classToAdd) {
23    var elements = document.querySelectorAll(selector);
24    if (elements != null && elements.length > 0) {
25        for (i = 0; i < elements.length; i++) {
26            elements[i].classList.add(classToAdd);
27        }
28    }
29}
30
31function removeClassFromElement(elementID, classToRemove) {
32    var element = document.getElementById(elementID);
33    if (element != null) {
34        element.classList.remove(classToRemove);
35    }    
36}
37
38function removeClassFromClass(selector, classToRemove) {
39    var elements = document.querySelectorAll(selector);
40    if (elements != null && elements.length > 0) {
41        for (i = 0; i < elements.length; i++) {
42            elements[i].classList.remove(classToRemove);
43        }
44    }
45}
46
47function changeImageSrc(elementID, src) {    
48    var element = document.getElementById(elementID);
49    if (element != null) {
50        element.src = src;
51    }
52}
53
54function setElementFocus(elementID, select) {
55    var element = document.getElementById(elementID);
56    if (element != null) {
57        element.focus();
58        if (select)
59        {
60            element.select();
61        }
62    }
63}
64
65function removeElementFocus(elementID) {
66    var element = document.getElementById(elementID);
67    if (element != null) {
68        element.blur();
69    }
70}
71
72function focusNextElement(offset) {
73    //add all elements we want to include in our selection
74    var focussableElements =
75        'a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])';
76    if (document.activeElement) {
77        var focussable = Array.prototype.filter.call(
78            document.querySelectorAll(focussableElements),
79            function (element) {
80                //check for visibility while always include the current activeElement
81                return (
82                    element.offsetWidth > 0 ||
83                    element.offsetHeight > 0 ||
84                    element === document.activeElement
85                );
86            }
87        );
88        var index = focussable.indexOf(document.activeElement);
89        if (index > -1) {
90            var nextElement = focussable[index + offset] || focussable[0];
91            nextElement.focus();
92        }
93    }
94}
95
96function capturePointer(element, pointerId) {
97    if (element) {
98        element.setPointerCapture(pointerId);
99    }
100}
101
102function releasePointer(element, pointerId) {
103    if (element) {
104        element.releasePointerCapture(pointerId);
105    }
106}
107
108function exportElementAsFile(element, filename) {
109    const svgElement = document.querySelector(element + '.mermaid svg');
110    if (!svgElement) {
111        alert('Diagram not found. Please wait for the diagram to load.');
112        return;
113    }
114
115    // Clone the SVG to avoid modifying the original
116    const clonedSvg = svgElement.cloneNode(true);
117
118    // Get the SVG source
119    const svgData = new XMLSerializer().serializeToString(clonedSvg);
120
121    // Create a blob from the SVG data
122    const blob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
123
124    // Create a download link
125    const url = URL.createObjectURL(blob);
126    const link = document.createElement('a');
127    link.href = url;
128    link.download = filename + '.svg';
129
130    // Trigger the download
131    document.body.appendChild(link);
132    link.click();
133    document.body.removeChild(link);
134
135    // Clean up
136    URL.revokeObjectURL(url);
137}
138
139// Layout functions
140function sizeAppBodyContent() {
141    // get the heights of things that are not the body
142    let headerHeight = 0;
143    if (document.querySelector(".app-header")) {
144        headerHeight = document.querySelector(".app-header").offsetHeight;
145    }
146    let bottomHeight = 0;
147    if (document.querySelector(".app-bottom")) {
148        bottomHeight = document.querySelector(".app-bottom").offsetHeight;
149    }
150    const windowHeight = window.innerHeight;
151
152    const contentWrapper = document.querySelector(".content-wrapper");
153
154    if (contentWrapper) {
155        // Set the height of the content wrapper to fill the remaining space
156        contentWrapper.style.height = `${windowHeight - headerHeight - bottomHeight}px`;
157    }
158
159    removeLoadingText();
160}
161
162function removeLoadingText() {
163    const loadingText = document.querySelector(".loading-text");
164    if (loadingText) {
165        // get rid of this element once the content is loaded
166        loadingText.remove();
167    }
168}
169
170function calculateSidePanelTop() {
171    let headerHeight = 0;
172    if (document.querySelector(".app-header")) {
173        headerHeight = document.querySelector(".app-header").offsetHeight;
174    }
175
176    let bottomHeight = 0;
177    if (document.querySelector(".app-bottom")) {
178        bottomHeight = document.querySelector(".app-bottom").offsetHeight;
179    }
180
181    const sidePanel = document.querySelector(".side-panel");
182    if (sidePanel) {
183        // Set the top of the side panel to be below the header 
184        sidePanel.style.top = `${headerHeight}px`;
185        if (bottomHeight > 0) {
186            sidePanel.style.bottom = `${bottomHeight}px`;
187        } else {
188            sidePanel.style.bottom = `0`;
189        }
190    }
191}
192
193function copyTextToClipboard(text) {
194    if (navigator.clipboard) {
195        navigator.clipboard.writeText(text);
196    }
197}
198
199function onDragOver(event) {
200    event.preventDefault();
201}
202
203// Document Component functions
204function uploadDocuments(elementID) {
205    var element = document.getElementById(elementID);
206    if (element != null) {
207        element.click();
208    }
209}
210
211// Dropdown functions
212function showDropDown(elementID) {
213    var element = document.getElementById(elementID);
214    if (element != null) {
215        addClassToElement(elementID, "show");
216        element.isMouseOver = false;
217        element.onmouseenter = () =>
218        {
219            element.isMouseOver = true;
220        }
221        element.onmouseleave = () => {
222            element.isMouseOver = false;
223        }
224    }
225}
226
227function hideDropDown(elementID) {
228    var element = document.getElementById(elementID);
229    if (element != null) {
230        if (!element.isMouseOver) {
231            removeClassFromElement(elementID, "show");
232            element.onmouseover = null;
233        }
234    }
235}
236
237function mapAddress(address) {
238    var geocoder = new google.maps.Geocoder();
239
240    geocoder.geocode({ 'address': address }, function (results, status) {
241        if (status == "OK") {
242            const map = new google.maps.Map(document.getElementById("map"), {
243                zoom: 15,
244                center: results[0].geometry.location,
245            });
246            const marker = new google.maps.Marker({
247                position: results[0].geometry.location,
248                map: map,
249            });
250        }
251    });
252}
253
254function openPrintDialog() {
255    window.print();
256}
257
258function goBack() {
259    window.history.back();
260}
261
262function hasHistory() {
263    return window.history.length > 2;
264}
265
266function scrollTop() {
267    document.documentElement.scrollTop = 0;
268}
269
270function scrollToId(id) {
271    const element = document.getElementById(id);
272    if (element instanceof HTMLElement) {
273        element.scrollIntoView({
274            behavior: "smooth",
275            block: "start",
276            inline: "nearest"
277        });
278    }
279}
280
281function accessibleKeypress(e) {
282
283    if (e.keyCode === 32 || e.keyCode === 13) {
284        e.preventDefault()
285        e.target.click();
286    }
287}
288
289async function startCamera(elementID, deviceId) {
290    let videoElement = document.getElementById(elementID);
291    if (videoElement == null) {
292        return false;
293    }
294
295    let devices = await navigator.mediaDevices.enumerateDevices();
296    let videoDevices = devices.filter(d => d.kind === "videoinput" && d.deviceId === deviceId);
297
298    if (videoDevices.length === 1)
299    {
300        let constraints = {
301            video: {
302                deviceId: {
303                    exact: deviceId
304                }
305            }
306        }
307
308        let stream = await navigator.mediaDevices.getUserMedia(constraints);
309        videoElement.srcObject = stream;
310        videoElement.play();
311
312        return true;
313    }
314
315    return false;
316}
317
318async function stopCamera(elementID) {
319    let videoElement = document.getElementById(elementID);
320    if (videoElement == null) {
321        return false;
322    }
323
324    let stream = videoElement.srcObject;
325    if (stream) {
326        let tracks = stream.getTracks();
327
328        tracks.forEach(function (track) {
329            track.stop();
330        });
331    }
332
333    videoElement.srcObject = null;
334    return true;
335}
336
337async function getCameras() {
338    let devices = await navigator.mediaDevices.enumerateDevices();
339    let videoDevices = devices.filter(d => d.kind === "videoinput");
340
341    return videoDevices;
342}
343
344async function hasCameraPermission() {
345    try {
346        let stream = await navigator.mediaDevices.getUserMedia({
347            video: true
348        });
349
350        let tracks = stream.getTracks();
351
352        tracks.forEach(function (track) {
353            track.stop();
354        });
355
356        return true;
357    }
358    catch (error) {
359        return false;
360    }
361}
362
363function grabFrame(elementID, dotNetObj, external) {
364    let videoElement = document.getElementById(elementID);
365    if (videoElement == null) {
366        return false;
367    }
368
369    let stream = videoElement.srcObject;
370    if (stream) {
371        let settings = stream.getVideoTracks()[0].getSettings();
372
373        let fullResC = document.createElement("canvas");
374        fullResC.width = settings.width;
375        fullResC.height = settings.height;
376        let fullResCtx = fullResC.getContext("2d");
377        fullResCtx.drawImage(videoElement, 0, 0, settings.width, settings.height);
378
379        fullResC.toBlob(async (response) => {
380            var arrayBuffer = await response.arrayBuffer();
381            var binary = "";
382            var bytes = new Uint8Array(arrayBuffer);
383            var len = bytes.byteLength;
384            for (var i = 0; i < len; i++) {
385                binary += String.fromCharCode(bytes[i]);
386            }
387
388            var base64 = window.btoa(binary);
389            dotNetObj.invokeMethodAsync('CallbackToBlob', base64, external);
390        });
391
392        return true;
393    }
394
395    return false;
396}
397
398function calculateMultiSelectHeight(elementID) {
399    var element = document.getElementById(elementID);
400    if (element != null) {
401        var contentElement = document.getElementById(elementID + "Content");
402        element.style.height = contentElement.clientHeight + "px";
403    }
404}
405
406async function requestNotificationPermission() {
407    try {
408        await Notification.requestPermission();
409
410        return true;
411    }
412    catch {
413        return false;
414    }
415}
416
417function createPushNotification(title, options) {
418    return new Notification(title, options);
419}
420
421function downloadFromUrl(url, fileName) {
422    const anchorElement = document.createElement('a');
423    anchorElement.href = url;
424    anchorElement.download = fileName;
425
426    anchorElement.click();
427    anchorElement.remove();
428}
429
430function iOS() {
431    return [
432        'iPad Simulator',
433        'iPhone Simulator',
434        'iPod Simulator',
435        'iPad',
436        'iPhone',
437        'iPod'
438    ].includes(navigator.platform)
439        // iPad on iOS 13 detection
440        || (navigator.userAgent.includes("Mac") && "ontouchend" in document)
441}
442
443function Android() {
444    return navigator.userAgent.toLowerCase().indexOf("android") > -1;
445}
446
447function downloadFromByteArray(byteArray, fileName, contentType) {
448    let uint8Array = null;
449
450    if (byteArray instanceof Uint8Array) {
451        // already passed in as the correct type
452        uint8Array = byteArray;
453    }
454    else {
455        // Convert base64 string to numbers array.
456        const numArray = atob(byteArray).split('').map(c =>
456 c.charCodeAt(0));
457
458        // Convert numbers array to Uint8Array object.
459        uint8Array = new Uint8Array(numArray);
460    }
461
462    // Wrap it by Blob object.
463    const blob = new Blob([uint8Array], { type: contentType });
464
465    // Create "object URL" that is linked to the Blob object.
466    const url = URL.createObjectURL(blob);
467
468    // Invoke download helper function that implemented in 
469    // the earlier section of this article.
470    downloadFromUrl(url, fileName);
471
472    // At last, release unused resources.
473    URL.revokeObjectURL(url);
474}
475
476function trackBlazorPageView(path) {
477    // --- Google Tag Manager / GA4 via GTM ---
478    if (window.dataLayer && typeof window.dataLayer.push === "function") {
479        window.dataLayer.push({
480            event: 'pageview',
481            page_path: path
482        });
483    }
484
485    // --- LinkedIn Insight Tag ---
486    if (typeof window.lintrk === "function") {
487        window.lintrk('track', { conversion_id: 0, path: path });
488    }
489
490    // --- Meta Pixel (Facebook) ---
491    if (typeof window.fbq === "function") {
492        window.fbq('track', 'PageView', { path: path });
493    }
494
495    // --- Microsoft Clarity ---
496    if (typeof window.clarity === "function") {
497        window.clarity("track", "pageview", { path: path });
498    }
499};
500
501function initialiseLinkedInInsights(partnerId) {
502    _linkedin_partner_id = partnerId;
503    window._linkedin_data_partner_ids = window._linkedin_data_partner_ids || [];
504    window._linkedin_data_partner_ids.push(_linkedin_partner_id);
505
506    (function (l) {
507        if (!l) {
508            window.lintrk = function (a, b) { window.lintrk.q.push([a, b]) };
509            window.lintrk.q = []
510        }
511        var s = document.getElementsByTagName("script")[0];
512        var b = document.createElement("script");
513        b.type = "text/javascript"; b.async = true;
514        b.src = "https://snap.licdn.com/li.lms-analytics/insight.min.js";
515        s.parentNode.insertBefore(b, s);
516    })(window.lintrk);
517}
518
519function initialiseGoogleAnalytics(GAID, cookieDomain) {
520    const script = document.createElement("script");
521    script.async = true;
522    script.src = "https://www.googletagmanager.com/gtag/js?id=" + GAID;
523
524    document.head.appendChild(script);
525
526    window.dataLayer = window.dataLayer || [];
527    window.gtag = function gtag(){ dataLayer.push(arguments); }
528
529    gtag('js', new Date());
530    gtag('config', GAID, {
531        cookie_domain: cookieDomain,
532        cookie_flags: 'SameSite=None;Secure'
533    });
534}
535
536function initialiseMetaPixel(appId) {
537    !function (f, b, e, v, n, t, s) {
538        if (f.fbq) return; n = f.fbq = function () {
539            n.callMethod ?
540                n.callMethod.apply(n, arguments) : n.queue.push(arguments)
541        };
542        if (!f._fbq) f._fbq = n; n.push = n; n.loaded = !0; n.version = '2.0';
543        n.queue = []; t = b.createElement(e); t.async = !0;
544        t.src = v; s = b.getElementsByTagName(e)[0];
545        s.parentNode.insertBefore(t, s)
546    }(window, document, 'script',
547        'https://connect.facebook.net/en_US/fbevents.js');
548    fbq('init', appId);
549    fbq('track', 'PageView');
550}
551
552function initialiseMicrosoftClarity(projectId) {
553    (function (c, l, a, r, i, t, y) {
554        c[a] = c[a] || function () { (c[a].q = c[a].q || []).push(arguments) };
555        t = l.createElement(r); t.async = 1; t.src = "https://www.clarity.ms/tag/" + i;
556        y = l.getElementsByTagName(r)[0]; y.parentNode.insertBefore(t, y);
557    })(window, document, "clarity", "script", projectId);
558}
559
560function updateCanonicalLinkElement(newUrl) {
561    const existingCanonicalLink = document.querySelector('link[rel="canonical"]');
562    if (existingCanonicalLink) {
563        existingCanonicalLink.href = newUrl;
564    } else {
565        const newCanonicalLink = document.createElement('link');
566        newCanonicalLink.rel = 'canonical';
567        newCanonicalLink.href = newUrl;
568        document.head.appendChild(newCanonicalLink);
569    }
570
571    const existingOgUrl = document.querySelector('meta[property="og:url"]');
572    if (existingOgUrl) {
573        existingOgUrl.content = newUrl;
574    } else {
575        const newOgUrl = document.createElement('meta');
576        newOgUrl.setAttribute('property', 'og:url');
577        newOgUrl.content = newUrl;
578        document.head.appendChild(newOgUrl);
579    }
580}
581
582function updatePageHeaderElements(title, robots, description) {
583    if (title && title.length > 0) {
584        const existingTitle = document.querySelector('title');
585        if (existingTitle) {
586            existingTitle.innerHTML = title;
587        } else {
588            const newTitle = document.createElement('title');
589            newTitle.innerHTML = title;
590            document.head.appendChild(newTitle);
591        }
592
593        // open graph properties
594        const existingOgTitle = document.querySelector('meta[property="og:title"]');
595        if (existingOgTitle) {
596            existingOgTitle.content = title;
597        } else {
598            const newOgTitle = document.createElement('meta');
599            newOgTitle.setAttribute('property', 'og:title');
600            newOgTitle.content = title;
601            document.head.appendChild(newOgTitle);
602        }
603    } else {
604        const existingTitle = document.querySelector('title');
605        if (existingTitle) {
606            document.head.removeChild(existingTitle);
607        }
608
609        const existingOgTitle = document.querySelector('meta[property="og:title"]');
610        if (existingOgTitle) {
611            document.head.removeChild(existingOgTitle);
612        }
613    }
614
615    if (robots && robots.length > 0) {
616        const existingMetaRobots = document.querySelector('meta[name="robots"]');
617        if (existingMetaRobots) {
618            existingMetaRobots.content = robots;
619        } else {
620            const newMetaRobots = document.createElement('meta');
621            newMetaRobots.name = 'robots';
622            newMetaRobots.content = robots;
623            document.head.appendChild(newMetaRobots);
624        }
625    } else {
626        const existingMetaRobots = document.querySelector('meta[name="robots"]');
627        if (existingMetaRobots) {
628            document.head.removeChild(existingMetaRobots)
629        }
630    }
631
632    if (description && description.length > 0) {
633        const existingMetaDescription = document.querySelector('meta[name="description"]');
634        if (existingMetaDescription) {
635            existingMetaDescription.content = description;
636        } else {
637            const newMetaDescription = document.createElement('meta');
638            newMetaDescription.name = 'description';
639            newMetaDescription.content = description;
640            document.head.appendChild(newMetaDescription);
641        }
642
643        // open graph properties
644        const existingOgDescription = document.querySelector('meta[property="og:description"]');
645        if (existingOgDescription) {
646            existingOgDescription.content = description;
647        } else {
648            const newOgDescription = document.createElement('meta');
649            newOgDescription.setAttribute('property', 'og:description');
650            newOgDescription.content = description;
651            document.head.appendChild(newOgDescription);
652        }
653    } else {
654        const existingMetaDescription = document.querySelector('meta[name="description"]');
655        if (existingMetaDescription) {
656            document.head.removeChild(existingMetaDescription)
657        }
658
659        const existingOgDescription = document.querySelector('meta[property="og:description"]');
660        if (existingOgDescription) {
661            document.head.removeChild(existingOgDescription)
662        }
663    }
664}
665
666window.onscroll = function () { setStickyHeader(".site-layout header", ".site-layout main") };
667
668function setStickyHeader(headerSelector, contentSelector) {
669    var header = document.querySelector(headerSelector);
670    var content = document.querySelector(contentSelector);
671
672    if (header != null && content != null) {
673        if (header.offsetTop > content.offsetTop) {
674            header.classList.add("sticky-header");
675        } else {
676            header.classList.remove("sticky-header");
677        }
678    }
679}
680
681//Rich Text Editor
682window.tinymceBaseConfig = {
683    setup: function (editor) {
684        var beautifyOptions = {
685            indent_size: 2,
686            wrap_line_length: 0,
687            indent_inner_html: true,
688            inline: [
689                'a', 'abbr', 'b', 'br', 'code', 'em', 'i', 'img',
690                's', 'small', 'span', 'strong'
691            ] // 'button','input','label','select','textarea' intentionally left out of this list
692        };
693
694        editor.on('GetContent', function (e) {
695            if (e.source_view && window.html_beautify) {
696                e.content = window.html_beautify(e.content, beautifyOptions);
697            }
698        });
699
700        editor.on('BeforeSetContent', function (e) {
701            if (e.source_view && window.html_beautify) {
702                e.content = window.html_beautify(e.content, beautifyOptions);
703            }
704        });
705    }
706};
707
708// TODO this will need to be enhanced to cope with more than one editor on a page
709window.richTextEditor = null;
710function richTextEditorInit(dotnetRef) {
711    window.richTextEditor = dotnetRef;
712}
713
714function richTextEditorDispose() {
715    window.richTextEditor = null;
716}
717
718// An unsubstituted {image:url} is not an address, so the browser fetches nonsense and draws the
719// broken-image glyph. The editing surface doubles as the preview, and a canvas of broken icons
720// reads as something wrong with the block rather than as a hole the renderer will fill in later.
721// content: url() replaces an img's own graphic, so the placeholder the site already shows for a
722// page with no meta image stands in for the picture - and because it is only a style inside the
723// iframe, the HTML that gets saved still contains the token the designer typed.
724// max-width is there for the block that leaves its images unsized: the placeholder then falls back
725// to its own dimensions, and a graphic wider than the editor would scroll the canvas sideways.
726var tokenImagePlaceholderStyle =
727    'img[src*="{"][src*="}"] {' +
728    ' content: url("/images/placeholder-image.svg");' +
729    ' max-width: 100%;' +
730    ' }';
731
732function richTextEditorContentStyle(id, contentStyle) {
733    // Get a reference to the iframe
734    var iframe = document.getElementById(id + '_ifr');
735    if (!iframe) return;
736
737    // Access the iframe's document
738    var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
739    if (!iframeDoc) return;
740
741    // Remove all <style> elements
742    var styles = iframeDoc.getElementsByTagName('style');
743    while (styles.length > 0) {
744        styles[0].parentNode.removeChild(styles[0]);
745    }
746
747    // Create a new style element
748    var style = document.createElement('style');
749    style.id = id + '_customStyle'; // Assign an ID for easy reference
750    // The placeholder goes first so a block that styles its own images still wins the cascade
751    style.innerHTML = tokenImagePlaceholderStyle + '\n' + (contentStyle || '');
752    iframeDoc.head?.appendChild(style);
753};
754
755// Ring animation timer
756function animateCloseRing(elementID, color, duration) {
757    var element = document.getElementById(elementID + "-close-ring");
758    if (element) {
759        element.setAttribute("data-animate", "run");
760    }
761    var step = 0.5;
762    var period = duration / 720;
763
764    while (period < 15) {
765        if (step == 0.5) {
766            step = 1
767        } else if (step == 1) {
768            step = 2
769        } else if (step == 2) {
770            step = 3
771        } else if (step == 3) {
772            step = 6
773        } else if (step == 6) {
774            step = 12
775        } else if (step == 12) {
776            step = 24
777        }
778
779        var period = duration / (360 / step);
780    }
781
782    setTimeout(runAnimateCloseRing, period, elementID, color, 0, step, period)
783}
784
785function runAnimateCloseRing(elementID, color, angle, delta, period) {
786    var angle = angle + delta;
787
788    var element = document.getElementById(elementID + "-close-ring");
789    if (element) {
790        if (element.dataset.animate == "stop") {
791            return;
792        }
793
794        element.style.backgroundImage = `conic-gradient(var(--${
794color}-lighter) 0deg, var(--${color}-lighter) ${angle}deg, var(--${color}-darker) 0deg)`;
795    }
796
797    if (angle < 360) {
798        setTimeout(runAnimateCloseRing, period, elementID, color, angle, delta, period)
799    }
800}
801
802function stopAnimateCloseRing(elementID) {
803    var element = document.getElementById(elementID + "-close-ring");
804    if (element) {
805        element.setAttribute("data-animate", "stop");
806    }
807}
808
809/* CMS Designer Helper functions
810 *
811 * Every image inside a participating block is a swap target - no marker needed on the image
812 * itself. This is a page designer, so a creator shouldn't have to re-author a block's HTML just
813 * to be allowed to change a picture in it. Which blocks take part is decided by
814 * CMS_IMAGE_SWAP_SCOPE below.
815 *
816 * Note that nothing here reads block markup back out of the DOM. All text editing goes through
817 * TinyMCE, which owns its own content and writes it to the model through its binding, and an
818 * image swap reports only which image changed (see updateImageSrc). That means the designer is
819 * free to decorate canvas markup - highlights, disabled controls - without any of it being able
820 * to leak into the block's saved HTML.
821 */
822const CMS_IMAGE_TARGET_SELECTOR = "img";
823
824/* Only blocks whose HTML is rendered straight onto the canvas take part in image swapping. The
825 * designer marks those with .cms-image-swap (see HostsInContentImages) - gallery blocks, rich
826 * text blocks and the component-rendered block types are left unmarked because they manage
827 * their own images, and layout spacers are unmarked because dropping there creates a new
828 * gallery block instead. */
829const CMS_IMAGE_SWAP_SCOPE = ".cms-image-swap";
830
831/* The image the pointer is currently over, consumed by updateImageSrc. */
832let cmsImageDropTarget = null;
833
834function updateImageSrc(elementID, imageUrl, imageAlt, dotNetObject) {
835    const target = document.getElementById(elementID);
836    if (target == null || target.closest(CMS_IMAGE_SWAP_SCOPE) == null) {
837        // OnDrop already refuses these, but never rewrite the markup of a block that owns its
838        // own images even if something calls in here for one
839        return;
840    }
841
842    // Prefer whichever image the pointer was actually over when the drop happened - the drop
843    // target is the whole block, so without this every drop replaces the block's first image.
844    // Tracked in a variable rather than read back off the highlight class, so it can't be
845    // cleared out from under us by whichever of drop/dragend the browser dispatches first.
846    // Requiring it to still be inside the block that took the drop makes a stale value from
847    // an earlier drag harmless.
848    const images = target.querySelectorAll(CMS_IMAGE_TARGET_SELECTOR);
849
850    const image = cmsImageDropTarget != null && target.contains(cmsImageDropTarget)
851        ? cmsImageDropTarget
852        : images[0];
853
854    if (image == null) {
855        return;
856    }
857
858    // Hand back which image changed rather than the block's rendered HTML. The canvas renders
859    // HTMLContent with its {token} placeholders already expanded, so reading the DOM back would
860    // save those expanded values over the author's tokens - .NET patches the one img tag in the
861    // stored HTML instead and re-renders from it. Expanding tokens never adds or removes an img
862    // tag, so this index lines up with the img tags in the stored HTML.
863    const index = Array.prototype.indexOf.call(images, image);
864    if (index < 0) {
865        return;
866    }
867
868    dotNetObject.invokeMethodAsync('OnImageSwapped', target.getAttribute("data-id"), index, imageUrl, imageAlt);
869}
870
871/* Highlighting is a single class on the canvas container, with the per-image styling done in
872 * CSS (see .image-drag-active in app.css). Adding and removing a class on every individual
873 * image instead left highlights stranded whenever the matching reset call didn't arrive, and
874 * meant every image had to be walked twice per drag. */
875function activateImageTargets() {
876    cmsImageDropTarget = null;
877
878    const designer = document.querySelector(".cms-canvas-container");
879    if (designer != null) {
880        designer.classList.add("image-drag-active");
881    }
882}
883
884function resetImageTargets() {
885    const designer = document.querySelector(".cms-canvas-container");
886    if (designer != null) {
887        designer.classList.remove("image-drag-active");
888    }
889
890    clearImageDropTarget();
891}
892
893function clearImageDropTarget() {
894    const current = document.querySelector("img.image-drop-target");
895    if (current != null) {
896        current.classList.remove("image-drop-target");
897        if (current.classList.length === 0) {
898            current.removeAttribute("class");
899        }
900    }
901}
902
903/**
904 * Marks the image under the pointer while an image is being dragged, so the creator can see
905 * which one a drop will replace and updateImageSrc knows which one to swap. dragover is used
906 * because :hover isn't updated during a native drag.
907 */
908function trackImageDropTarget(e) {
909    const designer = document.querySelector(".cms-canvas-container");
910    if (designer == null || !designer.classList.contains("image-drag-active")) {
911        return;
912    }
913
914    let image = e.target instanceof Element
915        ? e.target.closest(CMS_IMAGE_TARGET_SELECTOR)
916        : null;
917
918    // ignore images that belong to something managing its own images, so the creator never
919    // gets an affordance for a swap that OnDrop is going to refuse
920    if (image != null && image.closest(CMS_IMAGE_SWAP_SCOPE) == null) {
921        image = null;
922    }
923
924    if (cmsImageDropTarget === image) {
925        return;
926    }
927
928    clearImageDropTarget();
929    cmsImageDropTarget = image;
930
931    if (image != null) {
932        image.classList.add("image-drop-target");
933    }
934}
935
936/* Safety net: dragend bubbles to the document from whatever started the drag, so the highlight
937 * always gets cleared even when the Blazor drag-end callback doesn't make it back (drag
938 * cancelled, ended outside the window, source element re-rendered mid-drag). Only the
939 * highlight is cleared here - cmsImageDropTarget is kept so that it doesn't matter whether
940 * this runs before or after the drop is handled. */
941document.addEventListener("dragend", resetImageTargets);
942
943/* Tenant shortcut floating over the top right of the site
944 *
945 * A tenant user who logged in with "remember me" has their token in local storage (see the tenant
946 * app's login page). The visitor site is rendered on the server so it can't know that, which is why
947 * the link is rendered hidden and only revealed here. This is a convenience shortcut, not a
948 * permission check - the tenant app still validates the token when the link is followed.
949 */
950const TENANT_TOKEN_KEY = "token";
951
952function showTenantLink() {
953    const link = document.querySelector(".site-tenant-link");
954    if (link == null) {
955        return;
956    }
957
958    let token = null;
959    try {
960        token = localStorage.getItem(TENANT_TOKEN_KEY);
961    } catch {
962        // storage can be blocked entirely (private browsing, cookie settings), in which case
963        // there is no remembered login to offer
964        return;
965    }
966
967    if (!token || isTokenExpired(token)) {
968        return;
969    }
970
971    link.hidden = false;
972
973    // The link is floating clear of the layout, so on a site that has a caption it would sit on
974    // top of whatever is at the right hand end of it. Keeping the caption clear of the corner the
975    // link occupies is measured from the link itself, so it holds whatever the link ends up being
976    // sized at - it can only be measured once the link is showing.
977    const caption = document.querySelector(".caption-wrapper");
978    if (caption != null) {
979        const gap = 8;
980        caption.style.paddingRight = `${window.innerWidth - link.getBoundingClientRect().left + gap}px`;
981    }
982}
983
984/* Treats anything that isn't a readable JWT as expired - the link is only worth showing when we
985 * can see the token is still good, and letting the tenant app reject it would just bounce the
986 * user to the login page. */
987function isTokenExpired(token) {
988    const parts = token.split(".");
989    if (parts.length !== 3) {
990        return true;
991    }
992
993    try {
994        const payload = JSON.parse(atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")));
995        if (typeof payload.exp !== "number") {
996            return true;
997        }
998
999        return payload.exp * 1000 <= Date.now();
1000    } catch {
1001        return true;
1002    }
1003}
1004
1005document.addEventListener("DOMContentLoaded", showTenantLink);
1006
1007function sendDataMessage(message, target, elementID) {
1008    if (elementID == undefined) {
1009        if (window.parent) {
1010            window.parent.postMessage(message, target);
1011        }
1012    } else {
1013        var element = document.getElementById(elementID);
1014        if (element) {
1015            element.contentWindow.postMessage(message, target);
1016        }
1017    }
1018}
1019
1020const observer = new IntersectionObserver((entries, observer) => {
1021    entries.forEach(entry => {
1022        if (entry.target.dataset.visibility) {
1023            if (entry.isIntersecting) {
1024                if (!entry.target.classList.contains(entry.target.dataset.visibility)) {
1025                    entry.target.classList.add(entry.target.dataset.visibility);
1026                }
1027                observer.unobserve(entry.target);
1028            }
1029        }
1030    });
1031}, { threshold: 0.25 });
1032
1033window.addEventListener('scroll', () => {
1034    document.body.style.setProperty('--scroll', window.scrollY / (document.body.offsetHeight - window.innerHeight));
1035}, false);
1036
1037function advanceCarousel(carouselElement) {
1038    if (carouselElement) {
1039        let interval = carouselElement.dataset.timer;
1040        let element = carouselElement.querySelector('.carousel-button[data-direction="next"]');
1041
1042        if (element && carouselElement.dataset.auto == "true") {
1043            element.click();
1044            carouselElement.dataset.auto = true; // set this back to true again because the button click set it to false
1045        }
1046
1047        if (interval > 600) {
1048            setTimeout(() => {
1049                advanceCarousel(carouselElement);
1050            }, interval, carouselElement);
1051        }
1052    }
1053}
1054
1055function expandAllAccordions(parentID) {
1056
1057    let parent = document.getElementById(parentID);
1058
1059    if (!parent) return;
1060
1061    let toggles = parent.querySelectorAll('[data-toggle="collapse"]');
1062    for (let item of toggles) {
1063        let itemID = item.getAttribute("id");
1064
1065        if (item.dataset.target) {
1066            var target = document.querySelector(item.dataset.target);
1067            if (target != null) {
1068
1069
1070                let elementID = target.getAttribute("id");
1071
1072                var icon = item.querySelector(".accordion-icon");
1073
1074                if (checkClassOnElement(elementID, "accordion-collapse")) {
1075                    removeClassFromElement(elementID, "accordion-collapse");
1076                    removeClassFromElement(itemID, "collapsed");
1077                    if (icon != null) {
1078                        icon.classList.add("fa-rotate-180");
1079                    }
1080                }
1081            }
1082        }
1083    }
1084}
1085
1086function expandAccordion(headerID) {
1087
1088    let header = document.getElementById(headerID);
1089
1090    if (!header) return;
1091
1092    let itemID = header.getAttribute("id");
1093
1094    if (header.dataset.target) {
1095        var target = document.querySelector(header.dataset.target);
1096        if (target != null) {
1097
1098            let elementID = target.getAttribute("id");
1099
1100            var icon = header.querySelector(".accordion-icon");
1101
1102            if (checkClassOnElement(elementID, "accordion-collapse")) {
1103                removeClassFromElement(elementID, "accordion-collapse");
1104                removeClassFromElement(itemID, "collapsed");
1105                if (icon != null) {
1106                    icon.classList.add("fa-rotate-180");
1107                }
1108            }
1109        }
1110    }
1111}
1112
1113function collapseAllAccordions(parentID) {
1114
1115    let parent = document.getElementById(parentID);
1116
1117    if (!parent) return;
1118
1119    let toggles = parent.querySelectorAll('[data-toggle="collapse"]');
1120
1121    for (let item of toggles) {
1122        let itemID = item.getAttribute("id");
1123
1124        if (item.dataset.target) {
1125            var target = document.querySelector(item.dataset.target);
1126            if (target != null) {
1127
1128                let elementID = target.getAttribute("id");
1129
1130                var icon = item.querySelector(".accordion-icon");
1131
1132                if (!checkClassOnElement(elementID, "accordion-collapse")) {
1133                    addClassToElement(elementID, "accordion-collapse");
1134                    addClassToElement(itemID, "collapsed");
1135
1136                    if (icon != null) {
1137                        icon.classList.remove("fa-rotate-180");
1138                    }
1139                }
1140            }
1141        }
1142    }
1143}
1144
1145function hasCollapsedAccordions() {
1146    let accordions = document.querySelectorAll('.accordion-collapse');
1147    if (accordions) {
1148        return true;
1149    } else {
1150        return false;
1151    }
1152}
1153
1154function attachHandlers(dotNetObject) {
1155    let transitions = document.querySelectorAll('[data-transition]');
1156    if (transitions.length > 0)
1157    {
1158        let observers = {};
1159
1160        for (let item of transitions) {
1161            if (!item.hasAttribute("observered")) {
1162                var dataThreshold = item.dataset.transition;
1163                if (!dataThreshold) {
1164                    dataThreshold = "0.5";
1165                }
1166
1167                if (observers[dataThreshold]) {
1168                    observers[dataThreshold].observe(item);
1169                }
1170                else {
1171                    observers[dataThreshold] = new IntersectionObserver(entries => {
1172                        entries.forEach(entry => {
1173                            if (entry.isIntersecting) {
1174                                entry.target.dataset.transitionIn = true;
1175                                delete entry.target.dataset.transitionOut;
1176                            }
1177                            else {
1178                                entry.target.dataset.transitionOut = true;
1179                                delete entry.target.dataset.transitionIn;
1180                            }
1181                        });
1182                    }, { threshold: dataThreshold });
1183
1184                    observers[dataThreshold].observe(item);
1185                }
1186
1187                item.setAttribute("observered", "true");
1188            }
1189        }
1190    }
1191
1192    let carousels = document.querySelectorAll('[data-scroll="carousel"]');
1193    for (let item of carousels) {
1194        let containerID = item.querySelector('.carousel-container').getAttribute("id");
1195        let buttons = item.querySelectorAll('.carousel-button');
1196
1197        for (let button of buttons) {
1198            if (!button.hasAttribute("attached")) {
1199                if (button.dataset.direction == "back") {
1200                    button.addEventListener("click", function (e) {
1201                        e.stopPropagation();
1202
1203                        if (button.dataset.busy == "true") {
1204                            return;
1205                        }
1206
1207                        // stop the auto rotate if the user clicks the back button
1208                        item.dataset.auto = false;
1209                        button.dataset.busy = true;
1210
1211                        const container = document.querySelector('#' + containerID);
1212                        const items = container.querySelectorAll(".carousel-item");
1213
1214                        let prevItem = null;
1215
1216                        for (let slide of items) {
1217                            if (slide.dataset.visible == "true") {
1218                                slide.dataset.visible = false;
1219                                slide.style.transform = "translateX(100%)";
1220                                slide.style.opacity = 0;
1221
1222                                setTimeout(() => {
1223                                    slide.style.display = "none";
1224                                    slide.style.opacity = 1;
1225                                    slide.style.transform = "translateX(0)";
1226                                    slide.style.zIndex = 0;
1227                                }, 600, slide);
1228
1229                                if (prevItem) {
1230                                    prevItem.style.display = "block";
1231                                    prevItem.style.opacity = 1;
1232                                    prevItem.dataset.visible = true;
1233
1234                                    setTimeout(() => {
1235                                        prevItem.style.display = "block";
1236                                        prevItem.style.zIndex = 10;
1237                                    }, 600, prevItem);
1238                                }
1239
1240                                break;
1241                            } else {
1242                                prevItem = slide;
1243                            }
1244                            
1245                        }
1246
1247                        // items can be empty when an author has emptied a carousel block out
1248                        // in the designer, and there is no slide to wrap around to
1249                        if (!prevItem && items.length > 0) {
1250                            let slide = items[items.length - 1];
1251
1252                            slide.style.opacity = 1;
1253                            slide.style.display = "block";
1254                            slide.dataset.visible = true;
1255
1256                            setTimeout(() => {
1257                                slide.style.display = "block";
1258                                slide.style.zIndex = 10;
1259                            }, 600, slide);
1260                        }
1261
1262                        setTimeout(() => {
1263                            button.dataset.busy = false;
1264                        }, 600, button);
1265                    });
1266                } else {
1267                    button.addEventListener("click", function (e) {
1268                        e.stopPropagation();
1269
1270                        if (button.dataset.busy == "true") {
1271                            return;
1272                        }
1273
1274                        // stop the auto rotate if the user clicks the next button
1275                        item.dataset.auto = false;
1276                        button.dataset.busy = true;
1277
1278                        const container = document.querySelector('#' + containerID);
1279                        const items = container.querySelectorAll(".carousel-item");
1280
1281                        let showNext = false;
1282                        for (let slide of items) {
1283                            if (showNext) {
1284                                slide.dataset.visible = true;
1285                                slide.style.display = "block";
1286                                slide.style.opacity = 1;
1287
1288                                setTimeout(() => {
1289                                    slide.style.display = "block";
1290                                    slide.style.zIndex = 10;
1291                                }, 600, slide);
1292
1293                                showNext = false;
1294                                break;
1295                            }
1296
1297                            if (slide.dataset.visible == "true") {
1298                                slide.dataset.visible = false;
1299                                slide.style.transform = "translateX(-100%)";
1300                                slide.style.opacity = 0;
1301
1302                                setTimeout(() => {
1303                                    slide.style.display = "none";
1304                                    slide.style.transform = "translateX(0)";
1305                                    slide.style.opacity = 1;
1306                                    slide.style.zIndex = 0;
1307                                }, 600, slide);
1308
1309                                showNext = true;
1310                            }
1311                        }
1312
1313                        if (showNext) {
1314                            let slide = items[0];
1315
1316                            slide.style.opacity = 1;
1317                            slide.style.display = "block";
1318                            slide.dataset.visible = true;
1319
1320                            setTimeout(() => {
1321                                slide.style.display = "block";
1322                                slide.style.zIndex = 10;
1323                            }, 600, slide);
1324                        }
1325
1326                        setTimeout(() =>
1327                        {
1328                            button.dataset.busy = false;
1329                        }, 600, button);
1330                    });
1331                }
1332
1333                button.setAttribute("attached", "true");
1334            }
1335        }
1336
1337        if (!item.hasAttribute("attached")) {
1338            // show the item with the data visible property or the first item if 
1339            // none have been set to visible
1340            const container = document.querySelector('#' + containerID);
1341            const items = container.querySelectorAll(".carousel-item");
1342
1343            let visible = false;
1344
1345            for (let slide of items) {
1346                if (slide.dataset.visible == "true") {
1347                    slide.style.display = "block";
1348                    slide.style.zIndex = 10;
1349
1350                    visible = true;
1351                }
1352            }
1353
1354            if (!visible && items.length > 0) {
1355                items[0].style.display = "block";
1356                items[0].style.opacity = 1;
1357                items[0].style.zIndex = 10;
1358            }
1359
1360            container.addEventListener("click", function (e) {
1361                e.stopPropagation();
1362                if (container.parentElement.dataset.auto == "true") {
1363                    container.parentElement.dataset.auto = false;
1364                }
1365                else {
1366                    container.parentElement.dataset.auto = true;
1367                }
1368            });
1369
1370            // if there is a value set for the data-timer attribute
1371            if (item.dataset.timer > 600) {
1372                setTimeout(() => {
1373                    advanceCarousel(item);
1374                }, item.dataset.timer, item);
1375            }
1376
1377            item.setAttribute("attached", "true");
1378        }
1379    }
1380
1381    let scrollers = document.querySelectorAll('[data-scroll="horizontal"]');
1382    for (let item of scrollers) {
1383        let containerID = item.querySelector('.hs-container').getAttribute("id");
1384        let buttons = item.querySelectorAll('.hs-button');
1385
1386        for (let button of buttons) {
1387            if (!button.hasAttribute("attached")) {
1388                if (button.dataset.step) {
1389                    if (button.dataset.direction == "back") {
1390                        button.addEventListener("click", function (e) {
1391                            e.stopPropagation();
1392                            const step = parseInt(button.dataset.step);
1393
1394                            const container = document.querySelector('#' + containerID);
1395                            const items = container.querySelectorAll(".hs-item");
1396
1397                            const bounding = container.getBoundingClientRect();
1398                            let firstVisible = -1;
1399
1400                            // find the first visible item in the list
1401                            for (var i = 0; i < items.length; i++) {
1402                                const itemBounding = items[i].getBoundingClientRect();
1403
1404                                if ((itemBounding.x + itemBounding.width / 2) <= (bounding.x + bounding.width) &&
1405                                    (itemBounding.x + itemBounding.width / 2) >= (bounding.x)) {
1406                                    firstVisible = i;
1407                                    break;
1408                                }
1409                            }
1410
1411                            if (firstVisible >= step) {
1412                                items[firstVisible - step].scrollIntoView({
1413                                    behavior: 'smooth',
1414                                    block: 'nearest',
1415                                    inline: 'nearest'
1416                                });
1417                            }
1418                            else {
1419                                items[0].scrollIntoView({
1420                                    behavior: 'smooth',
1421                                    block: 'nearest',
1422                                    inline: 'nearest'
1423                                });
1424                            }
1425                        });
1426                    } else {
1427                        button.addEventListener("click", function (e) {
1428                            e.stopPropagation();
1429                            const step = parseInt(button.dataset.step);
1430
1431                            const container = document.querySelector('#' + containerID);
1432                            const items = container.querySelectorAll(".hs-item");
1433
1434                            const bounding = container.getBoundingClientRect();
1435                            let firstHidden = items.length + 1;
1436                            let foundVisible = false;
1437
1438                            // find the first hidden item after a visible item in the list
1439                            for (var i = 0; i < items.length; i++) {
1440                                const itemBounding = items[i].getBoundingClientRect();
1441
1442                                if ((itemBounding.x + itemBounding.width / 2) <= (bounding.x + bounding.width) &&
1443                                    (itemBounding.x + itemBounding.width / 2) >= (bounding.x)) {
1444                                    foundVisible = true;
1445                                }
1446                                else {
1447                                    if (foundVisible) {
1448                                        firstHidden = i;
1449                                        break;
1450                                    }
1451                                }
1452                            }
1453
1454                            if (firstHidden <= items.length - step) {
1455                                items[firstHidden - 1 + step].scrollIntoView({
1456                                    behavior: 'smooth',
1457                                    block: 'nearest',
1458                                    inline: 'nearest'
1459                                });
1460                            }
1461                            else {
1462                                items[items.length - 1].scrollIntoView({
1463                                    behavior: 'smooth',
1464                                    block: 'nearest',
1465                                    inline: 'nearest'
1466                                });
1467                            }
1468                        });
1469                    }
1470                }
1471
1472                button.setAttribute("attached", "true");
1473            }
1474        }
1475    }
1476
1477    /* Accordion Controls */
1478    let toggles = document.querySelectorAll('[data-toggle="collapse"]');
1479    for (let item of toggles) {
1480        let itemID = item.getAttribute("id");
1481
1482        if (item.dataset.target) {
1483            var target = document.querySelector(item.dataset.target);
1484            if (target != null) {
1485                if (!item.hasAttribute("attached")) {
1486                    let elementID = target.getAttribute("id");
1487
1488                    item.addEventListener("click", function (e) {
1489                        e.stopPropagation();
1490
1491                        var icon = item.querySelector(".accordion-icon");
1492
1493                        if (checkClassOnElement(elementID, "accordion-collapse")) {
1494                            removeClassFromElement(elementID, "accordion-collapse");
1495                            removeClassFromElement(itemID, "collapsed");
1496
1497                            if (icon != null) {
1498                                icon.classList.add("oi-rotate-180");
1499                            }
1500                        }
1501                        else {
1502                            addClassToElement(elementID, "accordion-collapse");
1503                            addClassToElement(itemID, "collapsed");
1504
1505                            if (icon != null) {
1506                                icon.classList.remove("oi-rotate-180");
1507                            }
1508                        }
1509                    });
1510
1511                    item.setAttribute("attached", "true");
1512                }
1513            }
1514        }
1515    }
1516
1517    let iframes = document.querySelectorAll('iframe[data-host="true"]');
1518    for (let item of iframes) {
1519        if (dotNetObject && !item.hasAttribute("attached")) {
1520            window.onmessage = (e) => {
1521                dotNetObject.invokeMethodAsync('CallbackForMessage', e.data, e.origin);
1522            }
1523
1524            item.setAttribute("attached", "true");
1525        }
1526    }
1527
1528    let embeds = document.querySelectorAll('[data-embedded="true"]');
1529    for (let item of embeds) {
1530        if (dotNetObject && !item.hasAttribute("attached")) {
1531            window.onmessage = (e) => {
1532                dotNetObject.invokeMethodAsync('CallbackForMessage', e.data, e.origin);
1533            }
1534
1535            item.setAttribute("attached", "true");
1536        }
1537    }
1538
1539    let appearers = document.querySelectorAll('[data-visibility]');
1540    for (let item of appearers) {
1541        if (observer && !item.hasAttribute("attached")) {
1542            observer.observe(item);
1543
1544            item.setAttribute("attached", "true");
1545        }
1546    }
1547
1548    /* CMS Creator Overrides
1549     *
1550     * All block text editing goes through TinyMCE now, so nothing on the canvas is directly
1551     * editable. This strips contenteditable off any block markup that still carries it from
1552     * when it was - left in place it would make the browser treat that element as editable
1553     * with nothing listening, so anything typed there would just be thrown away. The stored
1554     * HTML gets the same treatment on the next save (see StripContentEditable).
1555     *
1556     * TinyMCE's own editable body is inside an iframe, so it isn't reachable from here at all;
1557     * the .tox check is only there so this stays safe if the editor is ever switched to inline
1558     * mode, where the body would be a contenteditable element in this document. */
1559    let editable = document.querySelectorAll('.cms-canvas-container [contenteditable="true"]');
1560    for (let item of editable) {
1561        if (item.closest(".tox") == null) {
1562            item.removeAttribute("contenteditable");
1563        }
1564    }
1565
1566    // track which image a drop would land on while an image is being dragged over the canvas
1567    let canvas = document.querySelector(".cms-canvas-container");
1568    if (canvas != null && !canvas.hasAttribute("data-image-drag-attached")) {
1569        canvas.addEventListener("dragover", trackImageDropTarget);
1570        canvas.setAttribute("data-image-drag-attached", "true");
1571    }
1572
1573    let links = document.querySelectorAll('.cms-canvas-container a');
1574    for (let item of links) {
1575        // make sure links don't do anything when clicked
1576        if (!item.hasAttribute("disabled")) {
1577            item.setAttribute("disabled", "true");
1578        }
1579    }
1580
1581    let buttons = document.querySelectorAll('.cms-canvas-container button');
1582    for (let item of buttons) {
1583        // make sure buttons don't do anything when clicked
1584        if (!item.hasAttribute("disabled")) {
1585            item.setAttribute("disabled", "true");
1586        }
1587    }
1588
1589    let inputs = document.querySelectorAll('.cms-canvas-container input');
1590    for (let item of inputs) {
1591        // make sure input fields don't do anything
1592        if (!item.hasAttribute("disabled")) {
1593            item.setAttribute("disabled", "true");
1594        }
1595    }
1596}
1597
1598
1599
1600async function startCamera(elementID, deviceId) {
1601    let videoElement = document.getElementById(elementID);
1602    if (videoElement == null) {
1603        return false;
1604    }
1605
1606    let devices = await navigator.mediaDevices.enumerateDevices();
1607    let videoDevices = devices.filter(d => d.kind === "videoinput" && d.deviceId === deviceId);
1608
1609    if (videoDevices.length === 1) {
1610        let constraints = {
1611            video: {
1612                deviceId: {
1613                    exact: deviceId
1614                }
1615            }
1616        }
1617
1618        let stream = await navigator.mediaDevices.getUserMedia(constraints);
1619        videoElement.srcObject = stream;
1620        //videoElement.play();
1621
1622        return true;
1623    }
1624
1625    return false;
1626}
1627
1628async function stopCamera(elementID) {
1629    let videoElement = document.getElementById(elementID);
1630    if (videoElement == null) {
1631        return false;
1632    }
1633
1634    let stream = videoElement.srcObject;
1635    if (stream != null) {
1636        let tracks = stream.getTracks();
1637
1638        tracks.forEach(function (track) {
1639            track.stop();
1640        });
1641
1642        videoElement.srcObject = null;
1643    }
1644
1645    return true;
1646}
1647
1648async function getCameras() {
1649    let devices = await navigator.mediaDevices.enumerateDevices();
1650    let videoDevices = devices.filter(d => d.kind === "videoinput");
1651
1652    return videoDevices;
1653}
1654
1655async function hasCameraPermission() {
1656    try {
1657        let stream = await navigator.mediaDevices.getUserMedia({
1658            video: true
1659        });
1660
1661        let tracks = stream.getTracks();
1662
1663        tracks.forEach(function (track) {
1664            track.stop();
1665        });
1666
1667        return true;
1668    }
1669    catch (error) {
1670        return false;
1671    }
1672}
1673
1674
1675function grabFrame(elementID, dotNetObj, external) {
1676    let videoElement = document.getElementById(elementID);
1677    if (videoElement == null) {
1678        return false;
1679    }
1680
1681    let stream = videoElement.srcObject;
1682    let settings = stream.getVideoTracks()[0].getSettings();
1683
1684    let fullResC = document.createElement("canvas");
1685    fullResC.width = settings.width;
1686    fullResC.height = settings.height;
1687    let fullResCtx = fullResC.getContext("2d");
1688    fullResCtx.drawImage(videoElement, 0, 0, settings.width, settings.height);
1689
1690    fullResC.toBlob(async (response) => {
1691        var arrayBuffer = await response.arrayBuffer();
1692        var binary = "";
1693        var bytes = new Uint8Array(arrayBuffer);
1694        var len = bytes.byteLength;
1695        for (var i = 0; i < len; i++) {
1696            binary += String.fromCharCode(bytes[i]);
1697        }
1698
1699        var base64 = window.btoa(binary);
1700        dotNetObj.invokeMethodAsync('CallbackToBlob', base64, external);
1701    });
1702
1703    return true;
1704}
1705
1706
1707// Draws an SVG (base64) onto a canvas and downloads it as a PNG. The SVG is vector, so it is drawn
1708// at the size asked for rather than scaled up from a small bitmap, and the edges stay sharp.
1709function downloadSvgAsPng(svgBase64, fileName, size) {
1710    return new Promise((resolve, reject) => {
1711        const image = new Image();
1712        image.onload = () => {
1713            const canvas = document.createElement('canvas');
1714            canvas.width = size;
1715            canvas.height = size;
1716            canvas.getContext('2d').drawImage(image, 0, 0, size, size);
1717            canvas.toBlob(blob => {
1718                if (!blob) {
1719                    reject('The PNG could not be made.');
1720                    return;
1721                }
1722
1723                const url = URL.createObjectURL(blob);
1724                downloadFromUrl(url, fileName);
1725                // let the download start before the url goes away
1726                setTimeout(() => URL.revokeObjectURL(url), 1000);
1727                resolve();
1728            }, 'image/png');
1729        };
1730        image.onerror = () => reject('The QR code could not be drawn.');
1731        image.src = `data:image/svg+xml;base64,${svgBase64}`;
1732    });
1733}

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.