1/** 2 * ShareEmbedModal Web Component 3 * 4 * A reusable modal component for sharing links and generating embed codes. 5 * Combines share functionality (social media, email, SMS) with embed code generation. 6 * 7 * Attributes: 8 * - type: Determines embed configuration. Accepts 'vendor-list', 'vendor-application', 'volunteer-list', 9 * 'volunteer-application', 'open-application', 'sponsor-application', or 'sponsor-logos' 10 * - event-id: The event ID for generating embed codes 11 * - link: The URL to share (can also be set via setLink method) 12 * - event-link: The event link slug for constructing URLs 13 * - embed-locked: When present, the embed tab is gated for the current subscription tier. Clicking it keeps the 14 * share tab active and dispatches a bubbling, composed 'embed-locked' event for the host page to handle (e.g. upgrade flow). 15 * 16 * Features: 17 * - Share links via Facebook, Twitter, LinkedIn, Email, SMS, WhatsApp, Messenger 18 * - Copy link to clipboard 19 * - Generate customizable embed codes 20 * - Different embed configurations based on type 21 * 22 * Usage Example: 23 * <share-embed-modal 24 * id="share-embed-modal" 25 * type="vendor-list" 26 * event-id="123" 27 * event-link="my-event"> 28 * </share-embed-modal> 29 * 30 * Methods: 31 * - open(): Opens the modal 32 * - close(): Closes the modal 33 * - setLink(url): Sets the share link URL 34 * - setApplications(applications): Sets applications for vendor-application type 35 * - setCategories(categories): Sets categories for vendor-list type 36 * 37 */ 38import { WebComponent } from '../../../js/Components/Base/WebComponent.js'; 39 40class ShareEmbedModal extends WebComponent { 41 constructor() { 42 super(); 43 this.loadBaseCSS(); 44 this.loadJQuery(); 45 this.loadIcons(); 46 this.shadowRoot.appendChild(this.placeHolderRoot); 47 48 this.facebookAppId = '1562058430844259'; 49 this.applications = []; 50 this.categories = []; 51 this.peopleListColors = { text: '#222222', background: '#FFFFFF', link: '#038580' }; 52 53 // Apply styles 54 const style = document.createElement('style'); 55 style.textContent = ` 56 :host { 57 display: none; 58 } 59 60 :host(.show) { 61 display: block; 62 } 63 64 .tab-container { 65 display: flex; 66 border-bottom: 1px solid #C4CAC9; 67 margin-bottom: 24px; 68 position: relative; 69 justify-content: flex-start; 70 z-index: 1; 71 } 72 73 .tab { 74 padding: 8px 16px; 75 text-align: center; 76 cursor: pointer; 77 font-family: 'Plus Jakarta Sans', sans-serif; 78 background: transparent; 79 border: none; 80 border-bottom: 2px solid transparent; 81 font-size: 16px; 82 font-weight: 500; 83 color: #222222; 84 transition: all 0.2s; 85 pointer-events: auto; 86 position: relative; 87 margin-bottom: -1px; 88 } 89 90 .tab:hover { 91 color: #038580; 92 } 93 94 .tab.active { 95 font-weight: 600; 96 color: #038580; 97 border-bottom-color: #038580; 98 } 99 100 .tab-content { 101 display: none; 102 } 103 104 .tab-content.active { 105 display: block; 106 } 107 108 /* Share Section Styles */ 109 .share-section { 110 margin-bottom: 16px; 111 } 112 113 .share-title { 114 font-family: 'Plus Jakarta Sans', sans-serif; 115 font-size: 14px; 116 font-weight: 400; 117 margin-bottom: 4px; 118 color: #222222; 119 } 120 121 .input-group { 122 display: flex; 123 gap: 8px; 124 margin-bottom: 16px; 125 } 126 127 .link-input { 128 flex: 1; 129 padding: 8px 4px; 130 border: 1px solid #9DA6A5; 131 border-radius: 4px; 132 font-size: 16px; 133 font-family: 'Plus Jakarta Sans', sans-serif; 134 overflow-y: auto; 135 gap: 8px; 136 height: 40px; 137 max-height: 120px; 138 line-height: 1.5; 139 } 140 141 .copy-btn { 142 padding: 12px 24px; 143 background: #0389A0; 144 color: white; 145 border: none; 146 border-radius: 8px; 147 cursor: pointer; 148 font-size: 14px; 149 font-weight: 500; 150 white-space: nowrap; 151 transition: background 0.2s; 152 } 153 154 .copy-btn:hover { 155 background: #027183; 156 } 157 158 .share-options { 159 padding-top: 16px; 160 } 161 162 .social-share { 163 transition: opacity 0.2s; 164 } 165 166 .social-share:hover { 167 opacity: 0.7; 168 } 169 170 /* Embed Section Styles */ 171 .embed-section { 172 margin-bottom: 16px; 173 } 174 175 .embed-title { 176 font-size: 18px; 177 font-weight: 600; 178 margin-bottom: 8px; 179 color: #484848; 180 } 181 182 .form-group { 183 margin-bottom: 20px; 184 } 185 186 .form-label { 187 display: block; 188 font-family: 'Plus Jakarta Sans', sans-serif; 189 font-size: 14px; 190 font-weight: 400; 191 line-height: 18px; 192 color: #222222; 193 margin-bottom: 4px; 194 } 195 196 .embed-code { 197 width: 100%; 198 height: 120px; 199 padding: 8px; 200 border: 1px solid #9DA6A5; 201 border-radius: 4px; 202 font-size: 16px; 203 font-family: 'Plus Jakarta Sans', sans-serif; 204 font-weight: 400; 205 line-height: 24px; 206 box-sizing: border-box; 207 white-space: pre-wrap; 208 word-break: break-all; 209 } 210 211 .multi-select-container { 212 margin-bottom: 16px; 213 } 214 215 .embed-actions { 216 display: flex; 217 justify-content: flex-end; 218 margin-top: 16px; 219 } 220 221 .help-text { 222 font-size: 13px; 223 color: #666; 224 margin-top: 4px; 225 } 226 227 .footer-btn-hidden { 228 display: none; 229 } 230 231 /* People list embed customization */ 232 .sem-embed-columns { 233 display: flex; 234 gap: 16px; 235 align-items: flex-start; 236 } 237 238 .sem-color-fields-col { 239 flex: 1; 240 min-width: 0; 241 } 242 243 .sem-color-picker-dock { 244 flex: 1; 245 min-width: 0; 246 } 247 248 /* spectrum.js floats its picker as an absolutely-positioned popup by 249 default; docking it here renders it inline within this column instead. */ 250 .sem-color-picker-dock .sp-container { 251 position: relative !important; 252 top: auto !important; 253 left: auto !important; 254 } 255 256 /* Make the palette row a flex row so swatches grow/shrink to fit the 257 available width (between 12px and 16px) instead of wrapping. */ 258 .sem-color-picker-dock .sp-palette-row { 259 display: flex !important; 260 } 261 262 .sem-color-picker-dock .sp-palette .sp-thumb-el { 263 float: none !important; 264 flex: 1 1 0 !important; 265 min-width: 12px !important; 266 max-width: 16px !important; 267 aspect-ratio: 1 / 1 !important; 268 height: auto !important; 269 margin: 1px !important; 270 } 271 272 /* Below 850px there's no room for a dedicated picker column: the fields
273 stack into their own rows and the color picker falls back to spectrum's 274 normal floating popout instead of the docked layout. */ 275 @media (max-width: 850px) { 276 .sem-color-row-pair { 277 flex-direction: column; 278 } 279 280 .sem-color-picker-dock { 281 display: none; 282 } 283 } 284 285 .sem-color-row { 286 margin-bottom: 16px; 287 } 288 289 .sem-color-row-pair { 290 display: flex; 291 gap: 8px; 292 margin-bottom: 16px; 293 } 294 295 .sem-color-row-pair .sem-color-row { 296 flex: 1; 297 margin-bottom: 0; 298 } 299 300 .preview-circle { 301 cursor: pointer; 302 } 303 304 .sem-preview-link { 305 display: inline-flex; 306 align-items: center; 307 gap: 4px; 308 color: #038580; 309 text-decoration: none; 310 font-size: 14px; 311 font-weight: 500; 312 margin-top: 8px; 313 } 314 315 .sem-preview-link:hover { 316 text-decoration: underline; 317 } 318 `; 319 this.shadowRoot.appendChild(style); 320 321 // Create modal content 322 this.modalContainer = document.createElement('div'); 323 this.modalContainer.innerHTML = ` 324 <modal-element id="modal" size="large"> 325 <modal-header slot="modal-header" id="modal-title">Share & Embed</modal-header> 326 <modal-description slot="modal-description" id="modal-description">Share link on social media or copy link.</modal-description> 327 <modal-body slot="modal-body"> 328 <div class="tab-container"> 329 <button class="tab active" data-tab="share"> 330 Share 331 </button> 332 <button class="tab" data-tab="embed"> 333 Embed 334 </button> 335 </div> 336 337 <!-- Share Tab --> 338 <div class="tab-content active" data-content="share"> 339 <div class="flex flex-wrap justify-center my3" style="gap: 48px;"> 340 <div class="social-share pointer flex flex-wrap justify-center" style="width: 72px;" data-platform="whatsapp"> 341 <img src="/images/social-icons/whatsapp.png" alt="WhatsApp" class="col-12" style="height: 48px; width: 48px;"> 342 <div class="col-12 text-center mt1 size-12 text-gray-7E"> 343 WhatsApp 344 </div> 345 </div> 346 <div class="social-share pointer flex flex-wrap justify-center" style="width: 72px;" data-platform="messenger"> 347 <img src="/images/social-icons/messenger.png" alt="Messenger" class="col-12" style="height: 48px; width: 48px;"> 348 <div class="col-12 text-center mt1 size-12 text-gray-7E"> 349 Messenger 350 </div> 351 </div> 352 <div class="social-share pointer flex flex-wrap justify-center" style="width: 72px;" data-platform="facebook"> 353 <img src="/images/social-icons/facebook.png" alt="Facebook" class="col-12" style="height: 48px; width: 48px;"> 354 <div class="col-12 text-center mt1 size-12 text-gray-7E"> 355 Facebook 356 </div> 357 </div> 358 <div class="social-share pointer flex flex-wrap justify-center" style="width: 72px;" data-platform="x"> 359 <img src="/images/social-icons/x.png" alt="X" class="col-12" style="height: 48px; width: 48px;"> 360 <div class="col-12 text-center mt1 size-12 text-gray-7E"> 361 X 362 </div> 363 </div> 364 </div> 365 366 <div class="share-section"> 367 <div class="share-title">Link</div> 368 <div class="input-group"> 369 <textarea class="link-input" id="share-link-input" readonly></textarea> 370 </div> 371 </div> 372 373 </div> 374 375 <!-- Embed Tab --> 376 <div class="tab-content" data-content="embed"> 377 <div class="embed-section"> 378 379 <div class="form-group"> 380 <label class="form-label">Embed Code</label> 381 <textarea class="embed-code" id="embed-code-textarea" readonly></textarea> 382 </div> 383 384 <div id="embed-options"></div> 385 </div> 386 </div> 387 </modal-body> 388 <modal-footer slot="modal-footer"> 389 <button-element variant="primary" id="copy-link-btn"><i class="uil uil-copy" style="margin-right: 4px;"></i>Copy Link</button-element> 390 <button-element variant="primary" id="copy-embed-btn" class="footer-btn-hidden"> 391 <i class="uil uil-copy" style="margin-right: 4px;"></i> 392 Copy Embed Code 393 </button-element> 394 </modal-footer> 395 </modal-element> 396 `; 397 this.shadowRoot.appendChild(this.modalContainer); 398 } 399 400 static get observedAttributes() { 401 return ['type', 'event-id', 'link', 'event-link', 'base-url']; 402 } 403 404 // Per-type overrides for modal copy. Types not listed here keep the generic defaults. 405 static get typeContent() { 406 return { 407 'pm-people-list': { 408 title: 'Share & embed people list', 409 embedDescription: 'You can share and embed your people list here. It will display as confirmed guest list only.', 410 }, 411 }; 412 } 413 414 render() { 415 // DOM is built in constructor, no re-render needed 416 } 417 418 connectedCallback() { 419 this.setupEventListeners(); 420 } 421 422 attributeChangedCallback(name, oldValue, newValue) { 423 if (oldValue !== newValue) { 424 // Update share link input if link attribute changes 425 if (name === 'link') { 426 const linkInput = this.shadowRoot.querySelector('#share-link-input'); 427 if (linkInput) { 428 linkInput.value = newValue || ''; 429 } 430 } 431 } 432 } 433 434 get type() {
435 return this.getAttribute('type') || 'vendor-list'; 436 } 437 438 get eventId() { 439 return this.getAttribute('event-id') || ''; 440 } 441 442 get link() { 443 return this.getAttribute('link') || ''; 444 } 445 446 get eventLink() { 447 return this.getAttribute('event-link') || ''; 448 } 449 450 // Mirrors the ENV_TYPE-driven $baseUrl in tickets-ticket-2018-06-27.js so embed 451 // links resolve on local/staging instead of always pointing at production. 452 get baseUrl() { 453 return this.getAttribute('base-url') || 'https://www.eventeny.com'; 454 } 455 456 setLink(url) { 457 this.setAttribute('link', url); 458 } 459 460 setApplications(applications) { 461 this.applications = applications; 462 this.updateEmbedOptions(); 463 } 464 465 setCategories(categories) { 466 this.categories = categories; 467 this.updateEmbedOptions(); 468 } 469 470 setupEventListeners() { 471 const tabs = this.shadowRoot.querySelectorAll('.tab'); 472 const tabContents = this.shadowRoot.querySelectorAll('.tab-content'); 473 const copyLinkBtn = this.shadowRoot.querySelector('#copy-link-btn'); 474 const copyEmbedBtn = this.shadowRoot.querySelector('#copy-embed-btn'); 475 const shareButtons = this.shadowRoot.querySelectorAll('.social-share'); 476 477 // Tab switching
478 tabs.forEach(tab => { 479 tab.addEventListener('click', (e) => { 480 e.preventDefault(); 481 e.stopPropagation(); 482 const targetTab = tab.dataset.tab; 483 484 // Embed gated for this tier: keep on share tab and let the host page handle the upgrade flow 485 if (targetTab === 'embed' && this.hasAttribute('embed-locked')) { 486 this.dispatchEvent(new CustomEvent('embed-locked', { bubbles: true, composed: true })); 487 return; 488 } 489 490 // Update active tab 491 tabs.forEach(t => t.classList.remove('active')); 492 tab.classList.add('active'); 493 494 // Show corresponding content 495 tabContents.forEach(content => { 496 if (content.dataset.content === targetTab) { 497 content.classList.add('active'); 498 } else { 499 content.classList.remove('active'); 500 } 501 }); 502 503 // Update modal description based on active tab 504 const modalDescription = this.shadowRoot.querySelector('#modal-description'); 505 if (modalDescription) { 506 if (targetTab === 'share') { 507 modalDescription.textContent = 'Share link on social media or copy link.'; 508 } else if (targetTab === 'embed') { 509 const typeConfig = ShareEmbedModal.typeContent[this.type]; 510 modalDescription.textContent = typeConfig?.embedDescription || 'Customize and share embed code.'; 511 } 512 } 513 514 // Toggle footer buttons based on active tab 515 copyLinkBtn.classList.toggle('footer-btn-hidden', targetTab !== 'share'); 516 copyEmbedBtn.classList.toggle('footer-btn-hidden', targetTab !== 'embed'); 517 518 // Setup embed options when switching to embed tab 519 if (targetTab === 'embed') { 520 // Check if embed options need to be initialized 521 const embedOptions = this.shadowRoot.querySelector('#embed-options'); 522 if (embedOptions && embedOptions.children.length === 0) { 523 this.updateEmbedOptions(); 524 } else { 525 this.generateEmbedCode(); 526 } 527 } 528 }); 529 }); 530 531 // Copy link button 532 copyLinkBtn.addEventListener('click', () => { 533 const linkInput = this.shadowRoot.querySelector('#share-link-input'); 534 this.copyToClipboard(linkInput.value); 535 copyLinkBtn.innerHTML = 'Link copied!'; 536 setTimeout(() => { 537 copyLinkBtn.innerHTML = '<i class="uil uil-copy" style="margin-right: 4px;"></i>Copy Link'; 538 }, 3000); 539 }); 540 541 // Copy embed button 542 copyEmbedBtn.addEventListener('click', () => { 543 const embedCode = this.shadowRoot.querySelector('#embed-code-textarea'); 544 this.copyToClipboard(embedCode.value); 545 copyEmbedBtn.innerHTML = 'Link copied!'; 546 setTimeout(() => { 547 copyEmbedBtn.innerHTML = '<i class="uil uil-copy" style="margin-right: 4px;"></i>Copy Embed Code'; 548 }, 3000); 549 }); 550 551 // Share buttons 552 shareButtons.forEach(button => { 553 button.addEventListener('click', () => { 554 const platform = button.dataset.platform; 555 this.shareLink(platform); 556 }); 557 }); 558 } 559 560 updateEmbedOptions() { 561 const embedOptions = this.shadowRoot.querySelector('#embed-options'); 562 563 if (this.type === 'vendor-application' || this.type === 'volunteer-application' || this.type === 'open-application' || this.type === 'sponsor-application') { 564 if (this.applications.length > 0) { 565 embedOptions.innerHTML = ` 566 <div class="form-group"> 567 <label class="form-label">Application</label> 568 <div class="multi-select-container"> 569 <multi-select 570 id="applications-select" 571 label="Select application(s)" 572 search 573 collapsible 574 counter 575 tag-variant> 576 </multi-select> 577 </div> 578 </div> 579 `; 580 581 // Wait for multi-select to be defined and rendered 582 requestAnimationFrame(() => { 583 const multiSelect = embedOptions.querySelector('#applications-select'); 584 if (multiSelect) { 585 586 // Convert applications array to MultiSelect format 587 // Format: { id: [label, pillLabel, tags] } 588 const items = {}; 589 this.applications.forEach(app => { 590 items[app.id] = [app.title, '', {}]; 591 }); 592 593 multiSelect.addItems(items); 594 595 // Listen to selection changes 596 multiSelect.addEventListener('selectionchange', () => { 597 this.generateEmbedCode(); 598 }); 599 600 // Generate initial embed code 601 this.generateEmbedCode(); 602 } 603 }); 604 } 605 }
605 else if (this.type === 'vendor-list') { 606 let optionsHTML = ''; 607 608 if (this.applications.length > 0) { 609 optionsHTML += ` 610 <div class="form-group"> 611 <label class="form-label">Application</label> 612 <div class="multi-select-container"> 613 <multi-select 614 id="applications-select" 615 label="Select application(s)" 616 search 617 collapsible 618 counter 619 tag-variant> 620 </multi-select> 621 </div> 622 </div> 623 `; 624 } 625 626 if (this.categories.length > 0) { 627 optionsHTML += ` 628 <div class="form-group"> 629 <label class="form-label">Category</label> 630 <div class="multi-select-container"> 631 <multi-select 632 id="categories-select" 633 label="Select category/categories" 634 search 635 collapsible 636 counter 637 tag-variant> 638 </multi-select> 639 </div> 640 </div> 641 `; 642 } 643 644 optionsHTML += ` 645 <div class="form-group"> 646 <label class="form-label">Duration</label> 647 <dropdown-element id="embed-duration" data-embed-option full-width></dropdown-element> 648 </div> 649 `; 650 651 embedOptions.innerHTML = optionsHTML; 652 653 requestAnimationFrame(() => { 654 if (this.applications.length > 0) { 655 const applicationsSelect = embedOptions.querySelector('#applications-select'); 656 if (applicationsSelect) { 657 const items = {}; 658 this.applications.forEach(app => { 659 items[app.id] = [app.title, '', {}]; 660 }); 661 662 applicationsSelect.addItems(items); 663 applicationsSelect.addEventListener('selectionchange', () => { 664 this.generateEmbedCode(); 665 }); 666 } 667 } 668 669 if (this.categories.length > 0) { 670 const categoriesSelect = embedOptions.querySelector('#categories-select'); 671 if (categoriesSelect) { 672 const items = {}; 673 this.categories.forEach((cat, index) => { 674 items[index] = [cat.answer, '', {}]; 675 }); 676 categoriesSelect.addItems(items); 677 categoriesSelect.addEventListener('selectionchange', () => { 678 this.generateEmbedCode(); 679 }); 680 } 681 } 682 }); 683 684 // Wait for dropdown-element to be defined and populate it 685 customElements.whenDefined('dropdown-element').then(() => { 686 const durationDropdown = embedOptions.querySelector('#embed-duration'); 687 if (durationDropdown && typeof durationDropdown.setItems === 'function') { 688 // Set the dropdown items 689 durationDropdown.setItems({ 690 'time': 'Available during event time', 691 'preview': 'Available at all times' 692 }); 693 694 // Set default value 695 requestAnimationFrame(() => { 696 if (typeof durationDropdown.setValue === 'function') { 697 durationDropdown.setValue('time'); 698 } 699 }); 700 701 // Add change listener 702 durationDropdown.addEventListener('change', () => this.generateEmbedCode()); 703 } 704 }); 705 706 707 this.generateEmbedCode(); 708 } else if (this.type === 'pm-people-list') { 709 embedOptions.innerHTML = ` 710 <div class="sem-embed-columns"> 711 <div class="sem-color-fields-col"> 712 <div class="sem-color-row-pair"> 713 <div class="sem-color-row"> 714 <label class="form-label">Text color</label> 715 <div class="color-display color-display-rectangle"> 716 <div class="preview-circle preview-rectangle color-code-field" id="pm-swatch-text"></div> 717 <input type="text" class="hex-input hex-input-embed" id="pm-hex-text" value="${this.peopleListColors.text}"> 718 </div> 719 </div> 720 <div class="sem-color-row"> 721 <label class="form-label">Background color</label> 722 <div class="color-display color-display-rectangle"> 723 <div class="preview-circle preview-rectangle color-code-field" id="pm-swatch-background"></div> 724 <input type="text" class="hex-input hex-input-embed" id="pm-hex-background" value="${this.peopleListColors.background}"> 725 </div> 726 </div> 727 </div> 728 <div class="sem-color-row"> 729 <label class="form-label">Guest name link color</label> 730 <div class="color-display color-display-rectangle"> 731 <div class="preview-circle preview-rectangle color-code-field" id="pm-swatch-link"></div> 732 <input type="text" class="hex-input hex-input-embed" id="pm-hex-link" value="${this.peopleListColors.link}"> 733 </div> 734 </div> 735 <a href="" target="_blank" id="pm-preview-embed" class="sem-preview-link"><i class="uil uil-eye"></i> Preview embed content</a> 736 </div> 737 <div class="sem-color-picker-dock" id="pm-color-picker-dock"></div>
738 </div> 739 `; 740 this.setupPeopleListColorPickers(); 741 this.generateEmbedCode(); 742 } else if (this.type === 'sponsor-logos' || this.type === 'volunteer-list') { 743 embedOptions.innerHTML = ''; 744 this.generateEmbedCode(); 745 } 746 } 747 748 // Copies the shared spectrum/color-field stylesheets into this shadow root. 749 // Pages using type="pm-people-list" must print loadSpectrum() 750 // (funcs/base-loader-frontend-file-loader.php) so spectrum.js, jQuery, and 751 // window.SpectrumColorField are already loaded page-wide -- but shadow DOM 752 // encapsulation still blocks that page-level <link> from styling this shadow 753 // root, so a local copy of the CSS is unavoidable regardless of loading strategy. 754 injectSharedColorFieldStyles() { 755 if (!this.shadowRoot.querySelector('link[href*="/css/spectrum.min.css"]')) { 756 const spectrumLink = document.createElement('link'); 757 spectrumLink.rel = 'stylesheet'; 758 spectrumLink.type = 'text/css'; 759 spectrumLink.href = '/css/spectrum.min.css'; 760 this.shadowRoot.appendChild(spectrumLink); 761 } 762 if (!this.shadowRoot.querySelector('link[href*="/css/common/spectrum-color-field.css"]')) { 763 const fieldLink = document.createElement('link'); 764 fieldLink.rel = 'stylesheet'; 765 fieldLink.type = 'text/css'; 766 fieldLink.href = '/css/common/spectrum-color-field.css'; 767 this.shadowRoot.appendChild(fieldLink); 768 } 769 } 770 771 // Shared with the tickets embed modal's #color_text_code etc. wiring -- see 772 // js/common/spectrum-color-field.js. 773 setupPeopleListColorPickers() { 774 if (!window.SpectrumColorField) { 775 console.warn('share-embed-modal: window.SpectrumColorField is not loaded. Pages using type="pm-people-list" must print loadSpectrum() after jQuery -- see funcs/base-loader-frontend-file-loader.php.'); 776 return; 777 } 778 779 this.injectSharedColorFieldStyles(); 780 781 const $swatches = window.jQuery(this.shadowRoot.querySelectorAll('.color-code-field')); 782 const dock = this.shadowRoot.querySelector('#pm-color-picker-dock'); 783 // Below 850px the dock column is hidden (see the media query above), so 784 // the picker falls back to spectrum's normal floating popout on body. 785 const isNarrowScreen = window.matchMedia('(max-width: 850px)').matches; 786 787 window.SpectrumColorField.initColorFields($swatches, { 788 spectrumOptions: { 789 appendTo: isNarrowScreen ? 'body' : dock, 790 replacerClassName: 'sem-color-replacer', 791 }, 792 onChange: () => this.generateEmbedCode(), 793 }); 794 } 795 796 generateEmbedCode() { 797 const embedWidth = 1200; 798 const embedHeight = 800; 799 let embedSrc = ''; 800 if(this.type === 'volunteer-application') 801 embedSrc = `${this.baseUrl}/events/content/embed/volunteers/?id=${this.eventId}&app=`; 802 else 803 embedSrc = `${this.baseUrl}/events/embed/?ev=${this.eventId}`; 804 805 if (this.type === 'vendor-application' || this.type === 'volunteer-application' || this.type === 'open-application' || this.type === 'sponsor-application') { 806 // volunteer-application omitted on purpose: its base URL already ends with '&app=', so the IDs append there 807 if (this.type === 'vendor-application') embedSrc += '&type=application&app_type=vendor'; 808 else if (this.type === 'open-application') embedSrc += '&type=application&app_type=open'; 809 else if (this.type === 'sponsor-application') embedSrc += '&type=application&app_type=sponsor'; 810 811 // Get selected applications from multi-select 812 const multiSelect = this.shadowRoot.querySelector('#applications-select'); 813 if (multiSelect) { 814 const selectedApps = multiSelect.getFilteredSelected(); 815 const totalApps = multiSelect.getFilteredItemsNumber(); 816 817 if (selectedApps.length > 0 && selectedApps.length < totalApps) { 818 embedSrc += ((this.type === 'vendor-application' || this.type === 'open-application' || this.type === 'sponsor-application') ? '&app_filter=' : '') + selectedApps.join('|'); 819 } 820 } 821 } else if (this.type === 'vendor-list') { 822 embedSrc += '&type=vendor'; 823 824 // Get duration 825 const durationDropdown = this.shadowRoot.querySelector('#embed-duration'); 826 if (durationDropdown && typeof durationDropdown.getSelected === 'function') { 827 const selectedValue = durationDropdown.getSelected(); 828 if (selectedValue === 'preview') { 829 embedSrc += '&preview=' + this.eventId; 830 } 831 } 832 833 // Get selected applications from multi-select
834 const applicationsSelect = this.shadowRoot.querySelector('#applications-select'); 835 if (applicationsSelect) { 836 const selectedApps = applicationsSelect.getFilteredSelected(); 837 const totalApps = applicationsSelect.getFilteredItemsNumber(); 838 839 if (selectedApps.length > 0 && selectedApps.length < totalApps) { 840 embedSrc += '&app_types=' + selectedApps.join('|'); 841 } 842 } 843 844 // Get selected categories from multi-select 845 const multiSelect = this.shadowRoot.querySelector('#categories-select'); 846 if (multiSelect) { 847 const selectedIndices = multiSelect.getFilteredSelected(); 848 const totalCategories = multiSelect.getFilteredItemsNumber(); 849 850 if (selectedIndices.length > 0 && selectedIndices.length < totalCategories) { 851 // Convert indices back to category names 852 const selectedCategoryNames = selectedIndices.map(index => { 853 const category = this.categories[parseInt(index)]; 854 return category ? category.answer : null; 855 }).filter(name => name !== null); 856 857 embedSrc += '&category=' + selectedCategoryNames.join('|'); 858 } 859 } 860 } else if (this.type === 'pm-people-list') { 861 const textColor = this.shadowRoot.querySelector('#pm-hex-text')?.value || this.peopleListColors.text; 862 const backgroundColor = this.shadowRoot.querySelector('#pm-hex-background')?.value || this.peopleListColors.background; 863 const linkColor = this.shadowRoot.querySelector('#pm-hex-link')?.value || this.peopleListColors.link; 864 865 embedSrc += '&type=guest-list'; 866 embedSrc += '&color=' + encodeURIComponent(textColor); 867 embedSrc += '&background=' + encodeURIComponent(backgroundColor); 868 embedSrc += '&link=' + encodeURIComponent(linkColor); 869 } else if (this.type === 'sponsor-logos') { 870 embedSrc += '&type=sponsor-logos'; 871 } else if (this.type === 'volunteer-list') { 872 embedSrc = `https://www.eventeny.com/events/content/embed/volunteers/?id=${this.eventId}`; 873 } 874 875 const embedCode = (this.type === 'pm-people-list') 876 ? `<iframe width="100%" src="${embedSrc}" frameborder="0" style="height:100vh; width:100%; margin-top:0px; max-height: 100vh; max-width: 100%;"></iframe>` 877 : `<iframe width="${embedWidth}" height="${embedHeight}" src="${embedSrc}" frameborder="0"></iframe>`; 878 const embedTextarea = this.shadowRoot.querySelector('#embed-code-textarea'); 879 if (embedTextarea) { 880 embedTextarea.value = embedCode; 881 } 882 883 const previewLink = this.shadowRoot.querySelector('#pm-preview-embed'); 884 if (previewLink) { 885 // Mirrors $("#preview_page_embed").attr("href", embedSrc) in 886 // tickets-ticket-2018-06-27.js. Falls back to setAttribute if the page 887 // forgot to print loadSpectrum() (see setupPeopleListColorPickers). 888 if (window.jQuery) { 889 window.jQuery(previewLink).attr('href', embedSrc); 890 } else { 891 previewLink.setAttribute('href', embedSrc); 892 } 893 } 894 } 895 896 shareLink(platform) { 897 const link = this.link; 898 const encodedLink = encodeURIComponent(link); 899 900 const sharingLinks = { 901 facebook: `https://www.facebook.com/dialog/share?app_id=${this.facebookAppId}&display=popup&href=${encodedLink}`, 902 x: `https://twitter.com/intent/tweet?via=eventenyteam&url=${encodedLink}`, 903 linkedin: `https://www.linkedin.com/shareArticle?mini=true&url=${encodedLink}`, 904 email: `mailto:?body=${encodedLink}`, 905 sms: `sms:;?&body=${encodedLink}`, 906 messenger: `https://www.facebook.com/dialog/send?app_id=${this.facebookAppId}&link=${encodedLink}&redirect_uri=${encodedLink}`, 907 whatsapp: `https://web.whatsapp.com/send?text=${encodedLink}`, 908 }; 909 910 if (sharingLinks[platform]) { 911 window.open(sharingLinks[platform]); 912 } 913 } 914 915 copyToClipboard(text) { 916 if (navigator.clipboard && navigator.clipboard.writeText) { 917 navigator.clipboard.writeText(text); 918 } else { 919 // Fallback for older browsers 920 const temp = document.createElement('textarea'); 921 temp.value = text; 922 document.body.appendChild(temp); 923 temp.select(); 924 document.execCommand('copy'); 925 document.body.removeChild(temp); 926 } 927 } 928 929 open() { 930 const modal = this.shadowRoot.querySelector('#modal'); 931 const linkInput = this.shadowRoot.querySelector('#share-link-input');
932 const modalTitle = this.shadowRoot.querySelector('#modal-title'); 933 934 // Set the share link 935 linkInput.value = this.link; 936 937 // Apply type-specific title, if any 938 const typeConfig = ShareEmbedModal.typeContent[this.type]; 939 if (modalTitle) { 940 modalTitle.textContent = typeConfig?.title || 'Share & Embed'; 941 } 942 943 // Reset to share tab 944 const tabs = this.shadowRoot.querySelectorAll('.tab'); 945 const tabContents = this.shadowRoot.querySelectorAll('.tab-content'); 946 const modalDescription = this.shadowRoot.querySelector('#modal-description'); 947 const copyLinkBtn = this.shadowRoot.querySelector('#copy-link-btn'); 948 const copyEmbedBtn = this.shadowRoot.querySelector('#copy-embed-btn'); 949 950 tabs.forEach(t => t.classList.remove('active')); 951 tabs[0].classList.add('active'); 952 tabContents.forEach(content => content.classList.remove('active')); 953 tabContents[0].classList.add('active'); 954 955 // Reset footer buttons to match share tab 956 copyLinkBtn.classList.remove('footer-btn-hidden'); 957 copyEmbedBtn.classList.add('footer-btn-hidden'); 958 959 // Reset modal description to Share tab description 960 if (modalDescription) { 961 modalDescription.textContent = 'Share link on social media or copy link.'; 962 } 963 964 // Embed-only mode (Deals sponsor-logos): hide the Share tab, show only the Embed tab 965 const tabContainer = this.shadowRoot.querySelector('.tab-container'); 966 if (this.type === 'sponsor-logos') { 967 if (tabContainer) tabContainer.style.display = 'none';
968 tabs.forEach(t => t.classList.remove('active')); 969 tabContents.forEach(content => content.classList.remove('active')); 970 const embedTab = this.shadowRoot.querySelector('.tab[data-tab="embed"]'); 971 const embedContent = this.shadowRoot.querySelector('.tab-content[data-content="embed"]'); 972 if (embedTab) embedTab.classList.add('active'); 973 if (embedContent) embedContent.classList.add('active'); 974 copyLinkBtn.classList.add('footer-btn-hidden'); 975 copyEmbedBtn.classList.remove('footer-btn-hidden'); 976 if (modalTitle) modalTitle.textContent = 'Embed'; 977 if (modalDescription) modalDescription.textContent = 'Customize and share embed code.'; 978 const embedOptions = this.shadowRoot.querySelector('#embed-options'); 979 if (embedOptions && embedOptions.children.length === 0) this.updateEmbedOptions(); 980 else this.generateEmbedCode(); 981 } else { 982 if (tabContainer) tabContainer.style.display = ''; 983 if (modalTitle) modalTitle.textContent = 'Share & Embed'; 984 } 985 986 // Open the modal 987 if (modal && typeof modal.openModal === 'function') { 988 modal.openModal(); 989 } 990 991 this.classList.add('show'); 992 } 993 994 close() { 995 const modal = this.shadowRoot.querySelector('#modal'); 996 if (modal && typeof modal.closeModal === 'function') { 997 modal.closeModal(); 998 } 999 this.classList.remove('show'); 1000 } 1001} 1002 1003if (!customElements.get('share-embed-modal')) { 1004 customElements.define('share-embed-modal', ShareEmbedModal); 1005}
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.