Riddle embed webcomponent

The Riddle embed webcomponent is created with stencil. It works with Riddle V1 and V2.

Complete code

import { Component, Element, Host, h, Prop, State } from '@stencil/core';
import { RiddleEvent, RiddleEventV1, RiddleTrackEvent, RiddleTrackNetworks, RiddleTrackNetworksConfig_Custom, RiddleTrackNetworksConfig_Default } from './types/RiddleEvent';
import { RiddleDataLayerItem } from './types/RiddleDataLayer';

@Component({
  tag: 'riddle-embed',
  styleUrl: 'riddle-embed.css',
  shadow: true,
})
export class RiddleEmbed {
  @Prop() riddleid!: string;
  @Prop() autoscroll: boolean = false;
  @Prop() autoscrolloffset: number = 0;

  @Element() el: HTMLElement;

  private _riddleDataLayer: Array<RiddleDataLayerItem> = [];
  private _scrollPosition: number = 0;
  private _iframeElement?: HTMLIFrameElement;

  @State() iframeHeight: number = 0;
  @State() isRiddleV1: boolean = false;
  @State() isRiddleLoaded: boolean = false;

  componentWillLoad() {
    this.isRiddleV1 = /^[0-9]+$/.test(this.riddleid);

    if (this.isRiddleV1) {
      window.addEventListener('message', this.onRiddleV1Event, false);
    } else {
      window.addEventListener('message', this.onRiddleV2Event, false);
    }
  }

  componentDidLoad() {
    this._iframeElement = this.el.shadowRoot.querySelector('#riddle-iframe-' + this.riddleid);

    this._scrollPosition = 0;
    document.addEventListener('scroll', () => {
      if (this._scrollPosition != 0) {
        window.scrollTo(0, this._scrollPosition);
        this._scrollPosition = 0;
      }

      this.sendPositionDataToIframe();
    });
  }

  private validateDataLayerItem = (target: Array<RiddleDataLayerItem>, value: RiddleDataLayerItem): boolean => {
    const validKeyTypes = ['string', 'number'];
    let isValid = true;

    if (typeof value !== 'object') {
      console.log('typeof DataLayerItem:', typeof value);
      console.log('DataLayerItem must be an object like: { key: "key", value: "value" || 0 }');
      isValid = false;
    }

    if (typeof value.key !== 'string') {
      console.log('typeof DataLayerItem.key:', typeof value.key);
      console.log('DataLayerItem must have a "key"');
      isValid = false;
    }

    if (!validKeyTypes.includes(typeof value.value)) {
      console.log('typeof DataLayerItem.value:', typeof value.value);
      console.log('DataLayerItem must have a "value" of type: ' + validKeyTypes.join(' or '));
      isValid = false;
    }

    if (isValid) {
      target.push(value);
    }

    return isValid;
  };

  private initDataLayer() {
    const itemHandler = {
      set: (target: Array<RiddleDataLayerItem>, p: PropertyKey, value: RiddleDataLayerItem) => {
        if (p === 'length') {
          (target as object)[p] = value;
          return true;
        }

        const isValid = this.validateDataLayerItem(target, value);

        if (isValid) {
          this._iframeElement?.contentWindow?.postMessage(
            { dataLayerItem: value, riddleQueryString: window.location.href.split('?')[1], riddleParentLocation: window.location.href },
            '*',
          );
        }

        return isValid;
      },
    };

    this._riddleDataLayer = new Proxy<Array<RiddleDataLayerItem>>([], itemHandler);

    this.readFromUrl();
  }

  private readFromUrl() {
    const queryString = window.location.search;
    const urlSearchParams = new URLSearchParams(queryString);
    const params = Object.fromEntries(urlSearchParams.entries());

    for (const key in params) {
      if (Object.prototype.hasOwnProperty.call(params, key)) {
        const value = params[key];

        this.pushDataLayerItem({
          key: key,
          value: value,
        });
      }
    }
  }

  public pushDataLayerItem(item: RiddleDataLayerItem) {
    if (this._riddleDataLayer) {
      const index = this._riddleDataLayer.findIndex(i => i.key === item.key);

      if (index === -1) {
        this._riddleDataLayer.push(item);
      } else {
        this._riddleDataLayer[index] = item;
      }
    }
  }

  private UpdateHeight(height: number) {
    this.iframeHeight = height;
  }

  private getRiddleEventObject = (data: any) => {
    if (data.isRiddle2Event !== undefined && data.isRiddle2Event === true) {
      return data as RiddleEvent;
    } else {
      return data as RiddleEventV1;
    }
  };

