refactor: REfactor CookieNotice -> move out document.cookie manipulation, simplify CookieNotice

This commit is contained in:
Jérémy Munsch 2024-06-13 16:34:01 +02:00
parent 44bb86c75c
commit 8afa07eefc
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
3 changed files with 180 additions and 204 deletions

View file

@ -1,33 +1,31 @@
import { CookieOptions, CookiePolicy, CookieStatusList } from "../type";
import { CookieOptions, CookiePolicies, CookieStatusList } from "../type";
import { CookiePolicySameSite, CookieStatus } from "./../enums";
import { DocumentCookie } from "./DocumentCookie";
export default class CookieNotice {
/** The CN cookie status */
browserCookieName = "cn-init";
private browserCookieName = "cn-init";
/** Cookie life time */
cookieTimeLife = 6;
private cookieTimeLife = 6;
/** The index on which store the expiration date */
cookieExpirationName = "_expiration";
private cookieExpirationName = "_expiration";
/** Cookie policies for each functionality */
cookiePolicies: CookiePolicy = {
private cookiePolicies: CookiePolicies = {
sameSite: CookiePolicySameSite.lax,
secure: "",
};
/** Cookie expiration date as specified in the cookie itself */
cookieExpirationDate: Date;
/** Holding all cookie configs */
config: Record<string, CookieOptions>;
private cookieExpirationDate: Date;
/** Prevent user from closing modal */
restrictCloseModal: boolean = true;
private restrictCloseModal: boolean = true;
// * -- DOM ELEMENTS
domElList: {
private domElList: {
/** Modal transparent background => div.cn-modal-bg */
modalBg?: HTMLDivElement|null,
/** Modal window container for the cookie panel and button => div#cn-wrapper */
@ -58,7 +56,10 @@ export default class CookieNotice {
// * -- END DOM ELEMENTS
constructor(config: Record<string, CookieOptions>) {
/**
* @param config Holding all cookie configs
*/
constructor(private config: Record<string, CookieOptions>) {
// * Setup Config
this.config = config;
@ -76,7 +77,7 @@ export default class CookieNotice {
// * Setup CN Cookie
if (!this.cookiesMatchesConfig() || this.cookiesOlderThan(new Date())) {
// * Setup Browser Cookie if none.
this.resetBrowserCookie();
this.resetCnCookie();
this.openCookieModalToBannerPage();
}
@ -105,8 +106,70 @@ export default class CookieNotice {
});
}
// * --- PUBLIC ---
/** Accept a all cookies */
public acceptAllCookies(): void {
Object.keys(this.config).forEach((cookieName) => this.acceptCookie(cookieName));
this.closeCookieModal();
}
/** Accept a specific cookie */
public acceptCookie(cookieName: string): void {
const cookieOptions = this.getCookie(cookieName);
this.setCookieStatus(cookieName, CookieStatus.true, cookieOptions);
const cnSwitch = document.getElementById(`${cookieName}-switch`) as HTMLInputElement | null;
const cnSwitchInput = cnSwitch?.querySelector("input") as HTMLInputElement | null;
if (!cnSwitch || !cnSwitchInput) {
return;
}
cnSwitchInput.checked = true;
cnSwitch.ariaChecked = "true";
}
/** Deny a all cookies */
public denyAllCookies(): void {
Object.keys(this.config).forEach((cookieName) => this.denyCookie(cookieName));
this.closeCookieModal();
}
/** Deny a specific cookie */
public denyCookie(cookieName: string): void {
const cookieOptions = this.getCookie(cookieName);
this.setCookieStatus(cookieName, CookieStatus.false, cookieOptions);
const cnSwitch = document.getElementById(`${cookieName}-switch`) as HTMLInputElement | null;
const cnSwitchInput = cnSwitch?.querySelector("input") as HTMLInputElement | null;
if (!cnSwitch || !cnSwitchInput) {
return;
}
cnSwitchInput.checked = false;
cnSwitch.ariaChecked = "false";
}
/** Deny all waiting cookies */
public denyAllWaitingCookies(): void {
Object.entries(this.getCookieStatusList()).forEach(([cookieName, cookieStatus]) => {
if (cookieStatus === CookieStatus.wait) {
this.setCnCookieStatusFor(cookieName, CookieStatus.false);
return;
}
});
this.updateRestrictCloseModal();
}
/** Get a specific cookie config */
public getCookie(cookieName: string): CookieOptions {
const cookieOptions = Object.entries(this.config).find((entry) => entry[0] === cookieName)?.[1];
if (cookieOptions === undefined) {
throw new Error(`Cookie options ${cookieName} not found in list`);
}
return cookieOptions;
}
// * --- PRIVATE ---
/** Update restrict for closing cookie modal */
updateRestrictCloseModal(): void {
private updateRestrictCloseModal(): void {
const cookieCloseModalBtn = this.getDomEl("cookieCloseModalBtn"),
modalBg = this.getDomEl("modalBg");
this.restrictCloseModal = (Object.values(this.getCookieStatusList()).includes(CookieStatus.wait)) ? true : false;
@ -115,8 +178,9 @@ export default class CookieNotice {
}
/** Get cookies expiration date or null if not defined already */
readCookiesExpirationDate(): Date | null {
const cookies = this.getBrowserCookieValue() ?? "",
private readCookiesExpirationDate(): Date | null {
/** @var cookies A like analytics=true,youtube=true or undefined */
const cookies = DocumentCookie.get(this.browserCookieName) ?? "",
expirationDateString = cookies.split(",").find((cookieString) =>
cookieString.startsWith(this.cookieExpirationName));
if (!expirationDateString) {
@ -138,81 +202,24 @@ export default class CookieNotice {
*
* @param date The limit date for cookies
*/
cookiesOlderThan(date: Date): boolean {
private cookiesOlderThan(date: Date): boolean {
return this.cookieExpirationDate.getTime() < date.getTime();
}
/** Check if the actual cookie matches the config */
cookiesMatchesConfig(): boolean {
private cookiesMatchesConfig(): boolean {
const cookies = this.getCookieStatusList();
return Object.keys(cookies).length === Object.keys(this.config).length;
}
/** Accept a all cookies */
acceptAllCookies(): void {
Object.keys(this.config).forEach((cookieName) => this.acceptCookie(cookieName));
this.closeCookieModal();
}
/** Accept a specific cookie */
acceptCookie(cookieName: string): void {
const cookieOptions = this.getCookie(cookieName);
this.setCookieStatus(cookieName, CookieStatus.true, cookieOptions);
const cnSwitch = document.getElementById(`${cookieName}-switch`) as HTMLInputElement | null;
const cnSwitchInput = cnSwitch?.querySelector("input") as HTMLInputElement | null;
if (!cnSwitch || !cnSwitchInput) {
return;
}
cnSwitchInput.checked = true;
cnSwitch.ariaChecked = "true";
}
/** Deny a all cookies */
denyAllCookies(): void {
Object.keys(this.config).forEach((cookieName) => this.denyCookie(cookieName));
this.closeCookieModal();
}
/** Deny a specific cookie */
denyCookie(cookieName: string): void {
const cookieOptions = this.getCookie(cookieName);
this.setCookieStatus(cookieName, CookieStatus.false, cookieOptions);
const cnSwitch = document.getElementById(`${cookieName}-switch`) as HTMLInputElement | null;
const cnSwitchInput = cnSwitch?.querySelector("input") as HTMLInputElement | null;
if (!cnSwitch || !cnSwitchInput) {
return;
}
cnSwitchInput.checked = false;
cnSwitch.ariaChecked = "false";
}
/** Deny all waiting cookies */
denyAllWaitingCookies(): void {
Object.entries(this.getCookieStatusList()).forEach(([cookieName, cookieStatus]) => {
if (cookieStatus === CookieStatus.wait) {
this.setBrowserCookieStatus(cookieName, CookieStatus.false);
return;
}
});
this.updateRestrictCloseModal();
}
/** Get a specific cookie config */
getCookie(cookieName: string): CookieOptions {
const cookieOptions = Object.entries(this.config).find((entry) => entry[0] === cookieName)?.[1];
if (cookieOptions === undefined) {
throw new Error(`Cookie options ${cookieName} not found in list`);
}
return cookieOptions;
}
/**
* Fetch the cookie status list
*
* @returns the cookie status list
*/
getCookieStatusList(): CookieStatusList {
const cookies = this.getBrowserCookieValue(),
private getCookieStatusList(): CookieStatusList {
/** @var cookies A like analytics=true,youtube=true or undefined */
const cookies = DocumentCookie.get(this.browserCookieName),
statusList: CookieStatusList = {};
if (cookies === undefined) {
return statusList;
@ -228,27 +235,13 @@ export default class CookieNotice {
return statusList;
}
/**
* Check has cookie status
*
* @param key
* @param value
*/
containsCookieStatus(key: string): boolean {
const statusList: CookieStatusList = this.getCookieStatusList();
return (
Object.keys(statusList).find((cookieName) => cookieName === key) !==
undefined
);
}
/**
* Get a cookie status
*
* @param key
* @returns
*/
getCookieStatus(key: string): CookieStatus {
private getCookieStatus(key: string): CookieStatus {
const statusList: CookieStatusList = this.getCookieStatusList();
const cookieIndex = Object.keys(statusList).find((cookieName) => cookieName === key);
if (cookieIndex === undefined) {
@ -264,52 +257,25 @@ export default class CookieNotice {
* @param status
* @param cookieOption
*/
setCookieStatus(
private setCookieStatus(
cookieName: string,
status: CookieStatus,
cookieOption: CookieOptions
): void {
this.setCnCookieStatusFor(cookieName, status);
if (status === CookieStatus.true) {
this.addCookieStatus(cookieName, cookieOption);
if (cookieOption.script) {
cookieOption.script();
}
return;
}
if (status === CookieStatus.false) {
this.removeCookieStatus(cookieName, cookieOption);
if (cookieOption.fallback) {
cookieOption.fallback();
}
return;
}
this.setBrowserCookieStatus(cookieName, CookieStatus.wait);
}
/**
* Add a specific cookie and
* change his status to true
*
* @param cookieName
* @param cookieOption
*/
addCookieStatus(cookieName: string, cookieOption: CookieOptions): void {
if (cookieOption.script) {
cookieOption.script();
}
this.setBrowserCookieStatus(cookieName, CookieStatus.true);
}
/**
* Remove a specific cookie and
* change his status to false
*
* @param cookieName
* @param cookieOption
*/
removeCookieStatus(cookieName: string, cookieOption: CookieOptions): void {
const cookieNameElement = document.getElementById(cookieName);
if (cookieNameElement) {
cookieNameElement.remove();
}
if (cookieOption.fallback) {
cookieOption.fallback();
}
this.setBrowserCookieStatus(cookieName, CookieStatus.false);
this.setCnCookieStatusFor(cookieName, CookieStatus.wait);
}
/**
@ -318,48 +284,21 @@ export default class CookieNotice {
* @param cookieName
* @param status
*/
setBrowserCookieStatus(cookieName: string, status: CookieStatus): void {
private setCnCookieStatusFor(cookieName: string, status: CookieStatus): void {
const statusList: CookieStatusList = this.getCookieStatusList();
// * Force refresh
this.deleteBrowserCookie();
DocumentCookie.markForDeletion(this.browserCookieName);
statusList[cookieName] = status;
this.saveBrowserCookie(statusList);
this.persistCnCookie(statusList);
this.updateRestrictCloseModal();
}
/**
* Has CN cokie setup ?
* @returns true if the cookie is setup
*/
hasBrowserCookie(): boolean {
return Boolean(this.getBrowserCookieValue());
}
/**
* Get the Ldcweb cookie
*
* @returns A string like analytics=true,youtube=true or undefined
*/
getBrowserCookieValue(): string|undefined {
const matches = document.cookie.match(
new RegExp(
"(?:^|; )" +
this.browserCookieName.replace(
/([.$?*|{}()[\]\\/+^])/g,
"\\$1"
) +
"=([^;]*)"
)
);
return matches ? decodeURIComponent(matches[1]) : undefined;
}
/**
* Save cookie content on web browser
*
* @param list
*/
saveBrowserCookie(statusList: CookieStatusList): void {
private persistCnCookie(statusList: CookieStatusList): void {
const date = new Date(),
content = Object.entries(statusList)
.map(([cookieName, cookieStatus]) => {
@ -369,45 +308,28 @@ export default class CookieNotice {
this.cookieExpirationDate = new Date(date.setMonth(date.getMonth() + this.cookieTimeLife));
// * Append expiration date
content.push(`${this.cookieExpirationName}=${this.cookieExpirationDate.getTime()}`);
document.cookie = `${this.browserCookieName}=${content.join(",")};path=/${
location.hostname ? `;domain=${location.hostname}` : ""
};expires=${this.cookieExpirationDate.toUTCString()};${this.getCookiePoliciesAsString()}`;
}
/**
* Set an older expire parameter to a specific cookie
*/
deleteBrowserCookie(): void {
if (this.hasBrowserCookie()) {
// * Suppression du cookie et de sa version precedé d'un point
document.cookie = `${this.browserCookieName}=;path=/${
location.hostname ? `;domain=.${location.hostname}` : ""
};expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
document.cookie = `${this.browserCookieName}=;path=/${
location.hostname ? `;domain=${location.hostname}` : ""
};expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
}
}
getCookiePoliciesAsString(): string {
return Object.entries(this.cookiePolicies)
.map(([policyName, policyValue]) => `${policyName}=${policyValue}`)
.join(";");
DocumentCookie.set(
this.browserCookieName,
content.join(","),
this.cookieExpirationDate,
this.cookiePolicies
);
}
/**
* Reset Ldc Web CN cookie
*/
resetBrowserCookie(): void {
private resetCnCookie(): void {
// * Create Cookie as It does not exists.
const newCookieList: CookieStatusList = {};
Object.entries(this.config).forEach(
([cookieName]) => (newCookieList[cookieName] = CookieStatus.wait)
);
this.saveBrowserCookie(newCookieList);
this.persistCnCookie(newCookieList);
}
// LISTENERS
// * --- LISTENERS ---
/**
* Add event on a specific element
@ -417,7 +339,7 @@ export default class CookieNotice {
* @param funct
* @returns
*/
addEvent(element: HTMLElement, evnt: string, funct: (e: Event) => void): void {
private addEvent(element: HTMLElement, evnt: string, funct: (e: Event) => void): void {
element.addEventListener(evnt, funct.bind(this), false);
}
@ -428,7 +350,7 @@ export default class CookieNotice {
* @param evnt
* @param funct
*/
addEventPrevent(
private addEventPrevent(
element: HTMLElement,
evnt: string,
funct: (e: Event) => void
@ -449,7 +371,7 @@ export default class CookieNotice {
* @param element A HTMLElement on which the should should be bound.
* @param funct A closure for the event hanling.
*/
addEventKbdValidate(element: HTMLElement, funct: (e: Event) => void): void {
private addEventKbdValidate(element: HTMLElement, funct: (e: Event) => void): void {
this.addEvent(element, "keydown", (e: Event) => {
const kbdEvt = e as KeyboardEvent;
if (
@ -466,10 +388,10 @@ export default class CookieNotice {
});
}
// COOKIE MODAL
// * --- COOKIE MODAL ---
/** Show or hide Cookie modal */
toggleCookieModal(): void {
private toggleCookieModal(): void {
if (!document.body.classList.contains("cn-open")) {
this.openCookieModalToBannerPage();
return;
@ -478,13 +400,13 @@ export default class CookieNotice {
}
/** Open the cookies modal to the banner page */
openCookieModalToBannerPage() {
private openCookieModalToBannerPage() {
this.openCookieModalToCookiePage();
this.toggleCookieModalPages();
}
/** Open the cookies modal to the cookies page */
openCookieModalToCookiePage() {
private openCookieModalToCookiePage() {
document.body.classList.add("cn-open");
this.resetCookieModal();
this.getDomEl("bannerContent").classList.add("d-none");
@ -498,7 +420,7 @@ export default class CookieNotice {
}
/** Hide the cookies modal */
closeCookieModal() {
private closeCookieModal() {
this.updateRestrictCloseModal();
if (!this.restrictCloseModal) {
document.body.classList.remove("cn-open");
@ -516,7 +438,7 @@ export default class CookieNotice {
}
/** Setup tab indexes on visible elements */
setTabIndex() {
private setTabIndex() {
// * Prevent focus html tag
document.documentElement.setAttribute("tabindex", "-1");
setTimeout(() => {
@ -557,7 +479,7 @@ export default class CookieNotice {
}
/** Undo setup tab indexes on visible elements */
unsetTabIndex() {
private unsetTabIndex() {
document.documentElement.setAttribute("tabindex", "0");
setTimeout(() => {
document.body.querySelectorAll(".cn-disabled").forEach((el) => {
@ -569,7 +491,7 @@ export default class CookieNotice {
}
/** Reset the modal view state */
resetCookieModal() {
private resetCookieModal() {
const customizeBtn = this.getDomEl("customizeBtn");
this.getDomEl("bannerContent").classList.remove("d-none");
this.getDomEl("modalContainer").classList.add("d-none");
@ -581,7 +503,7 @@ export default class CookieNotice {
}
/** Set the cookies list in the modal */
setupCookieModalList() {
private setupCookieModalList() {
this.getDomEl("cookiesUnorderedList").innerHTML = "";
Object.entries(this.config).forEach(([cookieName, cookieConfig]) => {
const cookie = document.createElement("li");
@ -639,7 +561,7 @@ export default class CookieNotice {
this.getDomEl("cookiesUnorderedList").append(cookie);
if (!this.hasBrowserCookie()) {
if (!DocumentCookie.has(this.browserCookieName)) {
// * Ldcweb cookie is not set.
return;
}
@ -667,7 +589,7 @@ export default class CookieNotice {
});
}
switchToModalPageOrClose(): void {
private switchToModalPageOrClose(): void {
// * Modification du comportement du bouton, même si ca n'a pas de sens.
// https://lab.s-web.fr/ldc-web/projets/ldc-web-cookie-notice/-/issues/9`
@ -680,7 +602,7 @@ export default class CookieNotice {
}
/** Toggle Cookie modal pages */
toggleCookieModalPages(): void {
private toggleCookieModalPages(): void {
this.getDomEl("bannerContent").classList.toggle("d-none");
this.getDomEl("modalContainer").classList.toggle("d-none");
this.getDomEl("bannerContainer").classList.toggle("cn-modal-page");
@ -694,7 +616,7 @@ export default class CookieNotice {
}
/** Toggle customize btn */
toggleCustomizeBtn(): void {
private toggleCustomizeBtn(): void {
const customizeBtn = this.getDomEl("customizeBtn"),
customizeBtnBannerDataset = this.getDomEl("customizeBtn").dataset;
@ -711,7 +633,7 @@ export default class CookieNotice {
}
/** Get the actual page: banner or modal (with cookie choices) */
isOnBannerPage(): boolean {
private isOnBannerPage(): boolean {
return this.getDomEl("modalContainer").classList.contains("d-none");
}

View file

@ -0,0 +1,54 @@
import { CookiePolicies } from "../type";
export abstract class DocumentCookie {
public static has(cookieName: string): boolean {
return this.getCookies().has(cookieName);
}
public static get(cookieName: string): string|undefined {
return this.getCookies().get(cookieName);
}
public static set(
cookieName: string,
value: string,
date: Date | null = null,
cookiePolicies: CookiePolicies | null = null
): void {
let cookieValue = `${cookieName}=${value};path=/${
location.hostname ? `;domain=${location.hostname}` : ""
}`;
if (date) {
cookieValue += `;${date.toUTCString()}`;
}
if (cookiePolicies) {
cookieValue += `;${Object.entries(cookiePolicies)
.map(([policyName, policyValue]) => `${policyName}=${policyValue}`)
.join(";")}`;
}
document.cookie = cookieValue;
}
/**
* Set an older expire parameter to a specific cookie
*/
public static markForDeletion(cookieName: string): void {
if (DocumentCookie.has(cookieName)) {
console.debug(`No document cookie named ${cookieName}`);
}
// * Suppression du cookie et de sa version precedé d'un point
document.cookie = `${cookieName}=;path=/${
location.hostname ? `;domain=.${location.hostname}` : ""
};expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
document.cookie = `${cookieName}=;path=/${
location.hostname ? `;domain=${location.hostname}` : ""
};expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
}
/** Fetch browser cookies except(http-only ones) the cannot be handled using js */
private static getCookies(): Map<string,string> {
return new Map<string,string>(Object.fromEntries(document.cookie.split("; ")
.map(c => c.split("=")).filter(c => c[1] !== undefined)));
}
}

View file

@ -2,7 +2,7 @@ import type { CookiePolicySameSite, CookieStatus } from "./enums";
export type CookieStatusList = Record<string, CookieStatus>;
export type CookiePolicy = {
export type CookiePolicies = {
sameSite: CookiePolicySameSite;
secure: string;
};