import { LitElement, HTMLTemplateResult, html } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { Constant } from './constant.js'; import { getCaptureEyeStyles } from './capture-eye-styles.js'; import { ModalManager } from './modal/modal-manager.js'; import { CaptureEyeModal, EngagementZone } from './modal/modal.js'; import { MediaViewer } from './media-viewer/media-viewer.js'; import interactionTracker, { TrackerEvent, } from './modal/interaction-tracker.js'; import { isMobile } from './utils.js'; @customElement('capture-eye') export class CaptureEye extends LitElement { static override styles = getCaptureEyeStyles(); /** * Nid of the asset. */ @property({ type: String }) nid = ''; /** * layout name of the asset. Options: original, curated */ @property({ type: String }) layout = Constant.layout.original; /** * Set visibility behavior. Default is mouse hover to show. Options: hover, always */ @property({ type: String }) visibility = Constant.visibility.hover; /** * Set position. Default is top left. Options: top left, top right, bottom left, bottom right */ @property({ type: String }) position = ''; /** * Set color: default is #377dde, and for mobile, the default is #333333. */ @property({ type: String }) color = ''; /** * Set heading source. Default is headline. Options: headline, abstract */ @property({ type: String , attribute: 'heading-source' }) headingSource = ''; /** * Customizable copyright zone title. */ @property({ type: String, attribute: 'cz-title' }) copyrightZoneTitle = ''; /** * Urls of the engagement images. Use comma to separate multiple images. */ @property({ type: String, attribute: 'eng-img' }) engagementImage = ''; /** * Urls of the engagement links. Use comma to separate multiple links. */ @property({ type: String, attribute: 'eng-link' }) engagementLink = ''; /** * Text of the action button. */ @property({ type: String, attribute: 'action-button-text' }) actionButtonText = ''; /** * Url of the action button link. */ @property({ type: String, attribute: 'action-button-link' }) actionButtonLink = ''; /** * Control the CR Pin behavior. Default is on. Options: on, off */ @property({ type: String, attribute: 'cr-pin' }) crPin = Constant.crPin.on; @state({}) protected _isFullVisibility = false; private _resizeObserver: ResizeObserver | null = null; get assetUrl() { return `${Constant.url.ipfsGateway}/${this.nid}`; } get assetProfileUrl() { return `${Constant.url.profile}/${this.nid}`; } constructor() { super(); this.loadFontFace(); console.debug(MediaViewer.name); // The line ensures MediaViewer is included in compilation. console.debug(CaptureEyeModal.name); // The line ensures CaptureEyeModal is included in compilation. } override connectedCallback() { super.connectedCallback(); if (!this._resizeObserver && (this.visibility === Constant.visibility.always || isMobile())) { this._resizeObserver = new ResizeObserver((entries) => { for (const entry of entries) { this.handleResize(entry); } }); // Handle edge cases where the size remains unchanged and no errors occur setTimeout(() => { this.setButtonFullVisibility(); }, 3000); } } override disconnectedCallback() { super.disconnectedCallback(); ModalManager.getInstance().removeModal(); if (this._resizeObserver) { this._resizeObserver.disconnect(); this._resizeObserver = null; } } get isOpened() { return !ModalManager.getInstance().modalHidden; } open() { this.setButtonActive(true); this.openEye(); } close() { ModalManager.getInstance().removeModal(); } private buttonTemplate() { if (!this.nid) { return html``; } const mobile = isMobile(); const color = this.color ? this.color : mobile ? Constant.color.mobileEye : Constant.color.defaultEye; const size = mobile ? 24 : 32; const positions = this.position.split(' '); return html`
${this.generateCaptureEyeSvg(color, size)}
`; } override render() { return html`
${this.buttonTemplate()}
`; } openEye(event?: Event) { if (event) { event.stopPropagation(); event.preventDefault(); } const modalManager = ModalManager.getInstance(); const buttonElement = this.getButtonElement(); const buttonRect = buttonElement.getBoundingClientRect(); const modalOptions = { nid: this.nid, layout: this.layout, color: this.color, headingSource: this.headingSource, copyrightZoneTitle: this.copyrightZoneTitle, engagementZones: this.engagementZones, actionButtonText: this.actionButtonText, actionButtonLink: this.actionButtonLink, crPin: this.crPin, position: { top: buttonRect.top + window.scrollY, left: buttonRect.left + window.scrollX, name: this.position, }, }; modalManager.updateModal(modalOptions); this.setButtonActive(false); if (this.nid) { interactionTracker.trackInteraction( TrackerEvent.CAPTURE_EYE, this.nid, `layout=${this.layout},visibility=${this.visibility}` ); } } private generateCaptureEyeSvg( color: string, size: number ): HTMLTemplateResult { return html` `; } private getButtonElement(): HTMLElement { return this.shadowRoot?.querySelector('.capture-eye-button') as HTMLElement; } private handleMouseEnter() { this.setButtonActive(true); } private handleMouseLeave() { this.setButtonActive(false); } private handleResize(entry: ResizeObserverEntry) { if (entry.contentRect.width > 0 && entry.contentRect.height > 0) { this.setButtonFullVisibility(); } } private handleSlotChange(event: Event) { if (!this._resizeObserver) { return; } const slot = event.target as HTMLSlotElement; if (!slot) { return; } let sizeNotEmpty = false; const nodes = slot.assignedNodes({ flatten: true }).filter((node) => { if (node.nodeType !== Node.ELEMENT_NODE) { return false; } const rect = (node as Element).getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) { sizeNotEmpty = true; } return true; }); if (nodes.length === 0 || sizeNotEmpty) { this.setButtonFullVisibility(); } else { nodes.forEach((node) => { this._resizeObserver?.observe(node as Element); node.addEventListener('error', () => this.setButtonFullVisibility()); }); } } private setButtonActive(active: boolean) { const button = this.getButtonElement(); if (button) { if (active) { button.classList.add('active'); } else { button.classList.remove('active'); } } } private setButtonFullVisibility() { if (this._resizeObserver) { this._isFullVisibility = true; this._resizeObserver.disconnect(); this._resizeObserver = null; } } private loadFontFace() { /* * Inject link stylesheet to DOM directly since it will not work in shadow DOM */ const font = document.createElement('link'); font.href = Constant.url.fontFaceCssUrl; font.rel = 'stylesheet'; document.head.appendChild(font); } private get engagementZones() { const engagementImages = this.engagementImage .split(',') .map((url) => url.trim()) .filter((url) => url !== ''); const engagementLinks = this.engagementLink .split(',') .map((url) => url.trim()) .filter((url) => url !== ''); /* Use image as base. For unexpected use cases links > images, ignore the exceeding links. * For unexpected use cases images > links, use default link. */ return engagementImages.map((image, index) => { return { image, link: engagementLinks[index] || Constant.url.defaultEngagementLink, } as EngagementZone; }); } } declare global { interface HTMLElementTagNameMap { 'capture-eye': CaptureEye; } }