  private pushTrackEvent(riddleEvent: RiddleTrackEvent) {
    if (riddleEvent.trackNetworks) {
      riddleEvent.trackNetworks.forEach(trackNetwork => {
        try {
          if (trackNetwork.networkName === RiddleTrackNetworks.customTracking) {
            const tnef = (trackNetwork as RiddleTrackNetworksConfig_Custom).eventFunction;
            const ef = eval(tnef);
            ef(riddleEvent);
          } else {
            const tn = trackNetwork as RiddleTrackNetworksConfig_Default;

            const data = {
              ...riddleEvent,
            } as any;

            delete data.trackNetworks;

            switch (trackNetwork.networkName) {
              // facebook pixel
              case RiddleTrackNetworks.facebookPixel:
                const fbq = window[tn.networkObjectName];

                fbq('trackCustom', riddleEvent.category, {
                  eventAction: riddleEvent.action,
                  eventName: riddleEvent.name,
                  ...data,
                });

                break;

              // google analytics
              case RiddleTrackNetworks.googleAnalytics:
                const ga = window[tn.networkObjectName];
                ga('send', 'event', [riddleEvent.category], [riddleEvent.action], [riddleEvent.name], ...data);
                break;

              case RiddleTrackNetworks.googleAnalytics4:
                const gtag = window[tn.networkObjectName];
                gtag('event', riddleEvent.category, {
                  event_name: riddleEvent.name,
                  event_action: riddleEvent.action,
                  ...data,
                });
                break;

              // google tag manager
              case RiddleTrackNetworks.googleTagManager:
                const dataLayer = window[tn.networkObjectName];
                dataLayer.push({
                  event: 'RiddleEvent',
                  event_category: riddleEvent.category,
                  event_action: riddleEvent.action,
                  event_name: riddleEvent.name,
                  ...data,
                });
                break;

              // matomo tag
              case RiddleTrackNetworks.matomoTag:
                const _paq = window[tn.networkObjectName];
                _paq.push(['trackEvent', riddleEvent.category, riddleEvent.action, riddleEvent.name, ...data]);
                break;

              default:
                break;
            }
          }
        } catch (error) {
          console.log('[RiddleTrackingError] ' + trackNetwork.networkName + ' not found');
        }
      });
    }
  }

  private sendPositionDataToIframe() {
    var iframeOffsetTop = this._iframeElement.getBoundingClientRect().top + window.scrollY || window.pageYOffset;
    var viewtop = window.scrollY || window.pageYOffset;
    var viewbottom = viewtop + window.innerHeight;

    // Post position to iframe.
    this._iframeElement.contentWindow?.postMessage(
      {
        iframeOffsetTop: iframeOffsetTop,
        viewtop: viewtop,
        viewbottom: viewbottom,
      },
      '*',
    );
  }

  private preventSiteJump() {
    var doc = document.documentElement;
    this._scrollPosition = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
  }

  private onRiddleV1Event = (event: MessageEvent) => {
    let riddleEvent = this.getRiddleEventObject(event.data);

    if ((riddleEvent as RiddleEvent).isRiddle2Event) {
      return;
    }

    if ((riddleEvent as RiddleEventV1).riddleData !== undefined && (riddleEvent as RiddleEventV1).riddleData.id.toString() === this.riddleid.toString()) {
      if (this.isRiddleLoaded === false) {
        this.isRiddleLoaded = true;
      }
    }

    // Update height.
    if ((riddleEvent as RiddleEventV1).riddleHeight) {
      this.UpdateHeight((riddleEvent as RiddleEventV1).riddleHeight);
    }

    if (event.data.riddleEvent == 'page-change' && this.autoscroll) {
      // Scroll to top of Riddle.
      const pos = this._iframeElement.getBoundingClientRect().top;

      const target = pos + window.scrollY - 10 - this.autoscrolloffset;

      window.scrollTo({
        top: target,
        behavior: 'smooth',
      });

      this.sendPositionDataToIframe();
    } else if (event.data.riddleEvent == 'page-change' && !this.autoscroll) {
      this.preventSiteJump();
      this.sendPositionDataToIframe();
    }

    // Prevent site jump.
    if (event.data.riddleEvent && event.data.riddleEvent.action == 'answer-poll' && !this.autoscroll) {
      this.preventSiteJump();
    }

    // Redirect to custom landing page.
    this.redirectToCustomResultPage(riddleEvent);
  };

