@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
177 lines (174 loc) • 7.39 kB
JavaScript
const registerDetectors = (targetExtensions, supportedDetectors) => {
const detectors = supportedDetectors.filter(registration => targetExtensions.includes(registration.name));
return detectors.map(detector => ({
name: detector.name,
state: {
detected: false
},
fns: {
async: detector.fns.async,
sync: detector.fns.sync
}
}));
};
const RACE_COMPLETE = 'race_complete';
const SELECTORS = {
GRAMMARLY: 'grammarly-extension, grammarly-popups, [data-grammarly-shadow-root]'
};
/**
* This is the official list of supported browser extension detectors. To add support
* for detecting an additional browser extension, simply add a (DetectorRegistration)
* object like below to the list:
*
* ```
* {
* name: 'exampleExtension',
* fns: {
* // a synchronous check that should return true if the extension is detected
* sync: () => !!document.querySelector(".some-example-class"),
* // an asynchronous check that should invoke 'detected' if the extension is detected.
* // it can also invoke 'cleanup' with a callback to schedule cleanup tasks
* // (such as disconnecting observers).
* async: (detected, cleanup) => {
* if (document.querySelector(".some-example-class")) {
* detected();
* }
* }
* }
* }
* ```
*/
const supportedDetectors = [{
name: 'grammarly',
fns: {
sync: () => {
var _document;
return Boolean((_document = document) === null || _document === void 0 ? void 0 : _document.querySelector(SELECTORS.GRAMMARLY));
},
async: (detected, cleanup) => {
var _document2;
// First check to see if grammarly already exists on page
const exists = Boolean((_document2 = document) === null || _document2 === void 0 ? void 0 : _document2.querySelector(SELECTORS.GRAMMARLY));
if (exists) {
detected();
}
// Otherwise, setup a mutation observer to observe the page and its children
// for newly added nodes. Collect observed mutations in a queue and in 1 second
// intervals either process the queue or schedule the processing task for when
// the user agent's main thread is idle (if possible).
let queue = [];
const processQueue = () => {
for (const mutations of queue) {
for (const mutation of mutations) {
var _mutation$addedNodes;
if ((mutation === null || mutation === void 0 ? void 0 : mutation.type) === 'childList' && mutation !== null && mutation !== void 0 && (_mutation$addedNodes = mutation.addedNodes) !== null && _mutation$addedNodes !== void 0 && _mutation$addedNodes.length) {
const exists = Array.from(mutation.addedNodes).some(node => {
var _node$parentElement;
return (_node$parentElement = node.parentElement) === null || _node$parentElement === void 0 ? void 0 : _node$parentElement.querySelector(SELECTORS.GRAMMARLY);
});
if (exists) {
detected();
}
}
}
}
queue = [];
};
const intervalId = setInterval(() => {
if (typeof window.requestIdleCallback === 'function') {
window.requestIdleCallback(processQueue);
} else {
window.requestAnimationFrame(processQueue);
}
}, 1000);
const observer = new MutationObserver(mutations => {
queue.push(mutations);
});
cleanup(() => {
queue = [];
clearInterval(intervalId);
observer === null || observer === void 0 ? void 0 : observer.disconnect();
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
}
}
}];
/**
* Call this to return a list (or a Promise of a list) of detected browser extensions.
*
* This function supports a **synchronous** and **asynchronous** mode through options. You
* must pass a list of the browser extension names you want to target for detection.
* Only UserBrowserExtension extensions are supported, other target names will be silently
* ignored.
*
* If the async option is enabled, you must also pass a final timeout by when it
* should stop all detection attempts and return any partially detected extensions.
*
* Example usage:
* ```
* // synchronously/immediately check for extensions
* const extensions = sniffUserBrowserExtensions({ extensions: ['grammarly', 'requestly'] });
* // result will be ['grammarly'] or ['grammarly','requestly'] or ['requestly'] or [];
*
* // asynchronously check for extensions up to 30s
* sniffUserBrowserExtensions({
* extensions: ['grammarly', 'requestly'],
* async: true,
* asyncTimeoutMs: 30000,
* }).then(extensions => {
* // result will be ['grammarly'] or ['grammarly','requestly'] or ['requestly'] or [];
* })
* ```
*/
export function sniffUserBrowserExtensions(options) {
try {
// First we filter out supported extensions that aren't requested through options. We also
// prepare detector objects with some initial internal state (e.g. detector.state.detected = false)
const detectors = registerDetectors(options.extensions, supportedDetectors);
// If async mode is enabled, we convert the list of detector objects to a list of promises
// that resolve when the detector invokes detected() during its asynchronous check.
// We also track any scheduled cleanup() tasks.
if (options.async === true) {
const asyncCleanups = [];
const asyncDetections = Promise.all(detectors.map(detector => {
return new Promise(resolve => {
const detected = () => {
detector.state.detected = true;
resolve();
};
const cleanup = cb => {
asyncCleanups.push(cb);
};
if (typeof detector.fns.async === 'function') {
detector.fns.async(detected, cleanup);
} else {
detector.state.detected = false;
resolve();
}
});
}));
// We race all asynchronous checkers against a user-defined timeout (asyncTimeoutMs).
// When asynchronous checks are finalised first,or if the timeout elapses, we return
// the list of extensions detected up until that point.
const globalTimeout = new Promise(resolve => setTimeout(resolve, options.asyncTimeoutMs, RACE_COMPLETE));
return Promise.race([asyncDetections, globalTimeout]).then(() => {
return detectors.filter(detector => detector.state.detected).map(detector => detector.name);
})
// If there are any errors, we fail safely and silently with zero detected extensions.
.catch(() => []).finally(() => asyncCleanups.map(cleanup => cleanup()));
} else {
// If sync mode, we immediately execute synchronous checks
// and return a list of extensions whose synchronous checks returned true.
return detectors.filter(detector => {
var _detector$fns$sync, _detector$fns;
return (_detector$fns$sync = (_detector$fns = detector.fns).sync) === null || _detector$fns$sync === void 0 ? void 0 : _detector$fns$sync.call(_detector$fns);
}).map(detector => detector.name);
}
} catch (err) {
// If there are any unhandled errors, we fail safely and silently with zero detected extensions.
return options.async ? Promise.resolve([]) : [];
}
}