luhn-generator
Version:
A generator of numbers that passes the validation of Luhn algorithm or Luhn formula, also known as the 'modulus 10' or 'mod 10' algorithm
515 lines (440 loc) • 14.9 kB
JavaScript
import isVisible from './is-visible';
import VirtualNode from '../../core/base/virtual-node/virtual-node';
import { getNodeFromTree, getScroll, isShadowRoot } from '../../core/utils';
// split the page cells to group elements by the position
const gridSize = 200; // arbitrary size, increase to reduce memory use (less cells) but increase time (more nodes per grid to check collision)
/**
* Determine if node produces a stacking context.
* References:
* https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context
* https://github.com/gwwar/z-context/blob/master/devtools/index.js
* @param {VirtualNode} vNode
* @return {Boolean}
*/
function isStackingContext(vNode, parentVNode) {
const position = vNode.getComputedStylePropertyValue('position');
const zIndex = vNode.getComputedStylePropertyValue('z-index');
// the root element (HTML) is skipped since we always start with a
// stacking order of [0]
// position: fixed or sticky
if (position === 'fixed' || position === 'sticky') {
return true;
}
// positioned (absolutely or relatively) with a z-index value other than "auto",
if (zIndex !== 'auto' && position !== 'static') {
return true;
}
// elements with an opacity value less than 1.
if (vNode.getComputedStylePropertyValue('opacity') !== '1') {
return true;
}
// elements with a transform value other than "none"
const transform =
vNode.getComputedStylePropertyValue('-webkit-transform') ||
vNode.getComputedStylePropertyValue('-ms-transform') ||
vNode.getComputedStylePropertyValue('transform') ||
'none';
if (transform !== 'none') {
return true;
}
// elements with a mix-blend-mode value other than "normal"
const mixBlendMode = vNode.getComputedStylePropertyValue('mix-blend-mode');
if (mixBlendMode && mixBlendMode !== 'normal') {
return true;
}
// elements with a filter value other than "none"
const filter = vNode.getComputedStylePropertyValue('filter');
if (filter && filter !== 'none') {
return true;
}
// elements with a perspective value other than "none"
const perspective = vNode.getComputedStylePropertyValue('perspective');
if (perspective && perspective !== 'none') {
return true;
}
// element with a clip-path value other than "none"
const clipPath = vNode.getComputedStylePropertyValue('clip-path');
if (clipPath && clipPath !== 'none') {
return true;
}
// element with a mask value other than "none"
const mask =
vNode.getComputedStylePropertyValue('-webkit-mask') ||
vNode.getComputedStylePropertyValue('mask') ||
'none';
if (mask !== 'none') {
return true;
}
// element with a mask-image value other than "none"
const maskImage =
vNode.getComputedStylePropertyValue('-webkit-mask-image') ||
vNode.getComputedStylePropertyValue('mask-image') ||
'none';
if (maskImage !== 'none') {
return true;
}
// element with a mask-border value other than "none"
const maskBorder =
vNode.getComputedStylePropertyValue('-webkit-mask-border') ||
vNode.getComputedStylePropertyValue('mask-border') ||
'none';
if (maskBorder !== 'none') {
return true;
}
// elements with isolation set to "isolate"
if (vNode.getComputedStylePropertyValue('isolation') === 'isolate') {
return true;
}
// transform or opacity in will-change even if you don't specify values for these attributes directly
const willChange = vNode.getComputedStylePropertyValue('will-change');
if (willChange === 'transform' || willChange === 'opacity') {
return true;
}
// elements with -webkit-overflow-scrolling set to "touch"
if (
vNode.getComputedStylePropertyValue('-webkit-overflow-scrolling') ===
'touch'
) {
return true;
}
// element with a contain value of "layout" or "paint" or a composite value
// that includes either of them (i.e. contain: strict, contain: content).
const contain = vNode.getComputedStylePropertyValue('contain');
if (['layout', 'paint', 'strict', 'content'].includes(contain)) {
return true;
}
// a flex item or gird item with a z-index value other than "auto", that is the parent element display: flex|inline-flex|grid|inline-grid,
if (zIndex !== 'auto' && parentVNode) {
const parentDsiplay = parentVNode.getComputedStylePropertyValue('display');
if (
[
'flex',
'inline-flex',
'inline flex',
'grid',
'inline-grid',
'inline grid'
].includes(parentDsiplay)
) {
return true;
}
}
return false;
}
/**
* Check if a node or one of it's parents is floated.
* Floating position should be inherited from the parent tree
* @see https://github.com/dequelabs/axe-core/issues/2222
*/
function isFloated(vNode) {
if (!vNode) {
return false;
}
if (vNode._isFloated !== undefined) {
return vNode._isFloated;
}
const floatStyle = vNode.getComputedStylePropertyValue('float');
if (floatStyle !== 'none') {
vNode._isFloated = true;
return true;
}
const floated = isFloated(vNode.parent);
vNode._isFloated = floated;
return floated;
}
/**
* Return the index order of how to position this element. return nodes in non-positioned, floating, positioned order
* References:
* https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_without_z-index
* https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/Stacking_and_float
* https://drafts.csswg.org/css2/visuren.html#layers
* @param {VirtualNode} vNode
* @return {Number}
*/
function getPositionOrder(vNode) {
if (vNode.getComputedStylePropertyValue('position') === 'static') {
// 5. the in-flow, inline-level, non-positioned descendants, including inline tables and inline blocks.
if (
vNode.getComputedStylePropertyValue('display').indexOf('inline') !== -1
) {
return 2;
}
// 4. the non-positioned floats.
if (isFloated(vNode)) {
return 1;
}
// 3. the in-flow, non-inline-level, non-positioned descendants.
return 0;
}
// 6. the child stacking contexts with stack level 0 and the positioned descendants with stack level 0.
return 3;
}
/**
* Visually sort nodes based on their stack order
* References:
* https://drafts.csswg.org/css2/visuren.html#layers
* @param {VirtualNode}
* @param {VirtualNode}
*/
function visuallySort(a, b) {
/*eslint no-bitwise: 0 */
for (let i = 0; i < a._stackingOrder.length; i++) {
if (typeof b._stackingOrder[i] === 'undefined') {
return -1;
}
// 7. the child stacking contexts with positive stack levels (least positive first).
if (b._stackingOrder[i] > a._stackingOrder[i]) {
return 1;
}
// 2. the child stacking contexts with negative stack levels (most negative first).
if (b._stackingOrder[i] < a._stackingOrder[i]) {
return -1;
}
}
// nodes are the same stacking order
let aNode = a.actualNode;
let bNode = b.actualNode;
// elements don't correctly calculate document position when comparing
// across shadow boundaries, so we need to compare the position of a
// shared host instead
// elements have different hosts
if (aNode.getRootNode && aNode.getRootNode() !== bNode.getRootNode()) {
// keep track of all parent hosts and find the one both nodes share
const boundaries = [];
while (aNode) {
boundaries.push({
root: aNode.getRootNode(),
node: aNode
});
aNode = aNode.getRootNode().host;
}
while (
bNode &&
!boundaries.find(boundary => boundary.root === bNode.getRootNode())
) {
bNode = bNode.getRootNode().host;
}
// bNode is a node that shares a host with some part of the a parent
// shadow tree, find the aNode that shares the same host as bNode
aNode = boundaries.find(boundary => boundary.root === bNode.getRootNode())
.node;
// sort child of shadow to it's host node by finding which element is
// the child of the host and sorting it before the host
if (aNode === bNode) {
return a.actualNode.getRootNode() !== aNode.getRootNode() ? -1 : 1;
}
}
const {
DOCUMENT_POSITION_FOLLOWING,
DOCUMENT_POSITION_CONTAINS,
DOCUMENT_POSITION_CONTAINED_BY
} = window.Node;
const docPosition = aNode.compareDocumentPosition(bNode);
const DOMOrder = docPosition & DOCUMENT_POSITION_FOLLOWING ? 1 : -1;
const isDescendant =
docPosition & DOCUMENT_POSITION_CONTAINS ||
docPosition & DOCUMENT_POSITION_CONTAINED_BY;
const aPosition = getPositionOrder(a);
const bPosition = getPositionOrder(b);
// a child of a positioned element should also be on top of the parent
if (aPosition === bPosition || isDescendant) {
return DOMOrder;
}
return bPosition - aPosition;
}
/**
* Determine the stacking order of an element. The stacking order is an array of
* zIndex values for each stacking context parent.
* @param {VirtualNode}
* @return {Number[]}
*/
function getStackingOrder(vNode, parentVNode) {
const stackingOrder = parentVNode._stackingOrder.slice();
const zIndex = vNode.getComputedStylePropertyValue('z-index');
if (zIndex !== 'auto') {
stackingOrder[stackingOrder.length - 1] = parseInt(zIndex);
}
if (isStackingContext(vNode, parentVNode)) {
stackingOrder.push(0);
}
return stackingOrder;
}
/**
* Return the parent node that is a scroll region.
* @param {VirtualNode}
* @return {VirtualNode|null}
*/
function findScrollRegionParent(vNode, parentVNode) {
let scrollRegionParent = null;
let checkedNodes = [vNode];
while (parentVNode) {
if (parentVNode._scrollRegionParent) {
scrollRegionParent = parentVNode._scrollRegionParent;
break;
}
if (getScroll(parentVNode.actualNode)) {
scrollRegionParent = parentVNode;
break;
}
checkedNodes.push(parentVNode);
parentVNode = getNodeFromTree(
parentVNode.actualNode.parentElement || parentVNode.actualNode.parentNode
);
}
// cache result of parent scroll region so we don't have to look up the entire
// tree again for a child node
checkedNodes.forEach(
vNode => (vNode._scrollRegionParent = scrollRegionParent)
);
return scrollRegionParent;
}
/**
* Add a node to every cell of the grid it intersects with.
* @param {Grid}
* @param {VirtualNode}
*/
function addNodeToGrid(grid, vNode) {
// save a reference to where this element is in the grid so we
// can find it even if it's in a subgrid
vNode._grid = grid;
vNode.clientRects.forEach(rect => {
const x = rect.left;
const y = rect.top;
// "| 0" is a faster way to do Math.floor
// @see https://jsperf.com/math-floor-vs-math-round-vs-parseint/152
const startRow = (y / gridSize) | 0;
const startCol = (x / gridSize) | 0;
const endRow = ((y + rect.height) / gridSize) | 0;
const endCol = ((x + rect.width) / gridSize) | 0;
for (let row = startRow; row <= endRow; row++) {
grid.cells[row] = grid.cells[row] || [];
for (let col = startCol; col <= endCol; col++) {
grid.cells[row][col] = grid.cells[row][col] || [];
if (!grid.cells[row][col].includes(vNode)) {
grid.cells[row][col].push(vNode);
}
}
}
});
}
/**
* Setup the 2d grid and add every element to it, even elements not
* included in the flat tree
*/
export function createGrid(
root = document.body,
rootGrid = {
container: null,
cells: []
},
parentVNode = null
) {
// by not starting at the htmlElement we don't have to pass a custom
// filter function into the treeWalker to filter out head elements,
// which would be called for every node
if (!parentVNode) {
let vNode = getNodeFromTree(document.documentElement);
if (!vNode) {
vNode = new VirtualNode(document.documentElement);
}
vNode._stackingOrder = [0];
addNodeToGrid(rootGrid, vNode);
if (getScroll(vNode.actualNode)) {
const subGrid = {
container: vNode,
cells: []
};
vNode._subGrid = subGrid;
}
}
// IE11 requires the first 3 parameters
// @see https://developer.mozilla.org/en-US/docs/Web/API/Document/createTreeWalker
const treeWalker = document.createTreeWalker(
root,
window.NodeFilter.SHOW_ELEMENT,
null,
false
);
let node = parentVNode ? treeWalker.nextNode() : treeWalker.currentNode;
while (node) {
let vNode = getNodeFromTree(node);
// an svg in IE11 does not have a parentElement but instead has a
// parentNode. but parentNode could be a shadow root so we need to
// verity it's in the tree first
if (node.parentElement) {
parentVNode = getNodeFromTree(node.parentElement);
} else if (node.parentNode && getNodeFromTree(node.parentNode)) {
parentVNode = getNodeFromTree(node.parentNode);
}
if (!vNode) {
vNode = new axe.VirtualNode(node, parentVNode);
}
vNode._stackingOrder = getStackingOrder(vNode, parentVNode);
const scrollRegionParent = findScrollRegionParent(vNode, parentVNode);
const grid = scrollRegionParent ? scrollRegionParent._subGrid : rootGrid;
if (getScroll(vNode.actualNode)) {
const subGrid = {
container: vNode,
cells: []
};
vNode._subGrid = subGrid;
}
// filter out any elements with 0 width or height
// (we don't do this before so we can calculate stacking context
// of parents with 0 width/height)
const rect = vNode.boundingClientRect;
if (rect.width !== 0 && rect.height !== 0 && isVisible(node)) {
addNodeToGrid(grid, vNode);
}
// add shadow root elements to the grid
if (isShadowRoot(node)) {
createGrid(node.shadowRoot, grid, vNode);
}
node = treeWalker.nextNode();
}
}
export function getRectStack(grid, rect, recursed = false) {
// use center point of rect
let x = rect.left + rect.width / 2;
let y = rect.top + rect.height / 2;
// NOTE: there is a very rare edge case in Chrome vs Firefox that can
// return different results of `document.elementsFromPoint`. If the center
// point of the element is <1px outside of another elements bounding rect,
// Chrome appears to round the number up and return the element while Firefox
// keeps the number as is and won't return the element. In this case, we
// went with pixel perfect collision rather than rounding
const row = (y / gridSize) | 0;
const col = (x / gridSize) | 0;
let stack = grid.cells[row][col].filter(gridCellNode => {
return gridCellNode.clientRects.find(clientRect => {
let rectX = clientRect.left;
let rectY = clientRect.top;
// perform an AABB (axis-aligned bounding box) collision check for the
// point inside the rect
return (
x <= rectX + clientRect.width &&
x >= rectX &&
y <= rectY + clientRect.height &&
y >= rectY
);
});
});
const gridContainer = grid.container;
if (gridContainer) {
stack = getRectStack(
gridContainer._grid,
gridContainer.boundingClientRect,
true
).concat(stack);
}
if (!recursed) {
stack = stack
.sort(visuallySort)
.map(vNode => vNode.actualNode)
// always make sure html is the last element
.concat(document.documentElement)
// remove duplicates caused by adding client rects of the same node
.filter((node, index, array) => array.indexOf(node) === index);
}
return stack;
}