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.