Embed Riddle in modern frameworks

Riddle ID
<template>
    <div v-if="riddle2WrapperStyle.display === 'none'" class="rid-load-wrapper">
        <div class="rid-load-wrapper__loader">
            <p>
                <i></i>
                <i></i>
                <i></i>
            </p>
        </div>
    </div>
    <div class="riddle2-wrapper" :style="riddle2WrapperStyle">
        <iframe ref="_iframeElement" :style="iframeStyle"
            :src="`https://riddle.com/embed/a/${props.riddleId}?lazyImages=${lazyImages}&staticHeight=${staticHeight}`"
            allow="autoplay" referrerpolicy="strict-origin"></iframe>

    </div>
</template>

<script setup lang="ts">
import { onMounted, ref } from 'vue';

interface RiddleDataLayerItem {
    key: string;
    value: string | number;
}

type RiddleEventType =
    | "RiddleInited"
    | "RiddleNotInited"
    | "HeightChanged"
    | "PageChanged"
    | "Voted";

interface RiddleEvent {
    isRiddle2Event: boolean;
    type: RiddleEventType;
    riddleId: string;
    height: number;
    isUnrolled: boolean;
    redirectToCustomLandingPage?: {
        path: string;
        data: string;
    };
}

enum RiddleTrackEventAction {
    Form_Submit = "Form_Submit",
    Form_Skip = "Form_Skip",
    Block_Submit = "Block_Submit",
    Block_Skip = "Block_Skip",
    Block_View = "Block_View",
    Block_Next = "Block_Next",
    Social = "Social",
    Cta = "Cta",
    CoreMetrics = "CoreMetrics",
    LeadSettings = "LeadSettings",
}

enum RiddleTrackNetworks {
    customTracking = "customTracking",
    facebookPixel = "facebookPixel",
    googleAnalytics = "googleAnalytics",
    googleAnalytics4 = "googleAnalytics4",
    googleTagManager = "googleTagManager",
    matomoTag = "matomoTag",
}

enum RiddleTracDefaultkNetworkObjects {
    facebookPixel = "fbq",
    googleAnalytics = "ga",
    googleAnalytics4 = "gtag",
    googleTagManager = "dataLayer",
    matomoTag = "_paq",
}

interface RiddleTrackNetworksConfig {
    networkName: RiddleTrackNetworks;
}

interface RiddleTrackNetworksConfig_Default extends RiddleTrackNetworksConfig {
    networkName: RiddleTrackNetworks;
    networkObjectName: string;
}

interface RiddleTrackNetworksConfig_Custom extends RiddleTrackNetworksConfig {
    networkName: RiddleTrackNetworks;
    eventFunction: string;
}

type RiddleTrackEventDataTypes =
    | "blockId"
    | "blockType"
    | "blockTypeGroup"
    | "blockTitle"
    | "blockDescription"
    | "answerIsCorrect"
    | "answerScore"
    | "answerTotalScore"
    | "answerLivesTotal"
    | "answerLivesUsed"
    | "answerSpotsTotal"
    | "answerSpotsFound"
    | "answer"
    | "resultScore"
    | "resultTotalScore"
    | "resultScorePercentage"
    | "personalityScore"
    | "personalityTotalScore"
    | "personalityScorePercentage"
    | "timerRiddleTotalSeconds"
    | "timerRiddleCurrentSeconds"
    | "timerBlockTotalSeconds"
    | "timerBlockCurrentSeconds";

type RiddleTrackEvent = {
    [key in RiddleTrackEventDataTypes]: any;
} & {
    isRiddle2Event: boolean;
    riddleId: string;
    category: string;
    action: RiddleTrackEventAction;
    name: string;
    trackNetworks?: Array<RiddleTrackNetworksConfig>;
};

interface Props {
    riddleId: string;
    autoScroll?: boolean;
    autoScrollOffset?: number;
    staticHeight?: boolean;
    height?: number;
    lazyImages?: boolean;
    width: string;
}

const props = withDefaults(defineProps<Props>(), {
    autoScroll: false,
    autoScrollOffset: 0,
    staticHeight: false,
    height: 400,
    lazyImages: false,
    width: '640px'
})

const emit = defineEmits(['riddle-loaded'])

