Skip to content

Instantly share code, notes, and snippets.

@daformat
Last active December 4, 2022 22:03
Show Gist options
  • Save daformat/8b93ff95701202d9e9c9f0a89c6ceace to your computer and use it in GitHub Desktop.
Save daformat/8b93ff95701202d9e9c9f0a89c6ceace to your computer and use it in GitHub Desktop.
Highlight hovered DOM node à la DevTools
(function () {
/*
This code is mostly a quick and somewhat dirty demo to explore the concept of selecting DOM nodes visually
It's far from being bulletproof and the code could definitely be cleaner.
Copy paste it into your browser javascript console then click back inside the document or hide
the devtools so you can use the keyboard shorctus to update selection, otherwise the keystrokes
will be captured by the console.
The support for text selection is still quite basic but it's included for demonstration purposes.
It is **not compatible with DOM traversal keyboard commands yet**
------------------------------------------------------------------------------------------------------------
Keyboard commands
------------------------------------------------------------------------------------------------------------
Keys | | Description
--------|------------------|--------------------------------------------------------------------------------
`⇥` | (tab) | Cycle through siblings, noop if no siblings are found
`⇧ ⇥` | (shift tab) | Reverse cycle through siblings
`⌥ ⇥` | (alt tab) | Select first visible ancestor, bubbling up in the tree
`⌥ ⇧ ⇥` | (alt shift tab) | Select first visible child, going deeper in the tree
`+` | (plus) | Add next sibling to selection
`-` | (minus) | Add previous sibling to selection
`⌥ +` | (alt plus) | Remove the last sibling from selection (if any were added via the `+` command)
`⌥ -` | (alt minus) | Remove leading sibling from selection (if any were added via the `-` command)
`esc` | (escape) | Remove the UI and detach event handlers, just like nothing ever happened
------------------------------------------------------------------------------------------------------------
Additionally, `return` logs the current selection to the console
------------------------------------------------------------------------------------------------------------
Gotchas:
- non-visible nodes heuristics may not catch all cases, though it should catch a lot already (see `isVisible()`)
- elements with an overflow set to scroll: should we select inner elements? Looks a bit weird to see the bbox
going outside the parent because of the position of the selected node(s)
- elements within containers with an overflow other than visible: should we crop the bbox based on the container's
- iframes - samePolicy bypass - google docs captures the keystrokes
- floats (wikipedia) - (chrome) devtools gives the same result as us
- canvas / svg tags: what about when those are generated by code? Should we just grab the current pixels / tree?
- videos, embeds: how to deal with those?
We should definitely handle the visual output differently because right now we're doing wrong things,
like animating width / height properties and using mix-blend-mode (FIXED), this allowed us not to bother about
implementation details and focus on the interaction part. Maybe we won't even need an overlay in the end,
but if we do, we could use canvas/svg to generate it, or a clip-mask, or... we'll see then.
Another thing is that what we do for now is we simply draw the selection's bounding box, but this bounding box
can be confusing when it includes other elements which are not inside the selection, even though they **look**
like they are because of the rectangular shape of the bounding box:
___________
| [ ] [X] | [X]: selected This happens when adding siblings in selection,
| [X] [ ] | [ ]: not selected or when using text-based selection
-----------
Scrolling is not performant enough, it's not always super reponsive depending on pages, there are a few ways
we could try to work around this but it's far from being critical for our prototype.
To be secure we should use a prefix + uuid based css class / id for everything so the risk of a collision
with the page's authored styles is negligible. Right now we only do this for the portals and active element
selection indicators. We should do this for everything, including <kbd>s.
Also, the css should include some form of reset to make sure our styles aren't messed up by
the existing page styles.
With that being said, it's already looking not too shabby.
*/
// Reference to the currently highlighted node
let activeElement;
const animationDurationMs = 80;
const animationDelayMs = 0;
const scrollAnimationDurationMs = Math.round(animationDurationMs / 3);
const scrollAnimationDelayMs = 0;
// The prefix we use for IDs and classes of the main elements we insert in the DOM
const prefix = 'beam--';
// Cheap uuid generator, not super strong, but fast and random enough for the proof of concept
// Crypto.getRandomValues(...) could be used, it's slower but we're only calling this a couple
// of time right after so performance shouldn't be an issue
const uuid = () => '8-4-4-4-12'.replace(
/\d+/g,
m => {
let u = '';
for(i = 0; i < m; i++) {
u += (Math.random() * 16 | 0).toString(16);
}
return u;
}
);
// Generate uuids
const uuidPortal = uuid();
const uuidPortalActive = uuid();
// Siblings before / after to include in selection
let siblingsBefore = 0;
let siblingsAfter = 0;
// Stylesheet creation
const styleElement = document.createElement('style');
document.head.appendChild(styleElement);
const sheet = styleElement.sheet;
// First portal with a mix-blend-mode set to multiply
const portal = document.createElement('div');
portal.id = `${prefix}${uuidPortal}`;
document.body.appendChild(portal);
sheet.insertRule(
`#${prefix}${uuidPortal} {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
pointer-events: none;
mix-blend-mode: multiply;
z-index: 2147483647;
}`,
sheet.length
);
// background-color overlay only when hovering the document
sheet.insertRule(
`body:hover #${prefix}${uuidPortal}{
background-color: rgba(0, 0, 50, 0.15);
}`,
sheet.length
);
// Highlight in white (-> multiply) the current selection
const activeMarker = document.createElement('div');
activeMarker.className = `${prefix}${uuidPortalActive}`;
portal.appendChild(activeMarker);
sheet.insertRule(
`.${prefix}${uuidPortalActive}{
--border: 0px;
--padding: 5px;
--animation-duration: ${animationDurationMs}ms;
opacity: 0;
position: absolute;
left: 0;
top: 0;
transform: translate(
calc(var(--active-x) - var(--border) - var(--padding)),
calc(var(--active-y) - var(--border) - var(--padding))
);
width: calc(var(--active-width) + 2 * var(--border) + 2 * var(--padding));
height: calc(var(--active-height) + 2 * var(--border) + 2 * var(--padding));
pointer-events: none;
box-sizing: border-box;
background: #FFF;
background-clip: padding-box;
border-style: solid;
border-width: var(--border);
border-color: transparent;
border-radius: 8px;
transition:
transform var(--animation-duration) ease-in-out,
opacity var(--animation-duration) ease-in-out,
width var(--animation-duration) ease-in-out,
height var(--animation-duration) ease-in-out
;
transition-delay: ${animationDelayMs}ms;
will-change: opacity, transform, width, height;
}`,
sheet.length
);
// Animate position faster on scroll, feels more natural
sheet.insertRule(
`.${prefix}${uuidPortalActive}.scroll{
transition-easing-function: linear;
transition-duration: ${scrollAnimationDurationMs}ms;
transition-delay: ${scrollAnimationDelayMs}ms;
}`,
sheet.length
);
// Prevent animating when using text-selection
sheet.insertRule(
`.${prefix}${uuidPortalActive}.select{
transition: none;
}`,
sheet.length
);
// Animate opacity on hovering the document
sheet.insertRule(
`body:hover .${prefix}${uuidPortalActive}{
opacity: 1;
}`,
sheet.length
);
// Second portal, without mix-blend-mode to create an solid outline as a fallback on dark background
// and to display available keyboard commands on the bottom right of the screen
const portal2 = document.createElement('div');
portal2.id = `${prefix}${uuidPortal}2`;
document.body.appendChild(portal2);
sheet.insertRule(
`#${prefix}${uuidPortal}2 {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
pointer-events: none;
z-index: 2147483647;
}`,
sheet.length
);
// The element representing the position of the current html node
const activeMarker2 = document.createElement('div');
activeMarker2.className = `${prefix}${uuidPortalActive}`;
portal2.appendChild(activeMarker2);
sheet.insertRule(
`#${prefix}${uuidPortal}2 .${prefix}${uuidPortalActive}{
--border: 2px;
border-radius: calc(8px + var(--border));
border: var(--border) solid rgba(255, 255, 255, 0.9);
background-color: transparent;
box-shadow: 0 1px 1px rgba(0,0,0,0.06),
0 2px 2px rgba(0,0,0,0.06),
0 4px 4px rgba(0,0,0,0.06),
0 8px 8px rgba(0,0,0,0.06),
0 16px 16px rgba(0,0,0,0.06);
}`,
sheet.length
);
// Instructions / keyboard commands
const instructions = document.createElement('div');
instructions.className = `${prefix}${uuidPortalActive}--instructions`;
sheet.insertRule(
`.${prefix}${uuidPortalActive}--instructions {
position: absolute;
bottom: 30px;
right: 30px;
opacity: 0;
}`,
sheet.length
);
sheet.insertRule(
`body:hover .${prefix}${uuidPortalActive}--instructions {
opacity: 1;
}`,
sheet.length
);
sheet.insertRule(
`.${prefix}${uuidPortalActive}--instructions kbd{
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
display: inline-block;
padding: 14px 25px;
line-height: 24px;
background: linear-gradient(180deg,#202025,#111117);
color: #fff;
border: 0;
border-radius: 8px;
box-shadow:
0 0 0 1px #474752 inset,
0 2px 0 rgba(200,200,200,0.1),
0 3px 0 rgba(100,100,100,0.05),
0 4px 0 rgba(100,100,100,0.1),
0 5px 0 rgba(100,100,100,0.05),
0 5px 0 rgb(17 17 26),
0 6px 1px rgba(0,0,0,0.1),
0 7px 2px rgba(0,0,0,0.1),
0 9px 4px rgba(0,0,0,0.1),
0 9px 8px rgba(0,0,0,0.1);
font-weight: 400;
font-size: 18px;
margin: 0 3px;
transition: transform 0.1s ease-in-out;
text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.8);
position: relative;
width: 70px;
text-align: center;
}`,
sheet.length
);
sheet.insertRule(
`body:hover .${prefix}${uuidPortalActive}--instructions kbd{
transition: transform 0.1s ease-out;
transform: scale(1);
}`,
sheet.length
);
sheet.insertRule(
`body:hover .${prefix}${uuidPortalActive}--instructions kbd.pressed{
position: relative;
background: linear-gradient(180deg,#fafafa,#ffffff);
color: #222;
transform: scale(0.95) translateY(2px);
border-color: #fff;
text-shadow: none;
box-shadow:
0 0 0 1px #d4d4d4 inset,
0 1px 0 rgba(100,100,100,0.2),
0 2px 0 rgba(100,100,100,0.15),
0 2px 0 #fff,
0 3px 1px rgba(0,0,0,0.1),
0 4px 2px rgba(0,0,0,0.1),
0 5px 4px rgba(0,0,0,0.1),
0 6px 8px rgba(0,0,0,0.1);
}`,
sheet.length
);
const kbdTab = document.createElement('kbd');
kbdTab.innerText = '⇥';
const kbdPlus = document.createElement('kbd');
kbdPlus.innerText = '+';
kbdPlus.style.position = 'absolute';
kbdPlus.style.left = '76px';
kbdPlus.style.top = '-62px';
const kbdMinus = document.createElement('kbd');
kbdMinus.innerText = '-';
kbdMinus.style.position = 'absolute';
kbdMinus.style.left = '76px';
kbdMinus.style.top = '-124px';
const kbdShift = document.createElement('kbd');
kbdShift.innerText = '⇧';
const kbdAlt = document.createElement('kbd');
kbdAlt.innerText = '⌥';
instructions.appendChild(kbdTab);
instructions.appendChild(kbdAlt);
instructions.appendChild(kbdShift);
instructions.appendChild(kbdPlus);
instructions.appendChild(kbdMinus);
portal2.appendChild(instructions);
// Holds the history of nodes that were expanded to their ancestor
// so we can revert and go back to the original node
// ACTUALLY: it's more intuitive to just go back to the first children, leaving commented code for now
// let chain = [];
// We need to be able to know wether an element is visible
// so we can decide wether or not to allow selecting it
function isVisible(element) {
let visible = false;
if (element) {
// We start by gettting the element's computed style to check for any smoking guns
// Wether they're caused because of the css or because of the html5 hidden attribute
const style = getComputedStyle(element);
visible = style && !(
style.display === 'none'
// maybe hidden shouldn't be filtered out see the opacity comment
|| style.visibility.includes['hidden', 'collapse']
// The following heuristic isn't enough: twitter uses transparent inputs on top of their custom UI
// (see theme selector in display settings for an example)
// || style.opacity === '0'
|| (style.width === '1px' && style.height === '1px')
|| (style.width.includes(['0px', '0']) || style.height.includes(['0px', '0']))
|| (style.clip === 'rect(0 0 0 0)' || style.clipPath.match(/inset\(([5-9]\d|100)%\)/))
);
// Still visible? Use boundingClientRect as a final check, it's pretty expensive
// so we should strive to call it as little as possible
if (visible) {
const rect = element.getBoundingClientRect();
visible = (rect.width > 0 && rect.height > 0)
}
}
return visible;
}
// Get an element's visible children
function getVisibleChildren(active) {
return [].filter.call(
active.children,
n => isVisible(n)
);
}
// Filter visible siblings within a node's parent node children
// returns an object made of a `siblings` array, and the active
// element index within this array
function getVisibleSiblingsCollection(active) {
const siblings = [].filter.call(
active.parentNode.children,
n => isVisible(n)
);
const idx = siblings.findIndex(
n => n === active
);
return {
siblings,
idx
};
}
// Surround active node with selected siblings before/after and return
// the collection [...siblingBefore, active, ...siblingsAfter]
function collectSurroundingSiblings(active) {
// Collect all visible siblings and find the active node index;
const { siblings, idx } = getVisibleSiblingsCollection(active);
// Compute selection indices
let startIdx = idx;
let endIdx = idx + 1;
if (siblingsBefore > 0) {
const max = Math.min(siblingsBefore, idx + 1);
startIdx = idx - max;
}
if (siblingsAfter > 0) {
const max = Math.min(siblingsAfter, siblings.length - idx);
endIdx = idx + 1 + max;
}
// Return the node collection (including siblings in range)
return siblings.slice(startIdx, endIdx);
}
// function getAncestorWithDifferentBoundingBox(element) {
// const bbox = element.getBoundingClientRect();
// let parent = element;
// let same = true;
// while (same) {
// parent = parent.parentNode;
// same = (
// JSON.stringify(bbox) === JSON.stringify(
// parent.getBoundingClientRect()
// )
// );
// }
// return parent;
// }
// function getDescendentWithDifferentBoundingBox(element) {
// const bbox = element.getBoundingClientRect();
// let descendent = element;
// let same = true;
// while (same) {
// descendent = descendent.children[0];
// same = (
// JSON.stringify(bbox) === JSON.stringify(
// descendent.getBoundingClientRect()
// )
// );
// }
// return descendent;
// }
function getFirstVisibleAncestor(element) {
let parent = element;
let visible = false;
while (!visible && parent !== document.body) {
parent = parent.parentNode;
visible = isVisible(parent);
}
return parent;
}
function getFirstVisibleDescendent(element) {
let descendent = element;
let visible = false;
while (!visible && descendent.children.length > 0) {
descendent = getVisibleChildren(descendent)[0];
visible = isVisible(descendent);
}
return (
visible
? descendent
: element
);
}
// Update active selection visual indicators
// Expands to selected siblings if any
function updateActive(active) {
let rect;
if (active instanceof Selection) {
rect = active.getRangeAt(0).getBoundingClientRect();
} else {
// Create the node collection (including siblings in range) to highlight
const nodes = collectSurroundingSiblings(active);
// Compute overall bounding box
let xMin = Infinity,
yMin = Infinity,
xBoundary = 0,
yBoundary = 0;
for (const node of nodes) {
const r = node.getBoundingClientRect();
const {x, y, width, height} = r;
xMin = Math.min(x, xMin);
yMin = Math.min(y, yMin);
xBoundary = Math.max(x + width, xBoundary);
yBoundary = Math.max(y + height, yBoundary);
}
rect = {
x: xMin,
y: yMin,
width: xBoundary - xMin,
height: yBoundary - yMin
};
}
// Set css variables
activeMarker.style.setProperty('--active-x', rect.x + "px");
activeMarker.style.setProperty('--active-y', rect.y + "px");
activeMarker.style.setProperty('--active-width', rect.width + "px");
activeMarker.style.setProperty('--active-height', rect.height + "px");
activeMarker2.style.setProperty('--active-x', rect.x + "px");
activeMarker2.style.setProperty('--active-y', rect.y + "px");
activeMarker2.style.setProperty('--active-width', rect.width + "px");
activeMarker2.style.setProperty('--active-height', rect.height + "px");
}
// Mouse enter event
const handleEnter = (e) => {
if (!isSelecting) {
resetSiblingsRange();
// chain = [];
activeElement = e.target;
updateActive(activeElement);
}
};
// Mouse out event
const handleOut = (e) => {
if (!isSelecting) {
resetSiblingsRange();
// chain = [];
const nxt = e.toElement || e.relatedTarget;
if (nxt && nxt.contains(e.target)) {
activeElement = nxt;
updateActive(activeElement);
}
}
};
// We use a debouncing method for the scroll handler so it doesn't fire too often
// and clogs ressources because of all the bounding boxes computations
const debounce = (callback, wait, immediate = false) => {
let timeout = null;
let initialCall = true;
return function() {
const callNow = immediate && initialCall;
const next = () => {
callback.apply(this, arguments);
timeout = null;
}
if (callNow) {
initialCall = false;
next();
}
if (!timeout) {
timeout = setTimeout(next, wait);
}
};
};
// We need to kep track of the mouse position to access it whenever scrolling
let mouseX = 0;
let mouseY = 0;
// Mouse move handler updates the mouse position
const handleMouseMove = (e) => {
mouseX = e.clientX;
mouseY = e.clientY;
};
// When scrolling, we change the css transition props (ex: duration)
// to do this, we rely on a class, which we need to remove whenever
// scrolling is complete to restore original properties
let scrollTimeout;
const handleScroll = debounce((e) => {
activeMarker.classList.add('scroll');
activeMarker2.classList.add('scroll');
// Update active element if scrolled on another one
if (!isSelecting) {
const next = document.elementFromPoint(mouseX,mouseY) || activeElement;
if (next === activeElement) {
activeElement.style.transitionDuration = `${Math.round(scrollAnimationDurationMs / 2)}ms`;
} else {
activeElement.style.transitionDuration = `${scrollAnimationDurationMs}ms`;
}
activeElement = next;
}
updateActive(activeElement);
window.clearTimeout(scrollTimeout);
scrollTimeout = window.setTimeout(() => {
activeMarker.classList.remove('scroll');
activeMarker2.classList.remove('scroll');
}, scrollAnimationDurationMs)
}, Math.round(scrollAnimationDurationMs), true);
// Clean the mess we created upon unloading
function cleanup() {
// Remove events
window.removeEventListener('resize', handleResize);
window.removeEventListener('scroll', handleScroll, true);
document.removeEventListener('selectionchange', handleSelection);
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('keydown', handleKeyDown);
document.removeEventListener('keyup', handleKeyUp);
document.removeEventListener('keypress', handleOther);
document.body.removeEventListener('mouseenter', handleEnter, true);
document.body.removeEventListener('mouseleave', handleOut, true);
// Remove nodes
document.head.removeChild(styleElement);
document.body.removeChild(portal);
document.body.removeChild(portal2);
}
// Keyboard controls
// Should we let the keyboard event propagate and proceed with default?
// should those be set to true, we will only block the event when
// it corresponds to one of our controls
const preventDefault = true;
const preventPropagation = true;
const resetSiblingsRange = () => {
siblingsAfter = 0;
siblingsBefore = 0;
}
const handleKeyDown = (e) => {
let preventDefaultPropagation = true;
if (e.shiftKey) {
kbdShift.classList.add('pressed');
if(e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
activeMarker.classList.add('select');
activeMarker2.classList.add('select');
}
}
if (e.altKey) {
kbdAlt.classList.add('pressed');
}
// This is super dirty and should be refactored
const { siblings, idx } = !(activeElement instanceof Selection)
? getVisibleSiblingsCollection(activeElement)
: {}
;
if (e.key === '+' || e.key === '=' || e.key === '≠' || e.key === '±' ) {
if (activeElement !== document.body) {
if (e.altKey) {
// Remove one sibling after the active element from selection until none are left
siblingsAfter = Math.max(0, siblingsAfter - 1);
} else {
// Add one sibling after the active element to selection
siblingsAfter = Math.min(siblingsAfter + 1, siblings.length - idx - 1);
}
updateActive(activeElement);
kbdPlus.classList.add('pressed');
}
} else if (e.key === '-' || e.key === '_' || e.key === '—' || e.key === '–' ) {
if (activeElement !== document.body) {
if (e.altKey) {
// Remove one sibling before the active element from selection until none are left
siblingsBefore = Math.max(0, siblingsBefore - 1);
} else {
// Add one sibling before the active element to selection
siblingsBefore = Math.min(siblingsBefore + 1, idx);
}
updateActive(activeElement);
kbdMinus.classList.add('pressed');
}
} else if (e.key === 'Escape') {
// Cleanup DOM nodes and unregister event listeners
cleanup();
} else if (e.key === 'Tab') {
resetSiblingsRange();
kbdTab.classList.add('pressed');
if (e.shiftKey) {
if (e.altKey) {
// activeElement = chain.pop() || activeElement;
// Select first child, going deeper in the tree
activeElement = (
// getVisibleChildren(activeElement)[0]
// getDescendentWithDifferentBoundingBox(activeElement)[0]
getFirstVisibleDescendent(activeElement)
) || activeElement;
resetSiblingsRange();
} else {
// Reverse cycle through siblings
activeElement = siblings[idx <= 0 ? siblings.length - 1 : idx - 1];
}
} else if (activeElement !== document.body && activeElement.parentNode) {
if (e.altKey) {
// chain.push(activeElement);
// Select direct ancestor, bubbling up the tree
// activeElement = activeElement.parentNode || activeElement;
// activeElement = getAncestorWithDifferentBoundingBox(activeElement) || activeElement;
activeElement = getFirstVisibleAncestor(activeElement) || activeElement;
resetSiblingsRange();
} else {
// Cycle through siblings
activeElement = siblings[idx + 1 >= siblings.length ? 0 : idx + 1];
}
}
updateActive(activeElement);
} else if (e.key === "Enter") {
// Log current selection and dom nodes if available
if (activeElement instanceof Selection) {
console.log({
string: activeElement.toString(),
selection: activeElement
});
} else {
console.log({
node: activeElement,
selection: collectSurroundingSiblings(activeElement)
});
}
} else {
// We only prevent default and stop propagation on the keys we actually use
// Pressing any other key will behave as usual, triggering default behavior
// and propagating as expected
preventDefaultPropagation = false;
// console.log(e.key);
}
if (preventDefaultPropagation) {
preventDefault && e.preventDefault();
preventPropagation && e.stopPropagation();
}
};
const handleKeyUp = (e) => {
preventDefault && e.preventDefault();
preventPropagation && e.stopPropagation();
instructions.querySelectorAll('.pressed').forEach(k => k.classList.remove('pressed'));
};
const handleOther = (e) => {
preventDefault && e.preventDefault();
preventPropagation && e.stopPropagation();
};
// Make sure we update when resizing the window to prevent obsolete positions
const handleResize = (e) => {
updateActive(activeElement);
};
// Text-selection
let isSelecting = false;
let selectionOut;
const handleSelection = (e) => {
const s = document.getSelection();
if (s.toString()) { // discard empty selection
isSelecting = true;
activeMarker.classList.add('select');
activeMarker2.classList.add('select');
clearTimeout(selectionOut);
selectionOut = setTimeout(() => {
activeMarker.classList.remove('select');
activeMarker2.classList.remove('select');
}, 16)
activeElement = s;
} else {
isSelecting = false;
activeElement = document.elementFromPoint(mouseX,mouseY) || activeElement;
}
updateActive(activeElement);
}
// Fix for blend mode issues copy body background color
// https://stackoverflow.com/questions/46358881/why-dont-css-blend-modes-work-when-applied-against-the-document-body
if (document.body.style.backgroundColor) {
document.children[0].style.backgroundColor = document.body.style.backgroundColor;
}
// Attach all our event handlers
document.body.addEventListener('mouseenter', handleEnter, true);
document.body.addEventListener('mouseleave', handleOut, true);
document.addEventListener('selectionchange', handleSelection);
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('keydown', handleKeyDown);
document.addEventListener('keyup', handleKeyUp);
document.addEventListener('keypress', handleOther);
window.addEventListener('scroll', handleScroll, true);
window.addEventListener('resize', handleResize);
console.warn('Close the console or click back inside your document in order to use keyboard commands')
console.log('Try using <Tab>, <Shift> + <Tab>, <Alt> + <Tab>, <Alt> + <Shift> + <Tab>, <Plus>, <Minus>, <Alt> + <Plus>, <Alt> + <Minus>');
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment