Skip to content

Commit

Permalink
resolve conflict & merge with 1.7
Browse files Browse the repository at this point in the history
  • Loading branch information
stanislav-atr committed Oct 18, 2022
2 parents 0ee0f61 + e1489ee commit 23769ef
Show file tree
Hide file tree
Showing 13 changed files with 1,295 additions and 4 deletions.
30 changes: 30 additions & 0 deletions src/helpers/prepare-cookie.js → src/helpers/cookie-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,33 @@ export const prepareCookie = (name, value) => {

return cookieData;
};

/**
* Parses cookie string into object
* @param {string} cookieString string that conforms to document.cookie format
* @returns {Object} key:value object that corresponds with incoming cookies keys and values
*/
export const parseCookieString = (cookieString) => {
const COOKIE_DELIMITER = '=';
const COOKIE_PAIRS_DELIMITER = ';';

// Get raw cookies
const cookieChunks = cookieString.split(COOKIE_PAIRS_DELIMITER);
const cookieData = {};

cookieChunks.forEach((singleCookie) => {
let cookieKey;
let cookieValue;
const delimiterIndex = singleCookie.indexOf(COOKIE_DELIMITER);
if (delimiterIndex === -1) {
cookieKey = singleCookie.trim();
} else {
cookieKey = singleCookie.slice(0, delimiterIndex).trim();
cookieValue = singleCookie.slice(delimiterIndex + 1);
}
// Save cookie key=value data with null instead of empty ('') values
cookieData[cookieKey] = cookieValue || null;
});

return cookieData;
};
2 changes: 1 addition & 1 deletion src/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export * from './observer';
export * from './match-stack';
export * from './open-shadow-dom-utils';
export * from './array-utils';
export * from './prepare-cookie';
export * from './cookie-utils';
export * from './number-utils';
export * from './adjust-set-utils';
export * from './fetch-utils';
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/string-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export const startsWith = (str, prefix) => {
export const endsWith = (str, ending) => {
// if str === '', (str && false) will return ''
// that's why it has to be !!str
return !!str && str.indexOf(ending) === str.length - ending.length;
return !!str && str.lastIndexOf(ending) === str.length - ending.length;
};

export const substringAfter = (str, separator) => {
Expand Down
3 changes: 2 additions & 1 deletion src/redirects/pardot-1.0.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import {
* @redirect pardot-1.0
*
* @description
* Mocks the pd.js file of Salesforce
* Mocks the pd.js file of Salesforce.
* https://pi.pardot.com/pd.js
* https://developer.salesforce.com/docs/marketing/pardot/overview
*
* **Example**
* ```
* ||pi.pardot.com/pd.js$script,redirect=pardot
Expand Down
2 changes: 2 additions & 0 deletions src/scriptlets/scriptlets-list.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* This file must export all scriptlets which should be accessible
*/
export * from './trusted-click-element';
export * from './abort-on-property-read';
export * from './abort-on-property-write';
export * from './prevent-setTimeout';
Expand Down Expand Up @@ -48,3 +49,4 @@ export * from './prevent-refresh';
export * from './prevent-element-src-loading';
export * from './no-topics';
export * from './trusted-replace-xhr-response';
export * from './xml-prune';
301 changes: 301 additions & 0 deletions src/scriptlets/trusted-click-element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
import {
hit,
toRegExp,
parseCookieString,
} from '../helpers/index';

/* eslint-disable max-len */
/**
* @scriptlet trusted-click-element
*
* @description
* Clicks selected elements in a strict sequence, ordered by selectors passed, and waiting for them to render in the DOM first.
* Deactivates after all elements have been clicked or by 10s timeout.
*
* **Syntax**
* ```
* example.com#%#//scriptlet('trusted-click-element', selectors[, extraMatch[, delay]])
* ```
*
* - `selectors` — required, string with query selectors delimited by comma
* - `extraMatch` — optional, extra condition to check on a page; allows to match `cookie` and `localStorage`; can be set as `name:key[=value]` where `value` is optional.
* Multiple conditions are allowed inside one `extraMatch` but they should be delimited by comma and each of them should match the syntax. Possible `name`s:
* - `cookie` - test string or regex against cookies on a page
* - `localStorage` - check if localStorage item is present
* - 'delay' - optional, time in ms to delay scriptlet execution, defaults to instant execution.
* **Examples**
* 1. Click single element by selector
* ```
* example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"]')
* ```
*
* 2. Delay click execution by 500ms
* ```
* example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"]', '', '500')
* ```
*
* 3. Click multiple elements by selector with a delay
* ```
* example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"], button[name='check"], input[type="submit"][value="akkoord"]', '', '500')
* ```
*
* 4. Match cookies by keys using regex and string
* ```
* example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"]', 'cookie:userConsentCommunity, cookie:/cmpconsent|cmp/')
* ```
*
* 5. Match by cookie key=value pairs using regex and string
* ```
* example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"]', 'cookie:userConsentCommunity=true, cookie:/cmpconsent|cmp/=/[a-z]{1,5}/')
* ```
*
* 6. Match by localStorage item 'promo' key
* ```
* example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"]', 'localStorage:promo')
* ```
*
* 7. Click multiple elements with delay and matching by both cookie string and localStorage item
* ```
* example.com#%#//scriptlet('trusted-click-element', 'button[name="agree"], input[type="submit"][value="akkoord"]', 'cookie:cmpconsent, localStorage:promo', '250')
* ```
*/
/* eslint-enable max-len */
export function trustedClickElement(source, selectors, extraMatch = '', delay = NaN) {
if (!selectors) {
return;
}
// eslint-disable-next-line no-console
const log = console.log.bind(console);

const OBSERVER_TIMEOUT_MS = 10000;
const THROTTLE_DELAY_MS = 20;
const COOKIE_MATCH_MARKER = 'cookie:';
const LOCAL_STORAGE_MATCH_MARKER = 'localStorage:';
const SELECTORS_DELIMITER = ',';
const COOKIE_STRING_DELIMITER = ';';
// Regex to split match pairs by commas, avoiding the ones included in regexes
const EXTRA_MATCH_DELIMITER = /(,\s*){1}(?=cookie:|localStorage:)/;

let parsedDelay;
if (delay) {
parsedDelay = parseInt(delay, 10);
const isValidDelay = !Number.isNaN(parsedDelay) || parsedDelay < OBSERVER_TIMEOUT_MS;
if (!isValidDelay) {
log(`Passed delay '${delay}' is invalid or bigger than ${OBSERVER_TIMEOUT_MS} ms`);
return;
}
}

let canClick = !parsedDelay;

const cookieMatches = [];
const localStorageMatches = [];

if (extraMatch) {
// Get all match marker:value pairs from argument
const parsedExtraMatch = extraMatch
.split(EXTRA_MATCH_DELIMITER)
.map((matchStr) => matchStr.trim());

// Filter match pairs by marker
parsedExtraMatch.forEach((matchStr) => {
if (matchStr.indexOf(COOKIE_MATCH_MARKER) > -1) {
const cookieMatch = matchStr.replace(COOKIE_MATCH_MARKER, '');
cookieMatches.push(cookieMatch);
}
if (matchStr.indexOf(LOCAL_STORAGE_MATCH_MARKER) > -1) {
const localStorageMatch = matchStr.replace(LOCAL_STORAGE_MATCH_MARKER, '');
localStorageMatches.push(localStorageMatch);
}
});
}

if (cookieMatches.length > 0) {
const parsedCookieMatches = parseCookieString(cookieMatches.join(COOKIE_STRING_DELIMITER));
const parsedCookies = parseCookieString(document.cookie);
const cookieKeys = Object.keys(parsedCookies);
if (cookieKeys.length === 0) {
return;
}

const cookiesMatched = Object.keys(parsedCookieMatches).every((key) => {
// Avoid getting /.?/ result from toRegExp on undefined
// as cookie may be set without value,
// on which cookie parsing will return cookieKey:undefined pair
const valueMatch = parsedCookieMatches[key] ? toRegExp(parsedCookieMatches[key]) : null;
const keyMatch = toRegExp(key);

return cookieKeys.some((key) => {
const keysMatched = keyMatch.test(key);
if (!keysMatched) {
return false;
}

// Key matching is enough if cookie value match is not specified
if (!valueMatch) {
return true;
}

return valueMatch.test(parsedCookies[key]);
});
});

if (!cookiesMatched) {
return;
}
}

if (localStorageMatches.length > 0) {
const localStorageMatched = localStorageMatches
.every((str) => {
const itemValue = window.localStorage.getItem(str);
return itemValue || itemValue === '';
});
if (!localStorageMatched) {
return;
}
}

/**
* Create selectors array and swap selectors to null on finding it's element
*
* Selectors / nulls should not be (re)moved from array to:
* - keep track of selectors order
* - always know on what index corresponding element should be put
* - prevent selectors from being queried multiple times
*/
let selectorsSequence = selectors
.split(SELECTORS_DELIMITER)
.map((selector) => selector.trim());

const createElementObj = (element) => {
return {
element: element || null,
clicked: false,
};
};
const elementsSequence = Array(selectorsSequence.length).fill(createElementObj());

/**
* Go through elementsSequence from left to right, clicking on found elements
*
* Element should not be clicked if it is already clicked,
* or a previous element is not found or clicked yet
*/
const clickElementsBySequence = () => {
for (let i = 0; i < elementsSequence.length; i += 1) {
const elementObj = elementsSequence[i];
// Stop clicking if that pos element is not found yet
if (!elementObj.element) {
break;
}
// Skip already clicked elements
if (!elementObj.clicked) {
elementObj.element.click();
elementObj.clicked = true;
}
}

const allElementsClicked = elementsSequence
.every((elementObj) => elementObj.clicked === true);
if (allElementsClicked) {
// At this stage observer is already disconnected
hit(source);
}
};

const handleElement = (element, i) => {
const elementObj = createElementObj(element);
elementsSequence[i] = elementObj;

if (canClick) {
clickElementsBySequence();
}
};

/**
* Query all selectors from queue on each mutation
* Each selector is swapped to null in selectorsSequence on founding corresponding element
*
* We start looking for elements before possible delay is over, to avoid cases
* when delay is getting off after the last mutation took place.
*
*/
const findElements = (mutations, observer) => {
const fulfilledSelectors = [];
selectorsSequence.forEach((selector, i) => {
if (!selector) {
return;
}
const element = document.querySelector(selector);
if (!element) {
return;
}

handleElement(element, i);
fulfilledSelectors.push(selector);
});

// selectorsSequence should be modified after the loop to not break loop indexation
selectorsSequence = selectorsSequence.map((selector) => {
return fulfilledSelectors.indexOf(selector) === -1 ? selector : null;
});

// Disconnect observer after finding all elements
const allSelectorsFulfilled = selectorsSequence.every((selector) => selector === null);
if (allSelectorsFulfilled) {
observer.disconnect();
}
};

const throttle = (cb, ms) => {
let wait = false;
let savedArgs;
const wrapper = (...args) => {
if (wait) {
savedArgs = args;
return;
}

cb(...args);
wait = true;

setTimeout(() => {
wait = false;
if (savedArgs) {
wrapper(savedArgs);
savedArgs = null;
}
}, ms);
};
return wrapper;
};

// eslint-disable-next-line compat/compat
const observer = new MutationObserver(throttle(findElements, THROTTLE_DELAY_MS));
observer.observe(document.documentElement, {
attributes: true,
childList: true,
subtree: true,
});

if (parsedDelay) {
setTimeout(() => {
// Click previously collected elements
clickElementsBySequence();
canClick = true;
}, parsedDelay);
}

setTimeout(() => observer.disconnect(), OBSERVER_TIMEOUT_MS);
}

trustedClickElement.names = [
'trusted-click-element',
];

trustedClickElement.injections = [
hit,
toRegExp,
parseCookieString,
];
Loading

0 comments on commit 23769ef

Please sign in to comment.