PageSourceSearch

https://www.eventeny.com/js/Components/ShareEmbedModal.js?v=1790189618

js eventeny.com collected 2026-09-24 08:55:26 UTC 32,813 bytes, 1,005 lines download raw bytes

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>&nbsp;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.