Skip to content

Instantly share code, notes, and snippets.

@jonathantneal
Last active December 7, 2022 14:35
Focus Starting Point

Focus Starting Point

Focus Starting Point lets you access and observe the focus starting point.

The library is 989 bytes, 554 bytes gzipped.

Example Usage

const focusStartingPoint = new FocusStartingPoint()

focusStartingPoint.addEventListener('change', event => {
  console.group('focus navigation point')
  console.log('old:', event.oldNode, event.oldOffset)
  console.log('new:', event.newNode, event.newOffset)
  console.groupEnd()
})
/** Focus Starting Point @version 0.1.2 @license CC0-1.0 @author Jonathan Neal (https://github.com/jonathantneal) */
let weakTarget = new WeakMap
let weakOffset = new WeakMap
export class FocusStartingPoint extends EventTarget {
constructor(window = globalThis) {
super()
let { document, history } = window
// @ts-expect-error "Property 'caretPositionFromPoint' does not exist on type 'Document'."
let { caretPositionFromPoint, body } = document
let compareTarget = (/** @type {Node} */ seekingTarget, /** @type {number} */ seekingOffset) => {
if (
currentTarget !== seekingTarget
|| currentOffset !== seekingOffset
) {
weakTarget.set(this, seekingTarget)
weakOffset.set(this, seekingOffset)
this.dispatchEvent(Object.assign(new Event('change'), {
oldNode: currentTarget,
newNode: seekingTarget,
oldOffset: currentOffset,
newOffset: seekingOffset,
}))
currentTarget = seekingTarget
currentOffset = seekingOffset
}
}
let currentTarget = /** @type {Node} */ (document.activeElement)
let currentOffset = 0
let historyLength = history.length
/** @type {Element} */
let element
weakTarget.set(this, currentTarget)
weakOffset.set(this, currentOffset)
if (!caretPositionFromPoint) {
caretPositionFromPoint = (/** @type {number} */ clientX, /** @type {number} */ clientY) => {
let range = /** @type {Range} */ (document.caretRangeFromPoint(clientX, clientY))
let offsetNode = range.startContainer
let offset = range.startOffset
let boundaryRect = range.selectNode(offsetNode) || range.getBoundingClientRect()
if (
offsetNode.nodeType === 1
|| clientX < boundaryRect.left
|| clientX > boundaryRect.right
|| clientY < boundaryRect.top
|| clientY > boundaryRect.bottom
) {
offsetNode = element
offset = 0
}
return { offsetNode, offset }
}
}
window.addEventListener('focusin', () => {
compareTarget(/** @type {Element} */(document.activeElement), 0)
}, true)
window.addEventListener('hashchange', () => {
if (historyLength !== (historyLength = history.length)) {
compareTarget(document.querySelector(':target') || body, 0)
}
}, true)
window.addEventListener('pointerdown', event => {
element = /** @type {Element} */ (document.elementFromPoint(event.clientX, event.clientY))
let { offsetNode, offset } = caretPositionFromPoint.call(document, event.clientX, event.clientY)
offsetNode.contains(element)
? compareTarget(element, 0)
: compareTarget(offsetNode, offset)
}, true)
}
get node() {
return weakTarget.get(this)
}
get offset() {
return weakOffset.get(this)
}
}
let e=new WeakMap,t=new WeakMap;export class FocusStartingPoint extends EventTarget{constructor(n=globalThis){super();let o,{document:s,history:i}=n,{caretPositionFromPoint:r,body:a}=s,l=(n,o)=>{c===n&&d===o||(e.set(this,n),t.set(this,o),this.dispatchEvent(Object.assign(new Event('change'),{oldNode:c,newNode:n,oldOffset:d,newOffset:o})),c=n,d=o)},c=s.activeElement,d=0,f=i.length;e.set(this,c),t.set(this,d),r||(r=(e,t)=>{let n=s.caretRangeFromPoint(e,t),i=n.startContainer,r=n.startOffset,a=n.selectNode(i)||n.getBoundingClientRect();return(1===i.nodeType||e<a.left||e>a.right||t<a.top||t>a.bottom)&&(i=o,r=0),{offsetNode:i,offset:r}}),n.addEventListener('focusin',(()=>{l(s.activeElement,0)}),!0),n.addEventListener('hashchange',(()=>{f!==(f=i.length)&&l(s.querySelector(':target')||a,0)}),!0),n.addEventListener('pointerdown',(e=>{o=s.elementFromPoint(e.clientX,e.clientY);let{offsetNode:t,offset:n}=r.call(s,e.clientX,e.clientY);t.contains(o)?l(o,0):l(t,n)}),!0)}get node(){return e.get(this)}get offset(){return t.get(this)}}
{let e=new WeakMap,t=new WeakMap;globalThis.FocusStartingPoint=class extends EventTarget{constructor(n=globalThis){super();let o,{document:s,history:i}=n,{caretPositionFromPoint:a,body:r}=s,l=(n,o)=>{c===n&&d===o||(e.set(this,n),t.set(this,o),this.dispatchEvent(Object.assign(new Event('change'),{oldNode:c,newNode:n,oldOffset:d,newOffset:o})),c=n,d=o)},c=s.activeElement,d=0,f=i.length;e.set(this,c),t.set(this,d),a||(a=(e,t)=>{let n=s.caretRangeFromPoint(e,t),i=n.startContainer,a=n.startOffset,r=n.selectNode(i)||n.getBoundingClientRect();return(1===i.nodeType||e<r.left||e>r.right||t<r.top||t>r.bottom)&&(i=o,a=0),{offsetNode:i,offset:a}}),n.addEventListener('focusin',(()=>{l(s.activeElement,0)}),!0),n.addEventListener('hashchange',(()=>{f!==(f=i.length)&&l(s.querySelector(':target')||r,0)}),!0),n.addEventListener('pointerdown',(e=>{o=s.elementFromPoint(e.clientX,e.clientY);let{offsetNode:t,offset:n}=a.call(s,e.clientX,e.clientY);t.contains(o)?l(o,0):l(t,n)}),!0)}get node(){return e.get(this)}get offset(){return t.get(this)}}}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment