Skip to content

Commit

Permalink
added delayed modal
Browse files Browse the repository at this point in the history
  • Loading branch information
mirafedas committed Feb 19, 2024
1 parent b5e0ce8 commit 80e56ec
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 9 deletions.
42 changes: 42 additions & 0 deletions libs/blocks/modal/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ const CLOSE_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="2
</svg>`;
const MOBILE_MAX = 599;
const TABLET_MAX = 1199;
export const DISPLAY_MODE = {
oncePerPageLoad: 'pageload',
oncePerSession: 'session',
};
let messageAbortController;
let resizeAbortController;

Expand Down Expand Up @@ -246,3 +250,41 @@ window.addEventListener('hashchange', (e) => {
if (details) getModal(details);
}
});

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 defineDelayedModalParams = (search) => {
if (!search) return {};
const urlParams = new URLSearchParams(search);
const delay = Number(urlParams?.get('delay'));
const displayMode = urlParams?.get('display');
return {
...(Number.isInteger(delay) && delay > 0 && { delay: delay * 1000 }),
...((displayMode === DISPLAY_MODE.oncePerPageLoad
|| displayMode === DISPLAY_MODE.oncePerSession) && { displayMode }),
};
};

export function showModalWithDelay({ delay, displayMode, hash }) {
if (!delay || !displayMode || !hash) return;
if (displayMode === DISPLAY_MODE.oncePerPageLoad) {
setTimeout(() => {
window.location.hash = hash;
}, delay);
} else if (displayMode === DISPLAY_MODE.oncePerSession) {
if (!window.sessionStorage.getItem('wasDelayedModalShown')) {
setTimeout(() => {
window.location.hash = hash;
window.sessionStorage.setItem('wasDelayedModalShown', true);
}, delay);
}
}
}
34 changes: 26 additions & 8 deletions libs/features/personalization/personalization.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
createTag, getConfig, loadLink, loadScript, localizeLink, updateConfig,
} from '../../utils/utils.js';
import { getEntitlementMap } from './entitlements.js';
import { defineDelayedModalParams, decorateDelayedModalAnchor, showModalWithDelay } from '../../blocks/modal/modal.js';

/* c20 ignore start */
const PHONE_SIZE = window.screen.width < 768 || window.screen.height < 768;
Expand Down Expand Up @@ -105,18 +106,35 @@ export const preloadManifests = ({ targetManifests = [], persManifests = [] }) =

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

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

export const createFrag = (el, url, manifestId) => {
const { href, pathname, hash, search } = parseUrl(url);
const a = createTag('a', { href }, url);
if (manifestId) a.dataset.manifestId = manifestId;
let frag = createTag('p', undefined, a);
const isSection = el.parentElement.nodeName === 'MAIN';
const { delay, displayMode } = defineDelayedModalParams(search);
let frag = createTag('p', undefined, a);

if (delay && displayMode) {
frag = a;
decorateDelayedModalAnchor({ a, hash, pathname });
showModalWithDelay({ delay, displayMode, hash });
}
if (manifestId) a.dataset.manifestId = manifestId;
if (isSection) {
frag = createTag('div', undefined, frag);
}
Expand All @@ -125,9 +143,9 @@ const createFrag = (el, url, manifestId) => {
};

const COMMANDS = {
insertcontentafter: (el, target, manifestId) => el
insertcontentafter: async (el, target, manifestId) => el
.insertAdjacentElement('afterend', createFrag(el, target, manifestId)),
insertcontentbefore: (el, target, manifestId) => el
insertcontentbefore: async (el, target, manifestId) => el
.insertAdjacentElement('beforebegin', createFrag(el, target, manifestId)),
removecontent: (el, target, manifestId) => {
if (target === 'false') return;
Expand Down
99 changes: 99 additions & 0 deletions test/features/personalization/delayedModal.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { expect } from '@esm-bundle/chai';
import { parseUrl } from '../../../libs/features/personalization/personalization.js';
import { defineDelayedModalParams, decorateDelayedModalAnchor, showModalWithDelay, DISPLAY_MODE } from '../../../libs/blocks/modal/modal.js';
import { delay, waitForElement } from '../../helpers/waitfor.js';

const hash = '#dm';

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

it('takes the delayed modal parameters from the URL', () => {
expect(defineDelayedModalParams()).to.deep.equal({});
expect(defineDelayedModalParams('?delay=invalid&display=invalid')).to.deep.equal({});
expect(defineDelayedModalParams('?delay=-1&display=pageload')).to.deep.equal({ displayMode: DISPLAY_MODE.oncePerPageLoad });
expect(defineDelayedModalParams('?delay=1&display=pageload')).to.deep.equal({
delay: 1000,
displayMode: DISPLAY_MODE.oncePerPageLoad,
});
});

it('add proper attributes and class names to the link', () => {
const a = document.createElement('a');
decorateDelayedModalAnchor({
a,
hash,
pathname: '/testpage/',
});
expect(a.getAttribute('href')).to.equal(hash);
expect(a.getAttribute('data-modal-hash')).to.equal(hash);
expect(a.getAttribute('data-modal-path')).to.equal('/testpage/');
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('creates and opens the delayed modal', async () => {
const a = document.createElement('a');
decorateDelayedModalAnchor({
a,
hash,
pathname: '/testpage',
});
document.body.appendChild(a);
showModalWithDelay({
delay: '1',
displayMode: DISPLAY_MODE.oncePerPageLoad,
hash,
});
const delayedModal = await waitForElement(hash);
expect(delayedModal).to.exist;
delayedModal.remove();
a.remove();
window.location.hash = '';
});

it('does not open a delayed modal if it should be displayed once per session and was already displayed', async () => {
const a = document.createElement('a');
window.sessionStorage.removeItem('wasDelayedModalShown');
decorateDelayedModalAnchor({
a,
hash,
pathname: '/testpage',
});
document.body.appendChild(a);
showModalWithDelay({
delay: '1',
displayMode: DISPLAY_MODE.oncePerSession,
hash,
});
const delayedModal = await waitForElement(hash);
expect(delayedModal).to.exist;
expect(window.sessionStorage.getItem('wasDelayedModalShown')).to.equal('true');
delayedModal.remove();

showModalWithDelay({
delay: '1',
displayMode: DISPLAY_MODE.oncePerSession,
hash,
});
await delay(900);
expect(document.querySelector(hash)).to.not.exist;
a.remove();
window.sessionStorage.removeItem('wasDelayedModalShown');
});
28 changes: 27 additions & 1 deletion test/features/personalization/personalization.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ 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 } 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 +32,31 @@ describe('Functional Test', () => {
};
});

it('should create fragment', async () => {
const hash = '#testhash';
const parentElement = document.createElement('div');
const el = document.createElement('div');
parentElement.appendChild(el);
document.body.appendChild(parentElement);
const url = 'https://adobe.com/testpage/?delay=1&display=pageload#testhash';
const manifestId = 'testManifestId';
const frag = createFrag(el, url, manifestId);
expect(frag.nodeName).to.equal('A');
expect(frag.getAttribute('href')).to.equal(hash);
expect(frag.getAttribute('data-modal-hash')).to.equal(hash);
expect(frag.getAttribute('data-modal-path')).to.equal('/testpage/');
expect(frag.getAttribute('style')).to.equal('display: none');
expect(frag.classList.contains('modal')).to.be.true;
expect(frag.classList.contains('link-block')).to.be.true;
const delayedModal = await waitForElement(hash);
const modalStylesLink = window.document.head.querySelector('link[href="undefined/blocks/modal/modal.css"]');
expect(delayedModal).to.exist;
expect(modalStylesLink).to.exist;
delayedModal.remove();
modalStylesLink.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

0 comments on commit 80e56ec

Please sign in to comment.