const riddle2WrapperStyle = ref({
    margin: '0 auto',
    maxWidth: '100%',
    width: props.width,
    display: 'none'
})

const iframeStyle = ref({
    width: '100%',
    height: '100%',
    border: 'none'
})

const _iframeElement = ref<HTMLIFrameElement>()
const isMessageListenerAdded = ref(false)
const _riddleDataLayer = ref<Array<RiddleDataLayerItem>>()
const _scrollPosition = ref(0)

onMounted(() => {
    console.log('Riddle Embed mounted');

    if (!isMessageListenerAdded.value) {
        isMessageListenerAdded.value = true

        window.addEventListener('message', onRiddleV2Event, false);
    }

    _scrollPosition.value = 0;
    document.addEventListener('scroll', onScroll);
})

onBeforeUnmount(() => {
    window.removeEventListener('message', onRiddleV2Event);
    document.removeEventListener('scroll', onScroll);
})

const onScroll = () => {
    if (_scrollPosition.value != 0) {
        window.scrollTo(0, _scrollPosition.value);
        _scrollPosition.value = 0;
    }

    sendPositionDataToIframe();
}


const 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;
};

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

            const isValid = validateDataLayerItem(target, value);

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

            return isValid;
        },
    };

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

    readFromUrl();
}

const 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];

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

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

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

const UpdateHeight = (height: number) => {
    iframeStyle.value.height = height + 'px';
}

const 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');
            }
        });
    }
}

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

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

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

const onRiddleV2Event = (event: MessageEvent) => {
    let riddleEvent = event.data as RiddleEvent;

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

    // Remove Loader
    if ((riddleEvent as RiddleEvent).type === 'RiddleInited') {
        riddle2WrapperStyle.value.display = 'block';
        emit('riddle-loaded');
    }

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

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

    if (((riddleEvent as RiddleEvent).type == 'PageChanged' || (event.data as RiddleTrackEvent).action === 'Block_View') && props.autoScroll) {
        console.log("🚀 ~ onRiddleV2Event ~ props.autoScroll:", props.autoScroll)

        // scroll to top of riddle
        const pos = _iframeElement.value.getBoundingClientRect().top;

        const target = pos + window.scrollY - 10 - props.autoScrollOffset;

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

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

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

const redirectToCustomResultPage = (riddleEvent: RiddleEvent) => {
    if (
        (riddleEvent as RiddleEvent).redirectToCustomLandingPage
    ) {
        const path = (riddleEvent as RiddleEvent).redirectToCustomLandingPage.path;
        const data = (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();
    }
}
</script>

<style lang="scss" scoped>
.riddle2-wrapper {}

.rid-load-wrapper {
    position: relative;
    margin: 0 auto;
    max-width: 100%;
    width: 640px;
    height: 358.391px;
}

.rid-load-wrapper__loader {
    background: transparent;
}

.rid-load-wrapper__loader i {
    background: transparent;
}

.rid-load-wrapper__loader i:before {
    border-color: #00205b;
}

.rid-load-wrapper__loader {
    padding-top: 56%;
    border-radius: 5px;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    transition: opacity ease-out 0.5s;
    box-sizing: border-box;
}

.rid-load-wrapper__loader p {
    position: absolute;
    top: 50%;
    left: 50%;
    margin: -8px;
}

.rid-load-wrapper__loader i {
    position: absolute;
    display: block;
    width: 20px;
    height: 20px;
    border-radius: 5px;
    left: -8px;
    animation: 3s infinite rid-icon;
    transform: scale(1) rotate(30deg);
}

.rid-load-wrapper__loader i:before {
    box-sizing: border-box;
    content: '';
    display: block;
    position: absolute;
    left: 2px;
    right: 2px;
    bottom: 2px;
    top: 2px;
    border: 2px solid;
    border-radius: 3px;
}

.rid-load-wrapper__loader i+i {
    left: 0;
    top: -4px;
    transform: scale(1) rotate(45deg);
}

.rid-load-wrapper__loader i+i+i {
    left: 8px;
    top: 0;
    transform: scale(1) rotate(60deg);
}

@keyframes rid-icon {
    40% {
        left: 0;
        top: 0;
        transform: scale(1) rotate(0);
    }

    60% {
        left: 0;
        top: 0;
        transform: scale(1) rotate(0);
    }
}
</style>