189 lines
5.1 KiB
JavaScript
189 lines
5.1 KiB
JavaScript
|
|
(function () {
|
||
|
|
"use strict";
|
||
|
|
|
||
|
|
function normalizeSequenceState(sequence) {
|
||
|
|
return {
|
||
|
|
total: Math.max(0, Number(sequence?.total) || 0),
|
||
|
|
currentIndex: Number.isFinite(Number(sequence?.currentIndex)) ? Number(sequence.currentIndex) : -1,
|
||
|
|
previousKey: String(sequence?.previousKey ?? sequence?.previousId ?? "").trim(),
|
||
|
|
nextKey: String(sequence?.nextKey ?? sequence?.nextId ?? "").trim()
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function isEditableKeyTarget(target) {
|
||
|
|
if (!(target instanceof HTMLElement)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return target instanceof HTMLInputElement
|
||
|
|
|| target instanceof HTMLTextAreaElement
|
||
|
|
|| target instanceof HTMLSelectElement
|
||
|
|
|| target.isContentEditable
|
||
|
|
|| Boolean(target.closest("[contenteditable='true']"));
|
||
|
|
}
|
||
|
|
|
||
|
|
function hasOpenModalDialog() {
|
||
|
|
return Boolean(document.querySelector("[role='dialog'][aria-modal='true'][aria-hidden='false']"));
|
||
|
|
}
|
||
|
|
|
||
|
|
function createSequenceNavigator(config = {}) {
|
||
|
|
const getElements = typeof config.getElements === "function"
|
||
|
|
? config.getElements
|
||
|
|
: () => ({});
|
||
|
|
|
||
|
|
let buttonsBound = false;
|
||
|
|
let keyboardBound = false;
|
||
|
|
|
||
|
|
function getSequenceState() {
|
||
|
|
return normalizeSequenceState(
|
||
|
|
typeof config.getSequenceState === "function"
|
||
|
|
? config.getSequenceState()
|
||
|
|
: null
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function getPrevButton(elements) {
|
||
|
|
return typeof config.getPrevButton === "function" ? config.getPrevButton(elements) : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getNextButton(elements) {
|
||
|
|
return typeof config.getNextButton === "function" ? config.getNextButton(elements) : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getPositionEl(elements) {
|
||
|
|
return typeof config.getPositionEl === "function" ? config.getPositionEl(elements) : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
function isActive(elements) {
|
||
|
|
return typeof config.isActive === "function" ? config.isActive(elements) !== false : true;
|
||
|
|
}
|
||
|
|
|
||
|
|
function getTargetKey(sequence, offset) {
|
||
|
|
return offset < 0 ? sequence.previousKey : sequence.nextKey;
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatPositionText(sequence, elements) {
|
||
|
|
return typeof config.formatPositionText === "function"
|
||
|
|
? String(config.formatPositionText(sequence, elements) || "")
|
||
|
|
: "";
|
||
|
|
}
|
||
|
|
|
||
|
|
function selectTarget(targetKey, elements, offset) {
|
||
|
|
if (!targetKey || typeof config.selectTarget !== "function") {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return config.selectTarget(targetKey, elements, offset) !== false;
|
||
|
|
}
|
||
|
|
|
||
|
|
function afterSelect(targetKey, elements, offset) {
|
||
|
|
if (typeof config.afterSelect === "function") {
|
||
|
|
config.afterSelect(targetKey, elements, offset);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function shouldHandleKeyEvent(event, elements) {
|
||
|
|
if (!isActive(elements)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (hasOpenModalDialog()) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return !isEditableKeyTarget(event.target);
|
||
|
|
}
|
||
|
|
|
||
|
|
function sync(elements = getElements()) {
|
||
|
|
const sequence = getSequenceState();
|
||
|
|
const previousKey = getTargetKey(sequence, -1);
|
||
|
|
const nextKey = getTargetKey(sequence, 1);
|
||
|
|
const prevButton = getPrevButton(elements);
|
||
|
|
const nextButton = getNextButton(elements);
|
||
|
|
const positionEl = getPositionEl(elements);
|
||
|
|
|
||
|
|
if (prevButton) {
|
||
|
|
prevButton.disabled = !previousKey;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (nextButton) {
|
||
|
|
nextButton.disabled = !nextKey;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (positionEl) {
|
||
|
|
positionEl.textContent = formatPositionText(sequence, elements);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function step(offset, elements = getElements()) {
|
||
|
|
const sequence = getSequenceState();
|
||
|
|
const targetKey = getTargetKey(sequence, offset);
|
||
|
|
if (!targetKey) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
const didSelect = selectTarget(targetKey, elements, offset);
|
||
|
|
if (didSelect) {
|
||
|
|
afterSelect(targetKey, elements, offset);
|
||
|
|
}
|
||
|
|
|
||
|
|
return didSelect;
|
||
|
|
}
|
||
|
|
|
||
|
|
function bind(elements = getElements()) {
|
||
|
|
if (!buttonsBound) {
|
||
|
|
getPrevButton(elements)?.addEventListener("click", () => {
|
||
|
|
step(-1, getElements());
|
||
|
|
});
|
||
|
|
|
||
|
|
getNextButton(elements)?.addEventListener("click", () => {
|
||
|
|
step(1, getElements());
|
||
|
|
});
|
||
|
|
|
||
|
|
buttonsBound = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!keyboardBound) {
|
||
|
|
document.addEventListener("keydown", (event) => {
|
||
|
|
const latestElements = getElements();
|
||
|
|
if (!shouldHandleKeyEvent(event, latestElements)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const offset = event.key === "ArrowRight" ? 1 : -1;
|
||
|
|
const sequence = getSequenceState();
|
||
|
|
if (!getTargetKey(sequence, offset)) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
event.preventDefault();
|
||
|
|
step(offset, latestElements);
|
||
|
|
});
|
||
|
|
|
||
|
|
keyboardBound = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
sync(elements);
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
bind,
|
||
|
|
step,
|
||
|
|
sync,
|
||
|
|
getSequenceState
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
window.TarotSequenceNav = {
|
||
|
|
...(window.TarotSequenceNav || {}),
|
||
|
|
createSequenceNavigator
|
||
|
|
};
|
||
|
|
})();
|