import { ElementCoordinate } from './drawTabbableOnCanvas.interfaces' ; import { CircleOptions , LineOptions , TabbableOptions } from '../commands/tabbable.interfaces' ; /**
 * This method is based on this blog post
 * https://vivrichards.co.uk/accessibility/automating-page-tab-flows-using-visual-testing-and-javascript
 * by Viv Richards and optimized for using Canvas
 */ export default function drawTabbableOnCanvas ( drawOptions ) { window . scrollTo ( 0 , 0 ) ; const width = window . innerWidth ; const height = getDocumentScrollHeight ( ) ; const canvasNode = `<canvas id="wic-tabbable-canvas" width="${ width }" height="${ height }" style="position:absolute;top:0;left:0;z-index:999999;">` ; document . body . insertAdjacentHTML ( 'afterbegin' , canvasNode ) ; const accessibleElements = tabbable ( ) ; const elementCoordinates = accessibleElements . map ( node => { const currentElement = node . getBoundingClientRect ( ) ; return { x : currentElement . left + ( currentElement . width / 2 ) , y : currentElement . top + ( currentElement . height / 2 ) , } ; } ) ; elementCoordinates . unshift ( { x : 0 , y : 0 } ) ; elementCoordinates . forEach ( ( elementCoordinate , i ) => { if ( i === 0 ) { return ; } drawLine ( drawOptions . line , elementCoordinates [ i - 1 ] , elementCoordinate ) ; drawCircleAndNumber ( drawOptions . circle , elementCoordinate , i ) ; } ) ; /**
     * Draw a line
     */ function drawLine ( options , start , end ) { const tabbableCanvasContext = ( < HTMLCanvasElement > document . getElementById ( 'wic-tabbable-canvas' ) ) . getContext ( '2d' ) ; tabbableCanvasContext . beginPath ( ) ; tabbableCanvasContext . globalCompositeOperation = 'destination-over' ; tabbableCanvasContext . lineWidth = options . width ; tabbableCanvasContext . strokeStyle = options . color ; tabbableCanvasContext . moveTo ( start . x , start . y ) ; tabbableCanvasContext . lineTo ( end . x , end . y ) ; tabbableCanvasContext . stroke ( ) ; } /**
     * Draw a circle
     */ function drawCircleAndNumber ( options , position , i ) { const tabbableCanvasContext = ( < HTMLCanvasElement > document . getElementById ( 'wic-tabbable-canvas' ) ) . getContext ( '2d' ) ; tabbableCanvasContext . beginPath ( ) ; tabbableCanvasContext . globalCompositeOperation = 'source-over' ; tabbableCanvasContext . fillStyle = options . backgroundColor ; tabbableCanvasContext . arc ( position . x , position . y , options . size , 0 , Math . PI * 2 , true ) ; tabbableCanvasContext . fill ( ) ; tabbableCanvasContext . lineWidth = options . borderWidth ; tabbableCanvasContext . strokeStyle = options . borderColor ; tabbableCanvasContext . stroke ( ) ; if ( options . showNumber ) { tabbableCanvasContext . font = `${ options . fontSize }px ${ options . fontFamily }` ; tabbableCanvasContext . textAlign = 'center' ; tabbableCanvasContext . textBaseline = 'middle' ; tabbableCanvasContext . fillStyle = options . fontColor ; tabbableCanvasContext . fillText ( i . toString ( ) , position . x , position . y ) ; } } /**
     * Below code is coming from https://github.com/davidtheclark/tabbable
     * and is modified a bit to work inside the browser.
     * The original module couldn't be used for injection and didn't support TypeScript
     */ /**
     * Get all tabbable elements based on tabindex and then regular dom order
     */ function tabbable ( ) { let regularTabbables = [ ] ; let orderedTabbables = [ ] ; const candidateSelectors = [ 'input' , 'select' , 'textarea' , 'a[href]' , 'button' , '[tabindex]' , 'audio[controls]' , 'video[controls]' , '[contenteditable]:not([contenteditable="false"])' , ] . join ( ',' ) ; let candidates = document . querySelectorAll ( candidateSelectors ) ; for ( let i = 0 ; i < candidates . length ; i ++ ) { const candidate = candidates [ i ] ; if ( ! isNodeMatchingSelectorTabbable ( candidate ) ) { continue ; } const candidateTabindex = getTabindex ( candidate ) ; if ( candidateTabindex === 0 ) { regularTabbables . push ( candidate ) ; } else { orderedTabbables . push ( { documentOrder : i , tabIndex : candidateTabindex , node : candidate , } ) ; } } return Array . prototype . slice . call ( orderedTabbables . sort ( < any > sortOrderedTabbables ) . map ( a => a . node ) . concat ( regularTabbables ) ) ; } /**
     * Is the node tabbable
     */ function isNodeMatchingSelectorTabbable ( node ) { return ! ( ! isNodeMatchingSelectorFocusable ( node ) || isNonTabbableRadio ( node ) || getTabindex ( node ) < 0 ) ; } /**
     * Check if the node has a focused state
     */ function isNodeMatchingSelectorFocusable ( node ) { return ! ( ( node . hasAttribute ( 'disabled' ) || node . getAttribute ( 'disabled' ) ) || isHiddenInput ( node ) || isHidden ( node ) ) ; } /**
     * Get the tab index of the node
     */ function getTabindex ( node ) { const tabindexAttr = parseInt ( node . getAttribute ( 'tabindex' ) , 10 ) ; if ( ! isNaN ( tabindexAttr ) ) { return tabindexAttr ; } if ( isContentEditable ( node ) ) { return 0 ; } return node . tabIndex ; } /**
     * Return ordered tabbable nodes
     */ function sortOrderedTabbables ( nodeA , nodeB ) { return nodeA . tabIndex === nodeB . tabIndex ? ( < any > nodeA ) . documentOrder - ( < any > nodeB ) . documentOrder : nodeA . tabIndex - nodeB . tabIndex ; } /**
     * Is the content editable
     */ function isContentEditable ( node ) { return node . contentEditable === 'true' ; } /**
     * Is the node an input
     */ function isInput ( node ) { return node . tagName === 'INPUT' ; } /**
     * Is the input hidden
     */ function isHiddenInput ( node ) { return isInput ( node ) && ( < HTMLInputElement > node ) . type === 'hidden' ; } /**
     * Is the node a radio input
     */ function isRadio ( node ) { return isInput ( node ) && ( < HTMLInputElement > node ) . type === 'radio' ; } /**
     * Is the node a radio input and can it be tabbed
     */ function isNonTabbableRadio ( node ) { return isRadio ( node ) && ! isTabbableRadio ( < HTMLInputElement > node ) ; } /**
     * Get the checked radio input
     */ function getCheckedRadio ( nodes ) { for ( let i = 0 ; i < nodes . length ; i ++ ) { if ( nodes [ i ] . checked ) { return nodes [ i ] ; } } } /**
     * Is the radio input tabbable
     */ function isTabbableRadio ( node ) { if ( ! node . name ) { return true ; } let radioSet = ( < any > node . ownerDocument ) . querySelectorAll ( `input[type="radio"][name="${ node . name }"]` ) ; let checked = getCheckedRadio ( radioSet ) ; return ! checked || checked === node ; } /**
     * Is the node hidden
     */ function isHidden ( node ) { return ( node . offsetParent === null || getComputedStyle ( node ) . visibility === 'hidden' ) ; } /**
     * Get the document scroll height
     */ function getDocumentScrollHeight ( ) { const viewPortHeight = Math . max ( document . documentElement . clientHeight , window . innerHeight || 0 ) ; const scrollHeight = document . documentElement . scrollHeight ; const bodyScrollHeight = document . body . scrollHeight ; if ( ( viewPortHeight === scrollHeight ) && ( bodyScrollHeight > scrollHeight ) ) { return bodyScrollHeight ; } let pageHeight = 0 ; let largestNodeElement = document . querySelector ( 'body' ) ; if ( bodyScrollHeight === scrollHeight && bodyScrollHeight === viewPortHeight ) { findHighestNode ( document . documentElement . childNodes ) ; return pageHeight + largestNodeElement . getBoundingClientRect ( ) . top ; } return scrollHeight ; /**
         * Find the largest html element on the page
         */ function findHighestNode ( nodesList ) { for ( let i = nodesList . length - 1 ; i >= 0 ; i -- ) { const currentNode = nodesList [ i ] ; if ( currentNode . scrollHeight && currentNode . clientHeight ) { const elHeight = Math . max ( currentNode . scrollHeight , currentNode . clientHeight ) ; pageHeight = Math . max ( elHeight , pageHeight ) ; if ( elHeight === pageHeight ) { largestNodeElement = currentNode ; } } if ( currentNode . childNodes . length ) { findHighestNode ( currentNode . childNodes ) ; } } } } }