UNPKG

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
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; }