Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MWPW-140481: Delayed modal #1877

Closed
wants to merge 10 commits into from
15 changes: 3 additions & 12 deletions libs/blocks/modal/modal.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* eslint-disable import/no-cycle */
import { createTag, getMetadata, localizeLink, loadStyle, getConfig } from '../../utils/utils.js';
import {
createTag, getMetadata, localizeLink, loadStyle, getConfig, sendAnalytics,
} from '../../utils/utils.js';

const FOCUSABLES = 'a, button, input, textarea, select, details, [tabindex]:not([tabindex="-1"]';
const CLOSE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
Expand All @@ -21,17 +23,6 @@ export function findDetails(hash, el) {
return { id, path, isHash: hash === window.location.hash };
}

export function sendAnalytics(event) {
// eslint-disable-next-line no-underscore-dangle
window._satellite?.track('event', {
xdm: {},
data: {
web: { webInteraction: { name: event?.type } },
_adobe_corpnew: { digitalData: event?.data },
},
});
}

export function closeModal(modal) {
const { id } = modal;
const closeEvent = new Event('milo:modal:closed');
Expand Down
40 changes: 35 additions & 5 deletions libs/features/personalization/personalization.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
/* eslint-disable no-console */

import {
createTag, getConfig, loadLink, loadScript, localizeLink, updateConfig,
createTag,
getConfig,
loadLink,
loadScript,
localizeLink,
updateConfig,
defineHashParams,
setEventBasedModalListener,
} from '../../utils/utils.js';
import { getEntitlementMap } from './entitlements.js';

Expand Down Expand Up @@ -105,16 +112,39 @@ export const preloadManifests = ({ targetManifests = [], persManifests = [] }) =

export const getFileName = (path) => path?.split('/').pop();

const createFrag = (el, url, manifestId) => {
let href = url;
export const decorateDelayedModalAnchor = ({ a, hash, pathname }) => {
if (!a || !hash || !pathname) return;
a.setAttribute('href', hash);
a.setAttribute('data-modal-hash', hash);
a.setAttribute('data-modal-path', pathname);
a.setAttribute('style', 'display: none');
a.classList.add('modal');
a.classList.add('link-block');
};

export const parseUrl = (url) => {
if (!url) return {};
const parsed = {};
try {
const { pathname, search, hash } = new URL(url);
href = `${pathname}${search}${hash}`;
const { hashWithoutParams, ...params } = defineHashParams(hash);
Object.assign(parsed, { href: `${pathname}${search}${hashWithoutParams}`, pathname, hash: hashWithoutParams, ...params });
} catch {
// ignore
// if target has this format: '/fragments/somepath'
parsed.href = url;
}
return parsed;
};

export const createFrag = (el, url, manifestId) => {
const { hash, href, pathname, delay, display } = parseUrl(url);
const a = createTag('a', { href }, url);
if (manifestId) a.dataset.manifestId = manifestId;
if (delay && display) {
setEventBasedModalListener();
decorateDelayedModalAnchor({ a, hash, pathname });
window.dispatchEvent(new CustomEvent('modal:open', { detail: { hash, delay, display } }));
}
let frag = createTag('p', undefined, a);
const isSection = el.parentElement.nodeName === 'MAIN';
if (isSection) {
Expand Down
79 changes: 70 additions & 9 deletions libs/utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ export const MILO_EVENTS = { DEFERRED: 'milo:deferred' };
const LANGSTORE = 'langstore';
const PAGE_URL = new URL(window.location.href);

export const DISPLAY_MODE = {
oncePerPageLoad: 'pageload',
oncePerSession: 'session',
};

function getEnv(conf) {
const { host } = window.location;
const query = PAGE_URL.searchParams.get('env');
Expand Down Expand Up @@ -922,6 +927,25 @@ export function scrollToHashedElement(hash) {
});
}

export function defineHashParams(hash) {
const params = { hashWithoutParams: hash?.replaceAll(/:\w+=\w+/g, '') || '' };
hash?.split(':').forEach((part) => {
if (part.includes('delay')) {
const delayValue = Number(part.split('=')[1]);
if (Number.isInteger(delayValue) && delayValue > 0) {
params.delay = delayValue * 1000;
}
} else if (part.includes('display')) {
const displayValue = part.split('=')[1];
if (displayValue === DISPLAY_MODE.oncePerPageLoad
|| displayValue === DISPLAY_MODE.oncePerSession) {
params.display = displayValue;
}
}
});
return params;
}

export async function loadDeferred(area, blocks, config) {
const event = new Event(MILO_EVENTS.DEFERRED);
area.dispatchEvent(event);
Expand Down Expand Up @@ -966,6 +990,51 @@ function initSidekick() {
}
}

export function sendAnalytics(event) {
// eslint-disable-next-line no-underscore-dangle
window._satellite?.track('event', {
xdm: {},
data: {
web: { webInteraction: { name: event?.type } },
_adobe_corpnew: { digitalData: event?.data },
},
});
}

export function showModal({ delay, display, details, getModal }) {
if (!details) return;
const { id } = details;
if (delay && display) {
const modalOpenEvent = new Event(`${id}:modalOpen`);
if (display === DISPLAY_MODE.oncePerPageLoad) {
setTimeout(() => {
getModal(details);
sendAnalytics(modalOpenEvent);
}, delay);
} else if (display === DISPLAY_MODE.oncePerSession) {
if (!window.sessionStorage.getItem(`shown:${id}`)) {
setTimeout(() => {
getModal(details);
sendAnalytics(modalOpenEvent);
window.sessionStorage.setItem(`shown:${id}`, 'true');
}, delay);
}
}
} else {
getModal(details);
}
}

export function setEventBasedModalListener() {
window.addEventListener('modal:open', async ({ detail }) => {
const { miloLibs } = getConfig();
const { findDetails, getModal } = await import('../blocks/modal/modal.js');
loadStyle(`${miloLibs}/blocks/modal/modal.css`);
const { hash, delay, display } = detail || {};
showModal({ delay, display, details: findDetails(hash), getModal });
});
}

function decorateMeta() {
const { origin } = window.location;
const contents = document.head.querySelectorAll('[content*=".hlx."]');
Expand All @@ -980,15 +1049,7 @@ function decorateMeta() {
window.lana?.log(`Cannot make URL from metadata - ${meta.content}: ${e.toString()}`);
}
});

// Event-based modal
window.addEventListener('modal:open', async (e) => {
const { miloLibs } = getConfig();
const { findDetails, getModal } = await import('../blocks/modal/modal.js');
loadStyle(`${miloLibs}/blocks/modal/modal.css`);
const details = findDetails(e.detail.hash);
if (details) getModal(details);
});
setEventBasedModalListener();
}

function decorateDocumentExtras() {
Expand Down
64 changes: 63 additions & 1 deletion test/features/personalization/personalization.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ import { readFile } from '@web/test-runner-commands';
import { stub } from 'sinon';
import { getConfig, setConfig, loadBlock } from '../../../libs/utils/utils.js';
import initFragments from '../../../libs/blocks/fragment/fragment.js';
import { applyPers, normalizePath } from '../../../libs/features/personalization/personalization.js';
import { waitForElement } from '../../helpers/waitfor.js';
import {
applyPers,
createFrag,
normalizePath,
decorateDelayedModalAnchor,
parseUrl,
} from '../../../libs/features/personalization/personalization.js';

document.head.innerHTML = await readFile({ path: './mocks/metadata.html' });
document.body.innerHTML = await readFile({ path: './mocks/personalization.html' });
Expand Down Expand Up @@ -31,6 +38,30 @@ describe('Functional Test', () => {
};
});

it('should create a delayed modal anchor', async () => {
const hash = '#testhash';
const url = 'https://adobe.com/testpage/#testhash:delay=1:display=pageload';
const manifestId = 'testManifestId';
const parentElement = document.createElement('div');
const el = document.createElement('div');
parentElement.appendChild(el);
document.body.appendChild(parentElement);
const frag = createFrag(el, url, manifestId);
expect(frag.nodeName).to.equal('P');
const link = frag.querySelector('a');
expect(link.getAttribute('href')).to.equal(hash);
expect(link.getAttribute('data-modal-hash')).to.equal(hash);
expect(link.getAttribute('data-modal-path')).to.equal('/testpage/');
expect(link.getAttribute('style')).to.equal('display: none');
expect(link.classList.contains('modal')).to.be.true;
expect(link.classList.contains('link-block')).to.be.true;
el.appendChild(frag);
const delayedModal = await waitForElement(hash);
expect(delayedModal).to.exist;
delayedModal.remove();
parentElement.remove();
});

it('replaceContent should replace an element with a fragment', async () => {
let manifestJson = await readFile({ path: './mocks/manifestReplace.json' });
manifestJson = JSON.parse(manifestJson);
Expand Down Expand Up @@ -296,4 +327,35 @@ describe('normalizePath function', () => {
const path = await normalizePath('https://main--milo--adobecom.hlx.page/path/to/fragment.plain.html');
expect(path).to.equal('/de/path/to/fragment.plain.html');
});

it('sets proper attributes on the delayed modal anchor link', () => {
const a = document.createElement('a');
const hash = '#dm';
decorateDelayedModalAnchor({ a, hash, pathname: 'path/to/page' });
expect(a.getAttribute('href')).to.equal(hash);
expect(a.getAttribute('data-modal-hash')).to.equal(hash);
expect(a.getAttribute('data-modal-path')).to.equal('path/to/page');
expect(a.getAttribute('style')).to.equal('display: none');
expect(a.classList.contains('modal')).to.be.true;
expect(a.classList.contains('link-block')).to.be.true;
a.remove();
});

it('parses URL properly', () => {
const hash = '#dm';
expect(parseUrl()).to.deep.equal({});
expect(parseUrl('https://www.adobe.com/')).to.deep.equal({
hash: '',
href: '/',
pathname: '/',
});
expect(parseUrl('/fragments/testpage')).to.deep.equal({ href: '/fragments/testpage' });
expect(parseUrl('https://www.adobe.com/testpage/#dm:delay=1:display=pageload')).to.deep.equal({
hash,
href: '/testpage/#dm',
pathname: '/testpage/',
delay: 1000,
display: 'pageload',
});
});
});
Loading
Loading