navkit-vue
Version:
Vue navigation and utility composables
782 lines (773 loc) • 29.4 kB
JavaScript
var vue = require('vue');
var KeyboardEnum;
(function (KeyboardEnum) {
KeyboardEnum["Left"] = "ArrowLeft";
KeyboardEnum["Right"] = "ArrowRight";
KeyboardEnum["Up"] = "ArrowUp";
KeyboardEnum["Down"] = "ArrowDown";
KeyboardEnum["Enter"] = "Enter";
KeyboardEnum["Back"] = "Escape";
})(KeyboardEnum || (KeyboardEnum = {}));
// Logic
function useNavigation({ rows, onColumnStart, onColumnEnd, onRowStart, onRowEnd, onEnter, onReturn, initialPosition = { row: 0, col: 0 }, disabled = vue.ref(false), focusableSelector = "[data-focusable]", autofocus = true, focusClass = "focused", cyclic = false, invertAxis = false, autoNextRow = false, holdColumnPerRow = false, }) {
const computedRows = vue.computed(() => (vue.isRef(rows) ? rows.value : rows));
const currentPosition = vue.ref(initialPosition);
const lastFocusedElement = vue.ref(null);
const positions = vue.ref(Array.from({ length: computedRows.value.length }).map((_) => 0));
let isProcessing = false;
// Disabling the hook
const isDisabled = vue.computed(() => vue.unref(disabled));
const toggleDisabled = (value) => {
if (value !== undefined) {
if (vue.isRef(disabled)) {
disabled.value = value;
}
else {
disabled = value;
}
}
else {
if (vue.isRef(disabled)) {
disabled.value = !disabled.value;
}
else {
disabled = !disabled;
}
}
};
const findFocusableElements = () => {
return Array.from(document.querySelectorAll(focusableSelector));
};
const isValidPosition = (pos) => {
if (pos.row < 0 || pos.row >= computedRows.value.length)
return false;
return pos.col >= 0 && pos.col < computedRows.value[pos.row];
};
const getFocusableElement = (pos) => {
const elements = findFocusableElements();
if (!elements.length)
return null;
let currentRow = 0;
let currentCol = 0;
for (const element of elements) {
if (currentRow === pos.row && currentCol === pos.col) {
return element;
}
currentCol++;
if (currentCol >= computedRows.value[currentRow]) {
currentRow++;
currentCol = 0;
}
}
return null;
};
// Function to focus an element
const focusElement = (element) => {
if (isProcessing || !element)
return;
isProcessing = true;
try {
// Remove focus from previous element
if (lastFocusedElement.value) {
lastFocusedElement.value.classList.remove(focusClass);
lastFocusedElement.value.blur();
}
// Focus new element
element.classList.add(focusClass);
if (autofocus) {
element.focus();
}
lastFocusedElement.value = element;
}
catch (error) {
console.error("Focus error:", error);
}
finally {
isProcessing = false;
}
};
function setRowPosition(row, position) {
return (positions.value[row] = position);
}
// Handler for keydown events
const handleKeyDown = (event) => {
try {
if (isDisabled.value || isProcessing)
return;
const newPosition = { ...currentPosition.value };
switch (event.key) {
case KeyboardEnum.Up:
if (invertAxis) {
newPosition.col--;
positions.value[newPosition.row] = newPosition.col;
if (newPosition.col < 0) {
if (cyclic) {
newPosition.col = computedRows.value[newPosition.row] - 1;
}
else {
onRowStart === null || onRowStart === void 0 ? void 0 : onRowStart();
return;
}
}
break;
}
newPosition.row--;
if (newPosition.row < 0) {
if (cyclic) {
newPosition.row = computedRows.value.length - 1;
}
else {
onRowStart === null || onRowStart === void 0 ? void 0 : onRowStart();
return;
}
}
if (newPosition.col >= computedRows.value[newPosition.row]) {
newPosition.col = computedRows.value[newPosition.row] - 1;
}
else {
newPosition.col = holdColumnPerRow
? currentPosition.value.col
: positions.value[newPosition.row];
}
break;
case KeyboardEnum.Down:
if (invertAxis) {
newPosition.col++;
positions.value[newPosition.row] = newPosition.col;
if (newPosition.col >= computedRows.value[newPosition.row]) {
if (cyclic) {
newPosition.col = 0;
}
else {
onRowEnd === null || onRowEnd === void 0 ? void 0 : onRowEnd();
return;
}
}
break;
}
newPosition.row++;
if (newPosition.row >= computedRows.value.length) {
if (cyclic) {
newPosition.row = 0;
}
else {
onRowEnd === null || onRowEnd === void 0 ? void 0 : onRowEnd();
return;
}
}
if (newPosition.col >= computedRows.value[newPosition.row]) {
newPosition.col = computedRows.value[newPosition.row] - 1;
}
else {
newPosition.col = holdColumnPerRow
? currentPosition.value.col
: positions.value[newPosition.row];
}
break;
case KeyboardEnum.Left:
if (invertAxis) {
newPosition.row--;
newPosition.col = holdColumnPerRow
? currentPosition.value.col
: positions.value[newPosition.row];
break;
}
newPosition.col--;
positions.value[newPosition.row] = newPosition.col;
if (newPosition.col < 0) {
if (cyclic) {
if (autoNextRow) {
newPosition.row =
newPosition.row > 0
? newPosition.row - 1
: computedRows.value.length - 1;
}
newPosition.col = computedRows.value[newPosition.row] - 1;
}
else {
onColumnStart === null || onColumnStart === void 0 ? void 0 : onColumnStart();
return;
}
}
break;
case KeyboardEnum.Right:
if (invertAxis) {
newPosition.row++;
newPosition.col = holdColumnPerRow
? currentPosition.value.col
: positions.value[newPosition.row];
break;
}
newPosition.col++;
positions.value[newPosition.row] = newPosition.col;
if (newPosition.col >= computedRows.value[currentPosition.value.row]) {
if (cyclic) {
if (autoNextRow) {
newPosition.row =
newPosition.row < computedRows.value.length - 1
? newPosition.row + 1
: 0;
}
newPosition.col = 0;
}
else {
onColumnEnd === null || onColumnEnd === void 0 ? void 0 : onColumnEnd();
return;
}
}
break;
case KeyboardEnum.Enter:
onEnter === null || onEnter === void 0 ? void 0 : onEnter(currentPosition.value);
return;
case KeyboardEnum.Back:
onReturn === null || onReturn === void 0 ? void 0 : onReturn(currentPosition.value);
return;
default:
console.warn("Unhandled Key:", event.code);
return;
}
if (isValidPosition(newPosition)) {
currentPosition.value = newPosition;
}
else {
console.error("Position Not Valid. returning to default:", newPosition);
currentPosition.value = {
col: 0,
row: 0,
};
}
}
catch (error) {
console.error("Error in handleKeyNavigation:", error);
}
};
const stopWatchPosition = vue.watch(currentPosition, (newPosition) => {
if (!isDisabled.value && !isProcessing) {
const element = getFocusableElement(newPosition);
focusElement(element);
}
});
// Check for disabled value, remove listeners if is disabled
const stopWatchIsDisabled = vue.watch(isDisabled, (newValue) => {
var _a, _b;
if (newValue) {
(_a = lastFocusedElement.value) === null || _a === void 0 ? void 0 : _a.blur();
(_b = lastFocusedElement.value) === null || _b === void 0 ? void 0 : _b.classList.remove(focusClass);
removeEventListeners();
}
else {
if (autofocus && !isDisabled.value) {
const element = getFocusableElement(currentPosition.value);
focusElement(element);
}
addEventListeners();
}
});
const addEventListeners = () => {
window.addEventListener("keydown", handleKeyDown);
};
const removeEventListeners = () => {
window.removeEventListener("keydown", handleKeyDown);
};
// Lifecycle hooks
vue.onMounted(() => {
if (!isDisabled.value) {
addEventListeners();
if (autofocus) {
const element = getFocusableElement(currentPosition.value);
focusElement(element);
}
}
});
vue.onUnmounted(() => {
stopWatchIsDisabled();
stopWatchPosition();
});
return {
position: currentPosition,
currentRow: positions.value[invertAxis ? currentPosition.value.col : currentPosition.value.row],
currentElement: lastFocusedElement.value,
isDisabled,
setRowPosition,
toggleDisabled,
isValidPosition,
focusElement,
getCurrentFocusedElement: () => lastFocusedElement.value,
debug: {
focusableElements: findFocusableElements(),
isHookDisabled: isDisabled.value,
},
};
}
function useNavigationX({ columns, initialPosition = 0, disabled = vue.ref(false), focusableSelector = "[data-focusable]", autofocus = true, focusClass = "focused", cyclic = false, onEnter, onReturn, onColumnEnd, onColumnStart, onDown, onUp, }) {
const computedCols = vue.computed(() => vue.unref(columns));
const currentPosition = vue.ref(initialPosition);
const lastFocusedElement = vue.ref(null);
let isProcessing = false;
// Disabling the hook
const isDisabled = vue.computed(() => vue.unref(disabled));
const isDisabledWatcher = vue.watch(isDisabled, (newValue) => {
var _a, _b;
if (newValue) {
(_a = lastFocusedElement.value) === null || _a === void 0 ? void 0 : _a.blur();
(_b = lastFocusedElement.value) === null || _b === void 0 ? void 0 : _b.classList.remove(focusClass);
window.removeEventListener("keydown", handleKeyDown);
}
else {
if (autofocus && !isDisabled.value) {
const element = getFocusableElement(currentPosition.value);
focusElement(element);
}
window.addEventListener("keydown", handleKeyDown);
}
});
const toggleDisabled = (value) => {
if (value !== undefined) {
if (vue.isRef(disabled)) {
disabled.value = value;
}
else {
disabled = value;
}
}
else {
if (vue.isRef(disabled)) {
disabled.value = !disabled.value;
}
else {
disabled = !disabled;
}
}
};
function setPosition(pos) {
return (currentPosition.value = pos);
}
const findFocusableElements = () => {
return Array.from(document.querySelectorAll(focusableSelector));
};
const isValidPosition = (col) => {
return col >= 0 && col < computedCols.value;
};
const getFocusableElement = (pos) => {
const elements = findFocusableElements();
let currentCol = 0;
for (const element of elements) {
if (currentCol === pos) {
return element;
}
currentCol++;
}
return null;
};
const focusElement = (element) => {
if (isProcessing || !element)
return;
isProcessing = true;
try {
if (lastFocusedElement.value) {
lastFocusedElement.value.classList.remove(focusClass);
lastFocusedElement.value.blur();
}
element.classList.add(focusClass);
if (autofocus) {
element.focus();
}
lastFocusedElement.value = element;
}
catch (error) {
console.error("Focus error:", error);
}
finally {
isProcessing = false;
}
};
const handleKeyDown = (event) => {
if (isDisabled.value || isProcessing)
return;
let newPosition = currentPosition.value;
switch (event.key) {
case KeyboardEnum.Left:
newPosition--;
if (newPosition < 0) {
if (cyclic) {
newPosition = computedCols.value - 1;
}
else {
onColumnStart === null || onColumnStart === void 0 ? void 0 : onColumnStart();
return;
}
}
break;
case KeyboardEnum.Right:
newPosition++;
if (newPosition >= computedCols.value) {
if (cyclic) {
newPosition = 0;
}
else {
onColumnEnd === null || onColumnEnd === void 0 ? void 0 : onColumnEnd();
return;
}
}
break;
case KeyboardEnum.Up:
onUp === null || onUp === void 0 ? void 0 : onUp();
break;
case KeyboardEnum.Down:
onDown === null || onDown === void 0 ? void 0 : onDown();
break;
case KeyboardEnum.Enter:
onEnter === null || onEnter === void 0 ? void 0 : onEnter(currentPosition.value);
return;
case KeyboardEnum.Back:
onReturn === null || onReturn === void 0 ? void 0 : onReturn(currentPosition.value);
return;
default:
console.warn("Unhandled Key:", event.code);
return;
}
if (isValidPosition(newPosition)) {
currentPosition.value = newPosition;
}
else {
console.error("Position Not Valid:", newPosition);
}
};
const currentPositionWatcher = vue.watch(currentPosition, (newPosition) => {
if (!isDisabled.value && !isProcessing) {
const element = getFocusableElement(newPosition);
focusElement(element);
}
});
// Lifecycle hooks
vue.onMounted(() => {
if (isDisabled.value) {
window.removeEventListener("keydown", handleKeyDown);
}
else {
window.addEventListener("keydown", handleKeyDown);
}
if (autofocus && !isDisabled.value) {
const element = getFocusableElement(currentPosition.value);
focusElement(element);
}
});
vue.onUnmounted(() => {
window.removeEventListener("keydown", handleKeyDown);
if (lastFocusedElement.value) {
lastFocusedElement.value.classList.remove(focusClass);
}
isDisabledWatcher();
currentPositionWatcher();
});
return {
position: currentPosition,
currentElement: lastFocusedElement.value,
setPosition,
toggleDisabled,
isValidPosition,
focusElement,
getCurrentFocusedElement: () => lastFocusedElement.value,
};
}
function useNavigationY({ rows, initialPosition = 0, disabled = vue.ref(false), focusableSelector = "[data-focusable]", autofocus = true, focusClass = "focused", cyclic = false, onRowStart, onRowEnd, onEnter, onReturn, onRight, onLeft, }) {
const computedRows = vue.ref(vue.unref(rows));
const currentPosition = vue.ref(initialPosition);
const lastFocusedElement = vue.ref(null);
let isProcessing = false;
// Disabling the hook
const isDisabled = vue.computed(() => vue.unref(disabled));
const isDisabledWatcher = vue.watch(isDisabled, (newValue) => {
var _a, _b;
if (newValue) {
(_a = lastFocusedElement.value) === null || _a === void 0 ? void 0 : _a.blur();
(_b = lastFocusedElement.value) === null || _b === void 0 ? void 0 : _b.classList.remove(focusClass);
window.removeEventListener("keydown", handleKeyDown);
}
else {
if (autofocus && !isDisabled.value) {
const element = getFocusableElement(currentPosition.value);
focusElement(element);
}
window.addEventListener("keydown", handleKeyDown);
}
});
const toggleDisabled = (value) => {
if (value !== undefined) {
if (vue.isRef(disabled)) {
disabled.value = value;
}
else {
disabled = value;
}
}
else {
if (vue.isRef(disabled)) {
disabled.value = !disabled.value;
}
else {
disabled = !disabled;
}
}
};
function setPosition(pos) {
return (currentPosition.value = pos);
}
const findFocusableElements = () => {
return Array.from(document.querySelectorAll(focusableSelector));
};
const isValidPosition = (row) => {
return row >= 0 && row < computedRows.value;
};
const getFocusableElement = (pos) => {
const elements = findFocusableElements();
let currentRow = 0;
for (const element of elements) {
if (currentRow === pos) {
return element;
}
currentRow++;
}
return null;
};
const focusElement = (element) => {
if (isProcessing || !element)
return;
isProcessing = true;
try {
if (lastFocusedElement.value) {
lastFocusedElement.value.classList.remove(focusClass);
lastFocusedElement.value.blur();
}
element.classList.add(focusClass);
if (autofocus) {
element.focus();
}
lastFocusedElement.value = element;
}
catch (error) {
console.error("Focus error:", error);
}
finally {
isProcessing = false;
}
};
const handleKeyDown = (event) => {
if (isDisabled.value || isProcessing)
return;
let newPosition = currentPosition.value;
switch (event.key) {
case KeyboardEnum.Up:
newPosition--;
if (newPosition < 0) {
if (cyclic) {
newPosition = computedRows.value - 1;
}
else {
onRowStart === null || onRowStart === void 0 ? void 0 : onRowStart();
return;
}
}
break;
case KeyboardEnum.Down:
newPosition++;
if (newPosition >= computedRows.value) {
if (cyclic) {
newPosition = 0;
}
else {
onRowEnd === null || onRowEnd === void 0 ? void 0 : onRowEnd();
return;
}
}
break;
case KeyboardEnum.Left:
onLeft === null || onLeft === void 0 ? void 0 : onLeft();
break;
case KeyboardEnum.Right:
onRight === null || onRight === void 0 ? void 0 : onRight();
break;
case KeyboardEnum.Enter:
onEnter === null || onEnter === void 0 ? void 0 : onEnter(currentPosition.value);
break;
case KeyboardEnum.Back:
onReturn === null || onReturn === void 0 ? void 0 : onReturn(currentPosition.value);
break;
default:
console.warn("Unhandled Key:", event.code);
return;
}
if (isValidPosition(newPosition)) {
currentPosition.value = newPosition;
}
else {
console.error("Position Not Valid:", newPosition);
}
};
const currentPositionWatcher = vue.watch(currentPosition, (newPosition) => {
if (!isDisabled.value && !isProcessing) {
const element = getFocusableElement(newPosition);
focusElement(element);
}
});
// Lifecycle hooks
vue.onMounted(() => {
if (isDisabled.value) {
window.removeEventListener("keydown", handleKeyDown);
}
else {
window.addEventListener("keydown", handleKeyDown);
}
if (autofocus && !isDisabled.value) {
const element = getFocusableElement(currentPosition.value);
focusElement(element);
}
});
vue.onUnmounted(() => {
window.removeEventListener("keydown", handleKeyDown);
if (lastFocusedElement.value) {
lastFocusedElement.value.classList.remove(focusClass);
}
isDisabledWatcher();
currentPositionWatcher();
});
return {
position: currentPosition,
currentElement: lastFocusedElement.value,
setPosition,
toggleDisabled,
isValidPosition,
focusElement,
getCurrentFocusedElement: () => lastFocusedElement.value,
};
}
function useNavigationReturn({ onReturn, disabled, }) {
// Disabling the hook
const isDisabled = vue.ref(vue.unref(disabled));
const toggleDisabled = (value) => {
if (value !== undefined) {
isDisabled.value = value;
}
else {
isDisabled.value = !isDisabled.value;
}
};
const handleKeyDown = (event) => {
if (event.key === KeyboardEnum.Back) {
onReturn();
}
};
// Check for disabled value, remove listeners if is disabled
vue.watch(isDisabled, (newValue) => {
if (newValue) {
window.removeEventListener("keydown", handleKeyDown);
}
else {
window.addEventListener("keydown", handleKeyDown);
}
});
vue.onMounted(() => {
window.addEventListener("keydown", handleKeyDown);
});
vue.onUnmounted(() => {
window.removeEventListener("keydown", handleKeyDown);
});
return { toggleDisabled };
}
function useScrollIntoFocus({ position, selectedElement, behavior = "smooth", delay = 1000, parentSelector = "[data-parent]", buffer = 180, bufferX, bufferY, suppressLogs = true, scrollType = "throttle", }) {
let timeout = null;
let lastCall = 0;
const currElement = vue.ref(null);
const parentContainer = vue.ref(null);
const setParentContainer = () => {
parentContainer.value = document.querySelector(parentSelector);
if (!parentContainer.value) {
console.warn("Parent container not found!");
}
};
const isElementFullyVisible = (element, parent) => {
const elementRect = element.getBoundingClientRect();
const parentRect = parent.getBoundingClientRect();
return {
isFullyVisible: elementRect.left >= parentRect.left &&
elementRect.right <= parentRect.right &&
elementRect.top >= parentRect.top &&
elementRect.bottom <= parentRect.bottom,
horizontalOverflow: elementRect.right - parentRect.right,
verticalOverflow: elementRect.bottom - parentRect.bottom,
topOverflow: parentRect.top - elementRect.top,
leftOverflow: parentRect.left - elementRect.left,
};
};
const scrollWindow = () => {
const element = vue.unref(currElement);
const parent = vue.unref(parentContainer);
if (element && parent) {
const { isFullyVisible, horizontalOverflow, verticalOverflow, topOverflow, leftOverflow, } = isElementFullyVisible(element, parent);
if (!isFullyVisible) {
// Scroll Down
if (verticalOverflow > 0) {
parent.scrollTo({
top: parent.scrollTop + verticalOverflow + (bufferY !== null && bufferY !== void 0 ? bufferY : buffer),
behavior,
});
}
// Scroll Up (Element is above the viewport)
if (topOverflow > 0) {
parent.scrollTo({
top: parent.scrollTop - topOverflow - (bufferY !== null && bufferY !== void 0 ? bufferY : buffer),
behavior,
});
}
// Scroll Right
if (horizontalOverflow > 0) {
parent.scrollTo({
left: parent.scrollLeft + horizontalOverflow + (bufferX !== null && bufferX !== void 0 ? bufferX : buffer),
behavior,
});
}
// Scroll Left (Element is off-screen to the left)
if (leftOverflow > 0) {
parent.scrollTo({
left: parent.scrollLeft - leftOverflow - (bufferX !== null && bufferX !== void 0 ? bufferX : buffer),
behavior,
});
}
}
}
};
vue.nextTick(setParentContainer);
vue.watch(selectedElement, (newVal) => {
if (newVal) {
vue.nextTick(() => {
currElement.value = newVal;
});
}
});
vue.watch(position, async () => {
if (!parentContainer.value) {
await vue.nextTick(setParentContainer);
}
await vue.nextTick();
currElement.value = vue.unref(selectedElement);
if (scrollType === "throttle") {
const now = new Date().getTime();
if (now - lastCall >= delay) {
lastCall = now;
scrollWindow();
}
}
else if (scrollType === "debounce") {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(scrollWindow, delay);
}
});
}
exports.useNavigation = useNavigation;
exports.useNavigationReturn = useNavigationReturn;
exports.useNavigationX = useNavigationX;
exports.useNavigationY = useNavigationY;
exports.useScrollIntoFocus = useScrollIntoFocus;
;