|
// Numworks Simulator Enhancements |
|
// Arrow key fix, keyboard shortcuts, and keypress indicator system |
|
|
|
(function() { |
|
// console.log('[Numworks] Installing complete enhancement suite...'); |
|
|
|
function initialize() { |
|
// Check if we're on the simulator page |
|
if (!window.location.href.includes('numworks.com/simulator')) { |
|
// console.log('[Numworks] Not on simulator page, skipping...'); |
|
return; |
|
} |
|
|
|
const canvas = document.querySelector('canvas[tabindex]'); |
|
|
|
if (!canvas) { |
|
// console.warn('[Numworks] Canvas not found, retrying in 500ms...'); |
|
setTimeout(initialize, 500); |
|
return; |
|
} |
|
|
|
// console.log('[Numworks] Canvas found, installing enhancements...'); |
|
canvas.focus(); |
|
|
|
// ============================================ |
|
// Visual Feedback System with Color Coding |
|
// ============================================ |
|
|
|
// Color schemes for different button modes (using Numworks yellow for shift!) |
|
const COLORS = { |
|
normal: 'rgba(0, 0, 0, .2)', // Default active color (gray) |
|
shift: 'rgba(184, 143, 46, .4)', // Numworks shift yellow |
|
alpha: 'rgba(100, 150, 255, .35)' // Light blue for alpha mode |
|
}; |
|
|
|
// Track currently active keys to maintain held state |
|
const activeKeys = new Map(); // dataKey -> { element, previous, color } |
|
|
|
// Activate visual feedback on a button (for held keys) |
|
function activateKey(dataKey, color = COLORS.normal) { |
|
const el = document.querySelector(`.calculator span[data-key="${dataKey}"][data-v-09bb4f2f]`); |
|
if (!el) { |
|
// console.warn(`[Numworks] Visual feedback: element not found for data-key="${dataKey}"`); |
|
return; |
|
} |
|
|
|
// If already active with same color, don't re-trigger |
|
if (activeKeys.has(dataKey)) { |
|
const existing = activeKeys.get(dataKey); |
|
if (existing.color === color) return; |
|
// Different color - update it |
|
el.style.backgroundColor = color; |
|
existing.color = color; |
|
return; |
|
} |
|
|
|
// Save previous inline background (if any) so we can restore it |
|
const previous = el.style.backgroundColor; |
|
activeKeys.set(dataKey, { element: el, previous: previous, color: color }); |
|
|
|
// Apply the color |
|
el.style.backgroundColor = color; |
|
} |
|
|
|
// Deactivate visual feedback (on keyup) |
|
function deactivateKey(dataKey) { |
|
const stored = activeKeys.get(dataKey); |
|
if (!stored) return; |
|
|
|
const { element, previous } = stored; |
|
element.style.backgroundColor = previous; |
|
activeKeys.delete(dataKey); |
|
} |
|
|
|
// Force clear all active states (safety mechanism) |
|
function clearAllActiveKeys() { |
|
activeKeys.forEach((stored, dataKey) => { |
|
const { element, previous } = stored; |
|
element.style.backgroundColor = previous; |
|
}); |
|
activeKeys.clear(); |
|
} |
|
|
|
// Flash visual feedback for simulated clicks (non-held) |
|
function flashActiveKey(dataKey, color = COLORS.normal, durationMs = 120) { |
|
const el = document.querySelector(`.calculator span[data-key="${dataKey}"][data-v-09bb4f2f]`); |
|
if (!el) { |
|
// console.warn(`[Numworks] Visual feedback: element not found for data-key="${dataKey}"`); |
|
return; |
|
} |
|
|
|
// Save previous inline background |
|
const previous = el.style.backgroundColor; |
|
|
|
// Apply the color |
|
el.style.backgroundColor = color; |
|
|
|
setTimeout(() => { |
|
el.style.backgroundColor = previous; |
|
}, durationMs); |
|
} |
|
|
|
// Flash a combo (modifier + key with different colors) |
|
function flashCombo(modifierKey, targetKey, modifierColor, targetColor, delay = 5) { |
|
flashActiveKey(modifierKey, modifierColor); |
|
setTimeout(() => { |
|
flashActiveKey(targetKey, targetColor); |
|
}, delay); |
|
} |
|
|
|
// ============================================ |
|
// Key Mappings |
|
// ============================================ |
|
|
|
// Arrow keys to data-key mapping |
|
const arrowKeyMap = { |
|
'ArrowUp': 1, |
|
'ArrowDown': 2, |
|
'ArrowLeft': 0, |
|
'ArrowRight': 3 |
|
}; |
|
|
|
// Number keys to data-key mapping |
|
const numberKeyMap = { |
|
'0': 48, '1': 42, '2': 43, '3': 44, '4': 36, |
|
'5': 37, '6': 38, '7': 30, '8': 31, '9': 32 |
|
}; |
|
|
|
// Special keys to data-key mapping |
|
const specialKeyMap = { |
|
'Enter': 52, |
|
'(': 33, |
|
')': 34, |
|
'.': 49, |
|
'Backspace': 17, |
|
'Delete': 17, // Same as backspace |
|
'i': 21, // Imaginary number |
|
',': 22 |
|
}; |
|
|
|
// Operation keys to data-key mapping |
|
const operationKeyMap = { |
|
'^': 23, // Exponent (Shift+6) |
|
'*': 39, // Multiplication |
|
'/': 40, // Divide |
|
'+': 45, // Plus |
|
'-': 46 // Minus |
|
}; |
|
|
|
// Bracket keys (shift combos) |
|
const bracketKeyMap = { |
|
'[': 18, // Left bracket (shift combo) |
|
']': 19, // Right bracket (shift combo) |
|
'{': 20, // Left brace (shift combo) |
|
'}': 21, // Right brace (shift combo) |
|
'<': 28, // Less than (shift combo) |
|
'>': 29 // Greater than (shift combo) |
|
}; |
|
|
|
// Alphabetical keys (a-z) - CORRECTED with gaps! |
|
// a-q: 18-34 (continuous) |
|
// r-v: 36-40 (skip 35, skip 41) |
|
// w-z: 42-45 (skip 47) |
|
const alphaKeyMap = { |
|
'a': 18, 'b': 19, 'c': 20, 'd': 21, 'e': 22, 'f': 23, |
|
'g': 24, 'h': 25, 'i': 26, 'j': 27, 'k': 28, 'l': 29, |
|
'm': 30, 'n': 31, 'o': 32, 'p': 33, 'q': 34, |
|
// Gap at 35 |
|
'r': 36, 's': 37, 't': 38, 'u': 39, 'v': 40, |
|
// Gap at 41 |
|
'w': 42, 'x': 43, 'y': 44, 'z': 45, |
|
' ': 46, // Space |
|
// Gap at 47 |
|
'?': 48, // Question mark |
|
'!': 49 // Exclamation mark |
|
}; |
|
|
|
// Shift combo keys that trigger special functions |
|
const shiftComboKeys = { |
|
15: 'var', // Copy |
|
16: 'toolbox', // Paste |
|
17: 'backspace', // Clear |
|
18: 'leftBracket', |
|
19: 'rightBracket', |
|
20: 'leftBrace', |
|
21: 'rightBrace', |
|
28: 'lessThan', |
|
29: 'greaterThan' |
|
}; |
|
|
|
// Constants for modifier keys |
|
const SHIFT_KEY = 12; |
|
const ALPHA_KEY = 13; |
|
|
|
// Track modifier key states to handle caps lock / spam |
|
let shiftActive = false; |
|
let alphaActive = false; |
|
|
|
// ============================================ |
|
// Part 1: Arrow Key Fix (with visual feedback) |
|
// ============================================ |
|
|
|
const arrowKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight']; |
|
let isRedispatching = false; |
|
|
|
['keydown', 'keyup'].forEach(eventType => { |
|
window.addEventListener(eventType, function(e) { |
|
if (isRedispatching) return; |
|
if (!arrowKeys.includes(e.key)) return; |
|
|
|
// console.log(`[Numworks] Intercepted arrow ${eventType}: ${e.key}`); |
|
|
|
const dataKey = arrowKeyMap[e.key]; |
|
|
|
// Handle visual feedback - activate on keydown, deactivate on keyup |
|
if (eventType === 'keydown') { |
|
activateKey(dataKey); |
|
} else if (eventType === 'keyup') { |
|
deactivateKey(dataKey); |
|
} |
|
|
|
e.stopImmediatePropagation(); |
|
e.stopPropagation(); |
|
e.preventDefault(); |
|
|
|
isRedispatching = true; |
|
|
|
const cleanEvent = new KeyboardEvent(eventType, { |
|
key: e.key, |
|
code: e.code, |
|
keyCode: e.keyCode, |
|
which: e.keyCode, |
|
charCode: 0, |
|
bubbles: false, |
|
cancelable: true, |
|
composed: false, |
|
view: window, |
|
detail: 0 |
|
}); |
|
|
|
canvas.dispatchEvent(cleanEvent); |
|
isRedispatching = false; |
|
|
|
}, true); |
|
}); |
|
|
|
// ============================================ |
|
// Part 2: Intelligent Visual Feedback System |
|
// ============================================ |
|
|
|
// Safety: Clear all states on window blur or focus loss |
|
window.addEventListener('blur', clearAllActiveKeys); |
|
document.addEventListener('visibilitychange', () => { |
|
if (document.hidden) { |
|
clearAllActiveKeys(); |
|
} |
|
}); |
|
|
|
// Determine if a key is an alpha key |
|
function isAlphaKey(key) { |
|
return alphaKeyMap.hasOwnProperty(key); |
|
} |
|
|
|
// Add visual feedback for all keys with intelligent color coding |
|
window.addEventListener('keydown', function(e) { |
|
// Ignore CapsLock to prevent stuck states |
|
if (e.key === 'CapsLock') { |
|
return; |
|
} |
|
|
|
let dataKey = null; |
|
let color = COLORS.normal; |
|
|
|
// Check for alpha keys (letters, space, ?, !) |
|
// Only trigger if NOT using Ctrl/Cmd (to avoid conflicts with shortcuts) |
|
if (isAlphaKey(e.key) && !e.ctrlKey && !e.metaKey) { |
|
dataKey = alphaKeyMap[e.key]; |
|
color = COLORS.alpha; |
|
|
|
// Activate alpha key first (only if not already active) |
|
if (!alphaActive) { |
|
activateKey(ALPHA_KEY, COLORS.normal); |
|
alphaActive = true; |
|
} |
|
activateKey(dataKey, color); |
|
return; |
|
} |
|
|
|
// Check for shift combo keys (brackets, braces, etc.) |
|
if (bracketKeyMap.hasOwnProperty(e.key)) { |
|
dataKey = bracketKeyMap[e.key]; |
|
color = COLORS.shift; |
|
|
|
// Activate shift key first (only if not already active) |
|
if (!shiftActive) { |
|
activateKey(SHIFT_KEY, COLORS.normal); |
|
shiftActive = true; |
|
} |
|
activateKey(dataKey, color); |
|
return; |
|
} |
|
|
|
// Regular key detection |
|
if (numberKeyMap[e.key]) { |
|
dataKey = numberKeyMap[e.key]; |
|
} else if (specialKeyMap[e.key]) { |
|
dataKey = specialKeyMap[e.key]; |
|
} else if (operationKeyMap[e.key]) { |
|
dataKey = operationKeyMap[e.key]; |
|
} |
|
|
|
// Activate visual feedback if we found a matching data-key |
|
if (dataKey !== null) { |
|
activateKey(dataKey, color); |
|
} |
|
}, false); |
|
|
|
// Clear visual feedback on keyup |
|
window.addEventListener('keyup', function(e) { |
|
// Ignore CapsLock |
|
if (e.key === 'CapsLock') { |
|
return; |
|
} |
|
|
|
let dataKey = null; |
|
|
|
// Check all key maps |
|
if (isAlphaKey(e.key)) { |
|
dataKey = alphaKeyMap[e.key]; |
|
// Deactivate the specific key |
|
if (dataKey !== null) { |
|
deactivateKey(dataKey); |
|
} |
|
// Clear alpha modifier state |
|
deactivateKey(ALPHA_KEY); |
|
alphaActive = false; |
|
return; |
|
} |
|
|
|
if (bracketKeyMap.hasOwnProperty(e.key)) { |
|
dataKey = bracketKeyMap[e.key]; |
|
// Deactivate the specific key |
|
if (dataKey !== null) { |
|
deactivateKey(dataKey); |
|
} |
|
// Clear shift modifier state |
|
deactivateKey(SHIFT_KEY); |
|
shiftActive = false; |
|
return; |
|
} |
|
|
|
if (numberKeyMap[e.key]) { |
|
dataKey = numberKeyMap[e.key]; |
|
} else if (specialKeyMap[e.key]) { |
|
dataKey = specialKeyMap[e.key]; |
|
} else if (operationKeyMap[e.key]) { |
|
dataKey = operationKeyMap[e.key]; |
|
} |
|
|
|
if (dataKey !== null) { |
|
deactivateKey(dataKey); |
|
} |
|
}, false); |
|
|
|
// Extra safety: detect when modifiers are released directly |
|
window.addEventListener('keyup', function(e) { |
|
if (e.key === 'Shift') { |
|
deactivateKey(SHIFT_KEY); |
|
shiftActive = false; |
|
} else if (e.key === 'Alt' || e.key === 'Control' || e.key === 'Meta') { |
|
// Clear alpha on any modifier release as safety |
|
deactivateKey(ALPHA_KEY); |
|
alphaActive = false; |
|
} |
|
}, true); |
|
|
|
// ============================================ |
|
// Part 3: Keyboard Shortcuts for Calculator Buttons |
|
// ============================================ |
|
|
|
// Map of data-key values to button names (for reference) |
|
const buttonMap = { |
|
5: 'Back', |
|
6: 'Home', |
|
12: 'Shift', |
|
13: 'Alpha', |
|
15: 'Var', |
|
16: 'Toolbox', |
|
17: 'Backspace' |
|
}; |
|
|
|
// Helper function to click a calculator button using pointer events |
|
function clickButton(dataKey, showVisualFeedback = true, color = COLORS.normal) { |
|
const button = document.querySelector(`[data-key="${dataKey}"]`); |
|
|
|
if (!button) { |
|
// console.warn(`[Numworks] Button not found: data-key="${dataKey}"`); |
|
return false; |
|
} |
|
|
|
// Show visual feedback (flash for simulated clicks) |
|
if (showVisualFeedback) { |
|
flashActiveKey(dataKey, color); |
|
} |
|
|
|
// Get the center position of the button |
|
const rect = button.getBoundingClientRect(); |
|
const clientX = rect.left + rect.width / 2; |
|
const clientY = rect.top + rect.height / 2; |
|
|
|
// Create pointer down event |
|
const pointerDown = new PointerEvent("pointerdown", { |
|
bubbles: true, |
|
cancelable: true, |
|
pointerId: 1, |
|
pointerType: "mouse", |
|
isPrimary: true, |
|
clientX, |
|
clientY |
|
}); |
|
|
|
// Create pointer up event |
|
const pointerUp = new PointerEvent("pointerup", { |
|
bubbles: true, |
|
cancelable: true, |
|
pointerId: 1, |
|
pointerType: "mouse", |
|
isPrimary: true, |
|
clientX, |
|
clientY |
|
}); |
|
|
|
// Dispatch the events |
|
button.dispatchEvent(pointerDown); |
|
button.dispatchEvent(pointerUp); |
|
|
|
// console.log(`[Numworks] Clicked button: ${buttonMap[dataKey] || dataKey}`); |
|
return true; |
|
} |
|
|
|
// Helper function to click two buttons in sequence with color coding |
|
function clickSequence(modifierKey, targetKey, modifierColor = COLORS.normal, targetColor = COLORS.shift, delay = 5) { |
|
clickButton(modifierKey, true, modifierColor); |
|
setTimeout(() => clickButton(targetKey, true, targetColor), delay); |
|
} |
|
|
|
// Track if we're currently processing a shortcut to prevent duplicates |
|
let processingShortcut = false; |
|
|
|
// Keyboard shortcut handler |
|
window.addEventListener('keydown', function(e) { |
|
// Prevent duplicate processing |
|
if (processingShortcut) { |
|
// console.log('[Numworks] Already processing shortcut, skipping...'); |
|
return; |
|
} |
|
|
|
let handled = false; |
|
|
|
// Escape: Back button |
|
if (e.key === 'Escape') { |
|
e.preventDefault(); |
|
e.stopImmediatePropagation(); |
|
e.stopPropagation(); |
|
processingShortcut = true; |
|
// console.log('[Numworks] Shortcut: Back'); |
|
clickButton(5); // Back (5) |
|
setTimeout(() => { processingShortcut = false; }, 100); |
|
handled = true; |
|
} |
|
|
|
// Ctrl+Backspace or Cmd+Backspace: Shift + Backspace (Clear field) |
|
else if ((e.ctrlKey || e.metaKey) && e.key === 'Backspace') { |
|
e.preventDefault(); |
|
e.stopImmediatePropagation(); |
|
e.stopPropagation(); |
|
processingShortcut = true; |
|
// console.log('[Numworks] Shortcut: Clear field (Shift+Backspace)'); |
|
clickSequence(SHIFT_KEY, 17, COLORS.normal, COLORS.shift, 5); |
|
setTimeout(() => { processingShortcut = false; }, 100); |
|
handled = true; |
|
} |
|
|
|
// Ctrl+C or Cmd+C: Shift + Var (Copy) |
|
else if ((e.ctrlKey || e.metaKey) && e.key === 'c') { |
|
e.preventDefault(); |
|
e.stopImmediatePropagation(); |
|
e.stopPropagation(); |
|
processingShortcut = true; |
|
// console.log('[Numworks] Shortcut: Copy (Shift+Var)'); |
|
clickSequence(SHIFT_KEY, 15, COLORS.normal, COLORS.shift, 5); |
|
setTimeout(() => { processingShortcut = false; }, 100); |
|
handled = true; |
|
} |
|
|
|
// Ctrl+V or Cmd+V: Shift + Toolbox (Paste) |
|
else if ((e.ctrlKey || e.metaKey) && e.key === 'v') { |
|
e.preventDefault(); |
|
e.stopImmediatePropagation(); |
|
e.stopPropagation(); |
|
processingShortcut = true; |
|
// console.log('[Numworks] Shortcut: Paste (Shift+Toolbox)'); |
|
clickSequence(SHIFT_KEY, 16, COLORS.normal, COLORS.shift, 5); |
|
setTimeout(() => { processingShortcut = false; }, 100); |
|
handled = true; |
|
} |
|
|
|
// Ctrl+T (NOT Cmd+T to avoid new tab on macOS): Toolbox |
|
else if (e.ctrlKey && !e.metaKey && e.key === 't') { |
|
e.preventDefault(); |
|
e.stopImmediatePropagation(); |
|
e.stopPropagation(); |
|
processingShortcut = true; |
|
// console.log('[Numworks] Shortcut: Toolbox'); |
|
clickButton(16); // Toolbox (16) |
|
setTimeout(() => { processingShortcut = false; }, 100); |
|
handled = true; |
|
} |
|
|
|
// Ctrl+H (NOT Cmd+H to avoid hide on macOS): Home |
|
else if (e.ctrlKey && !e.metaKey && e.key === 'h') { |
|
e.preventDefault(); |
|
e.stopImmediatePropagation(); |
|
e.stopPropagation(); |
|
processingShortcut = true; |
|
// console.log('[Numworks] Shortcut: Home'); |
|
clickButton(6); // Home (6) |
|
setTimeout(() => { processingShortcut = false; }, 100); |
|
handled = true; |
|
} |
|
|
|
if (handled) { |
|
// console.log('[Numworks] Shortcut handled successfully'); |
|
} |
|
}, true); // Capture phase to catch it early |
|
} |
|
|
|
// Initialize when ready |
|
if (document.readyState === 'loading') { |
|
document.addEventListener('DOMContentLoaded', initialize); |
|
} else { |
|
setTimeout(initialize, 1000); |
|
} |
|
})(); |