  private onRiddleV2Event = (event: MessageEvent) => {
    let riddleEvent = this.getRiddleEventObject(event.data);

    if (!(riddleEvent as RiddleEvent).isRiddle2Event || (riddleEvent as RiddleEvent).riddleId.toString() !== this.riddleid.toString()) {
      return;
    }

    // Remove loader.
    if ((riddleEvent as RiddleEvent).type === 'RiddleInited') {
      this.isRiddleLoaded = true;
    }

    // Update height.
    if ((riddleEvent as RiddleEvent).height) {
      this.UpdateHeight((riddleEvent as RiddleEvent).height);
      this.initDataLayer();
    }

    if (typeof event.data.category == 'string' && (event.data.category as string).startsWith('RiddleTrackEvent')) {
      const data = event.data as RiddleTrackEvent;
      this.pushTrackEvent(data);
    }

    if (((riddleEvent as RiddleEvent).type == 'PageChanged' || (event.data as RiddleTrackEvent).action === 'Block_View') && this.autoscroll) {
      // Scroll to top of Riddle.
      const pos = this._iframeElement.getBoundingClientRect().top;

      const target = pos + window.scrollY - 10 - this.autoscrolloffset;

      window.scrollTo({
        top: target,
        behavior: 'smooth',
      });

      this.sendPositionDataToIframe();
    } else if ((riddleEvent as RiddleEvent).type == 'PageChanged' && !this.autoscroll) {
      this.preventSiteJump();
      this.sendPositionDataToIframe();
    }

    // Prevent site jump.
    if ((riddleEvent as RiddleEvent).type === 'AnswerPoll' && !this.autoscroll) {
      this.preventSiteJump();
    }

    // Redirect to custom landing page.
    this.redirectToCustomResultPage(riddleEvent);
  };

  private redirectToCustomResultPage(riddleEvent: RiddleEvent | RiddleEventV1) {
    if (
      (riddleEvent as RiddleEvent).redirectToCustomLandingPage ||
      ((riddleEvent as RiddleEventV1).redirectToCustomLandingpagePath && (riddleEvent as RiddleEventV1).redirectToCustomLandingpageData)
    ) {
      const path = (riddleEvent as RiddleEventV1).redirectToCustomLandingpagePath || (riddleEvent as RiddleEvent).redirectToCustomLandingPage.path;
      const data = (riddleEvent as RiddleEventV1).redirectToCustomLandingpageData || (riddleEvent as RiddleEvent).redirectToCustomLandingPage.data;

      const form = window.parent.document.createElement('form');
      form.setAttribute('method', 'post');
      form.setAttribute('action', path);

      const hiddenField = window.parent.document.createElement('input');
      hiddenField.setAttribute('type', 'hidden');
      hiddenField.setAttribute('name', 'data');
      hiddenField.setAttribute('value', data);

      form.appendChild(hiddenField);
      window.parent.document.body.appendChild(form);

      form.submit();
    }
  }

  private getIframeUrl = (): string => {
    return 'https://www.riddle.com/a/' + this.riddleid;
  };

  private getIframeStyle = (): any => {
    return { height: this.iframeHeight + 'px', display: this.isRiddleLoaded ? 'block' : 'none' };
  };

  private getRiddleLoaderHtml = (): string => {
    return (
      <div class="rid-load-wrapper" style={{ display: !this.isRiddleLoaded ? 'block' : 'none' }}>
        <div class="rid-load-wrapper__loader">
          <p>
            <i></i>
            <i></i>
            <i></i>
          </p>
        </div>
      </div>
    );
  };

  private getRiddleV1Html = (): string => {
    return (
      <Host>
        <div class="App">
          {
            <div class="riddle_target" data-rid-id={this.riddleid}>
              <iframe id={'riddle-iframe-' + this.riddleid} class="riddle_target_iframe" src={this.getIframeUrl()} style={this.getIframeStyle()}></iframe>

              {this.getRiddleLoaderHtml()}
            </div>
          }
        </div>
      </Host>
    );
  };

  private getRiddleV2Html = (): string => {
    return (
      <Host>
        <div class="App">
          {
            <div class="riddle2-wrapper" data-rid-id={this.riddleid}>
              <iframe
                id={'riddle-iframe-' + this.riddleid}
                class="riddle2-wrapper_iframe"
                src={this.getIframeUrl()}
                style={this.getIframeStyle()}
                allow="autoplay"
                referrerPolicy="strict-origin"
              ></iframe>

              {this.getRiddleLoaderHtml()}
            </div>
          }
        </div>
      </Host>
    );
  };

  render() {
    if (this.isRiddleV1) {
      return this.getRiddleV1Html();
    } else {
      return this.getRiddleV2Html();
    }
  }
}