diff --git a/src/algo/AVL.js b/src/algo/AVL.js index 433fa6f..1c741c3 100644 --- a/src/algo/AVL.js +++ b/src/algo/AVL.js @@ -59,6 +59,7 @@ export default class AVL extends Algorithm { 4, false, ); + this.insertField.setAttribute('data-shortcut-target', 'insert'); this.controls.push(this.insertField); this.insertButton = addControlToAlgorithmBar('Button', 'Insert'); @@ -75,6 +76,7 @@ export default class AVL extends Algorithm { 4, false, ); + this.deleteField.setAttribute('data-shortcut-target', 'delete'); this.controls.push(this.deleteField); this.deleteButton = addControlToAlgorithmBar('Button', 'Delete'); @@ -91,6 +93,7 @@ export default class AVL extends Algorithm { 4, false, ); + this.findField.setAttribute('data-shortcut-target', 'find'); this.controls.push(this.findField); this.findButton = addControlToAlgorithmBar('Button', 'Find'); @@ -311,12 +314,12 @@ export default class AVL extends Algorithm { act.setText, 0, 'Searching for ' + - value + - ' : ' + - value + - ' < ' + - tree.data + - ' (look to left subtree)', + value + + ' : ' + + value + + ' < ' + + tree.data + + ' (look to left subtree)', ); this.cmd(act.step); this.cmd(act.setHighlight, tree.graphicID, 0); @@ -338,12 +341,12 @@ export default class AVL extends Algorithm { act.setText, 0, ' Searching for ' + - value + - ' : ' + - value + - ' > ' + - tree.data + - ' (look to right subtree)', + value + + ' : ' + + value + + ' > ' + + tree.data + + ' (look to right subtree)', ); this.cmd(act.step); this.cmd(act.setHighlight, tree.graphicID, 0); @@ -438,7 +441,7 @@ export default class AVL extends Algorithm { this.resizeTree(); const connected = this.connectSmart(curr.graphicID, curr.left.graphicID); connected && this.cmd(act.step); - // } else if (data > curr.data) { + // } else if (data > curr.data) { } else if (this.compare(data, curr.data) > 0) { this.cmd(act.setText, 0, `${data} > ${curr.data}. Looking at right subtree`); this.cmd(act.step); @@ -683,7 +686,7 @@ export default class AVL extends Algorithm { this.connectSmart(curr.graphicID, curr.left.graphicID); this.resizeTree(); } - // } else if (data > curr.data) { + // } else if (data > curr.data) { } else if (this.compare(data, curr.data) > 0) { this.cmd(act.setText, 0, `${data} > ${curr.data}. Looking right`); this.cmd(act.step); diff --git a/src/algo/ArrayList.js b/src/algo/ArrayList.js index 4bb1de7..5f6768e 100644 --- a/src/algo/ArrayList.js +++ b/src/algo/ArrayList.js @@ -82,6 +82,7 @@ export default class ArrayList extends Algorithm { 4, false, ); + this.addValueField.setAttribute('data-shortcut-target', 'insert'); this.controls.push(this.addValueField); addLabelToAlgorithmBar('at index', addTopHorizontalGroup); @@ -143,6 +144,7 @@ export default class ArrayList extends Algorithm { 4, true, ); + this.removeField.setAttribute('data-shortcut-target', 'delete'); this.controls.push(this.removeField); // Remove from index button diff --git a/src/algo/BST.js b/src/algo/BST.js index e1e3111..171b74f 100644 --- a/src/algo/BST.js +++ b/src/algo/BST.js @@ -75,6 +75,7 @@ export default class BST extends Algorithm { 4, false, ); + this.insertField.setAttribute('data-shortcut-target', 'insert'); this.controls.push(this.insertField); this.insertButton = addControlToAlgorithmBar('Button', 'Insert'); @@ -91,6 +92,7 @@ export default class BST extends Algorithm { 4, false, ); + this.deleteField.setAttribute('data-shortcut-target', 'delete'); this.controls.push(this.deleteField); this.deleteButton = addControlToAlgorithmBar('Button', 'Delete'); @@ -107,6 +109,7 @@ export default class BST extends Algorithm { 4, false, ); + this.findField.setAttribute('data-shortcut-target', 'find'); this.controls.push(this.findField); this.findButton = addControlToAlgorithmBar('Button', 'Find'); @@ -603,12 +606,12 @@ export default class BST extends Algorithm { act.setText, 0, 'Searching for ' + - value + - ' : ' + - value + - ' < ' + - tree.data + - ' (look to left subtree)', + value + + ' : ' + + value + + ' < ' + + tree.data + + ' (look to left subtree)', ); this.cmd(act.step); @@ -635,12 +638,12 @@ export default class BST extends Algorithm { act.setText, 0, ' Searching for ' + - value + - ' : ' + - value + - ' > ' + - tree.data + - ' (look to right subtree)', + value + + ' : ' + + value + + ' > ' + + tree.data + + ' (look to right subtree)', ); this.cmd(act.step); this.cmd(act.setHighlight, tree.graphicID, 0, 'find'); @@ -765,7 +768,7 @@ export default class BST extends Algorithm { this.resizeTree(); const connected = this.connectSmart(curr.graphicID, curr.left.graphicID); connected && this.cmd(act.step); - // } else if (data > curr.data) { + // } else if (data > curr.data) { } else if (this.compare(data, curr.data) > 0) { this.highlight(10, 0, 'add'); this.highlight(11, 0, 'add'); @@ -865,7 +868,7 @@ export default class BST extends Algorithm { this.connectSmart(curr.graphicID, curr.left.graphicID); this.resizeTree(); } - // } else if (data > curr.data) { + // } else if (data > curr.data) { } else if (this.compare(data, curr.data)) { this.highlight(11, 0, this.predSuccMethod); this.highlight(12, 0, this.predSuccMethod); diff --git a/src/algo/LinkedList.js b/src/algo/LinkedList.js index 4d8644f..3f77328 100644 --- a/src/algo/LinkedList.js +++ b/src/algo/LinkedList.js @@ -97,6 +97,7 @@ export default class LinkedList extends Algorithm { 4, false, ); + this.addValueField.setAttribute('data-shortcut-target', 'insert'); this.controls.push(this.addValueField); addLabelToAlgorithmBar('at index', addTopHorizontalGroup); @@ -158,6 +159,7 @@ export default class LinkedList extends Algorithm { 4, true, ); + this.removeField.setAttribute('data-shortcut-target', 'delete'); this.controls.push(this.removeField); // Remove from index button @@ -838,8 +840,8 @@ export default class LinkedList extends Algorithm { runningRemoveIndexOnly ? this.highlight(12, 0, 'removeIndex') : runningRemoveBack - ? this.highlight(10, 0, 'removeBack') - : this.highlight(3, 0, 'removeFront'); + ? this.highlight(10, 0, 'removeBack') + : this.highlight(3, 0, 'removeFront'); this.cmd(act.step); this.cmd(act.delete, this.linkedListElemID[index]); @@ -863,8 +865,8 @@ export default class LinkedList extends Algorithm { runningRemoveIndexOnly ? this.highlight(13, 0, 'removeIndex') : runningRemoveBack - ? this.highlight(11, 0, 'removeBack') - : this.highlight(4, 0, 'removeFront'); + ? this.highlight(11, 0, 'removeBack') + : this.highlight(4, 0, 'removeFront'); this.cmd(act.step); return this.commands; diff --git a/src/components/AlgoScreen/AlgoSection.js b/src/components/AlgoScreen/AlgoSection.js index b7e3745..65655e2 100644 --- a/src/components/AlgoScreen/AlgoSection.js +++ b/src/components/AlgoScreen/AlgoSection.js @@ -1,7 +1,7 @@ import '../../css/AlgoScreen.css'; import '../../css/App.css'; -import { BsBookHalf, BsClock, BsCodeSlash, BsInfoCircle, BsTranslate } from 'react-icons/bs'; +import { BsBookHalf, BsClock, BsCodeSlash, BsInfoCircle, BsKeyboard, BsTranslate } from 'react-icons/bs'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useLocation, useSearchParams } from 'react-router-dom'; import AlgorithmNotFound404 from '../../components/AlgorithmNotFound404'; @@ -10,6 +10,7 @@ import BigOModal from '../../modals/BigOModal'; import ModalSidebarSectionTab from '../../components/AlgoScreen/ModalSidebarSectionTab'; import Pseudocode from '../../components/AlgoScreen/Pseudocode'; import ReactGA from 'react-ga4'; +import { SHORTCUTS_CONFIG } from './ShortcutConfig'; import { algoMap } from '../../AlgoList'; import infoModals from '../../modals/InfoModals'; import pseudocodeText from '../../pseudocode.json'; @@ -77,6 +78,59 @@ const AlgoSection = ({ theme }) => { }, [pseudocodeData]); // Handle page view and animation setup + useEffect(() => { + const handleKeyDown = (e) => { + // Ignore if focus is on an input or textarea + if ( + ['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName) || + document.activeElement.isContentEditable + ) { + return; + } + + const key = e.key; + + // Toggle shortcuts with '?' or 'Ctrl+/' + if (key === '?' || (e.ctrlKey && key === '/')) { + e.preventDefault(); + if (!infoModalEnabled) { + setInfoModalEnabled(true); + setInfoModalTab('shortcuts'); + } else { + // If modal is open, verify if it's on shortcuts tab, if so close, else switch + if (infoModalTab === 'shortcuts') { + setInfoModalEnabled(false); + } else { + setInfoModalTab('shortcuts'); + } + } + return; + } + + // Ignore if modifier keys are pressed (except Shift for ?) + if (e.ctrlKey || e.altKey || e.metaKey) return; + + const config = SHORTCUTS_CONFIG[algoName]; + if (config && config[key]) { + e.preventDefault(); + const targetId = config[key].target; + const element = document.querySelector(`[data-shortcut-target="${targetId}"]`); + if (element) { + element.focus(); + if (element.select) { + element.select(); + } + // Add a temporary highlight class + element.classList.add('shortcut-highlight'); + setTimeout(() => element.classList.remove('shortcut-highlight'), 500); + } + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [algoName, infoModalEnabled, infoModalTab]); + useEffect(() => { ReactGA.send({ hitType: 'pageview', page: algoName }); @@ -113,7 +167,7 @@ const AlgoSection = ({ theme }) => { animManagRef.current.addListener("AnimationStarted", null, () => { if (pseudocodeDataRef.current && !modalOpenedRef.current) { - modalOpenedRef.current = true; + modalOpenedRef.current = true; setInfoModalEnabled(true); setInfoModalTab('code'); } @@ -150,7 +204,29 @@ const AlgoSection = ({ theme }) => { const togglePseudocodeType = () => { setPseudocodeType(prev => (prev === 'english' ? 'code' : 'english')); }; - + + useEffect(() => { + const handleClickOutside = (event) => { + if (infoModalEnabled && canvasRef.current && canvasRef.current.contains(event.target)) { + // Clicked on canvas (outside modal) + setInfoModalEnabled(false); + } + + // Also check if clicked on the main container but not the modal + // The modal is absolute positioned, so we might need a specific ref for it if the above isn't enough. + // However, the modal is inside "viewport" div. + const modal = document.querySelector('.modal.info-modal'); + if (infoModalEnabled && modal && !modal.contains(event.target) && !event.target.closest('.menu-modal')) { + setInfoModalEnabled(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [infoModalEnabled]); + if (!algoDetails) { return ; } @@ -167,10 +243,27 @@ const AlgoSection = ({ theme }) => { className="menu-modal" size={30} onClick={toggleInfoModal} - opacity={infoModalEnabled ? '100%' : '40%'} + opacity={infoModalEnabled && infoModalTab !== 'shortcuts' ? '100%' : '40%'} title="Information & Documentation" /> )} + {SHORTCUTS_CONFIG[algoName] && ( + { + if (infoModalEnabled && infoModalTab === 'shortcuts') { + setInfoModalEnabled(false); + } else { + setInfoModalEnabled(true); + setInfoModalTab('shortcuts'); + } + }} + opacity={infoModalEnabled && infoModalTab === 'shortcuts' ? '100%' : '40%'} + title="Keyboard Shortcuts" + style={{ marginLeft: '10px' }} + /> + )} @@ -208,6 +301,15 @@ const AlgoSection = ({ theme }) => { title={'Big O'} /> )} + {SHORTCUTS_CONFIG[algoName] && ( + setInfoModalTab('shortcuts')} + currentTab={infoModalTab} + name={'shortcuts'} + icon={BsKeyboard} + title={'Shortcuts'} + /> + )} {infoModalTab === 'about' && infoModals[algoName] && ( @@ -243,6 +345,48 @@ const AlgoSection = ({ theme }) => { )} + + {infoModalTab === 'shortcuts' && SHORTCUTS_CONFIG[algoName] && ( +
+

Keyboard Shortcuts

+
+ {Object.entries(SHORTCUTS_CONFIG[algoName]).map(([key, { label }]) => ( + + {key} + {label} + + ))} + + ? + Toggle shortcuts help + +
+
+ )} )} diff --git a/src/components/AlgoScreen/ShortcutConfig.js b/src/components/AlgoScreen/ShortcutConfig.js new file mode 100644 index 0000000..e948bd0 --- /dev/null +++ b/src/components/AlgoScreen/ShortcutConfig.js @@ -0,0 +1,22 @@ +export const SHORTCUTS_CONFIG = { + BST: { + i: { label: 'Focus Insert', target: 'insert' }, + d: { label: 'Focus Delete', target: 'delete' }, + f: { label: 'Focus Find', target: 'find' }, + }, + AVL: { + i: { label: 'Focus Insert', target: 'insert' }, + d: { label: 'Focus Delete', target: 'delete' }, + f: { label: 'Focus Find', target: 'find' }, + }, + LinkedList: { + i: { label: 'Focus Add Value', target: 'insert' }, + d: { label: 'Focus Remove Index', target: 'delete' }, + }, + ArrayList: { + i: { label: 'Focus Add Value', target: 'insert' }, + d: { label: 'Focus Remove Index', target: 'delete' }, + }, + // Common shortcuts (handled globally but listed here for help visibility if needed, + // though help usually just shows page-specific ones. ? is global). +}; diff --git a/src/css/AlgoScreen.css b/src/css/AlgoScreen.css index abefa11..137283e 100644 --- a/src/css/AlgoScreen.css +++ b/src/css/AlgoScreen.css @@ -29,6 +29,7 @@ * { -webkit-appearance: none; -moz-appearance: none; + appearance: none; } .VisualizationMainPage body { @@ -48,7 +49,8 @@ .VisualizationMainPage #container { background: #ffffff; /* margin: 0 auto; /* the auto margins (in conjunction with a width) center the page */ - text-align: left; /* this overrides the text-align: center on the body element. */ + text-align: left; + /* this overrides the text-align: center on the body element. */ height: 100vh; display: flex; flex-direction: column; @@ -196,13 +198,16 @@ em { filter: var(--filter); transform: scale(1.1); } + .pseudocode-toggle { cursor: pointer; transition: 0.2s; } + .pseudocode-toggle:hover { scale: 125%; } + .VisualizationMainPage .modal { position: absolute; right: 20px; @@ -213,7 +218,8 @@ em { } .VisualizationMainPage .bigo { - border-collapse: collapse; /* Ensures borders are not doubled */ + border-collapse: collapse; + /* Ensures borders are not doubled */ position: absolute; min-width: 40%; } @@ -223,7 +229,8 @@ em { color: var(--primary); padding: 5px; min-width: 50px; - border: 1px solid gray; /* Adds border to table cells */ + border: 1px solid gray; + /* Adds border to table cells */ } .bigo table { @@ -238,7 +245,8 @@ em { .bigo .blur { filter: blur(5px); - transition: filter 0.3s ease; /* Smooth transition */ + transition: filter 0.3s ease; + /* Smooth transition */ } .VisualizationMainPage .modal-content { @@ -252,8 +260,10 @@ em { } .VisualizationMainPage .modal-content ul { - list-style-type: none; /* Makes bullet points invisible */ - padding-left: 0; /* Removes default padding */ + list-style-type: none; + /* Makes bullet points invisible */ + padding-left: 0; + /* Removes default padding */ } .VisualizationMainPage .modal-content li { @@ -392,11 +402,11 @@ input[type='radio'] { margin-right: 2px; } -input[type='radio'] + label { +input[type='radio']+label { color: var(--primary); } -input[type='radio']:checked + label { +input[type='radio']:checked+label { padding: 1px 3px 1px 1px; background-color: rgba(176, 179, 184, 0.8); box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.4); @@ -412,19 +422,23 @@ input[type='checkbox'] { } @keyframes shake { + 10%, 90% { transform: translate3d(-1px, 0, 0); } + 20%, 80% { transform: translate3d(2px, 0, 0); } + 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); } + 40%, 60% { transform: translate3d(4px, 0, 0); @@ -517,6 +531,7 @@ input[type='checkbox'] { opacity: 0; transform: translateX(30px); } + to { opacity: 1; transform: translateX(0); @@ -781,6 +796,7 @@ input[type='checkbox'] { opacity: 0; transform: translateX(30px); } + to { opacity: 1; transform: translateX(0); @@ -1077,3 +1093,9 @@ input[type='checkbox'] { width: 100%; height: 90vh; } + +.shortcut-highlight { + outline: 2px solid #f9c333; + transition: outline 0.1s; + box-shadow: 0 0 10px #f9c333; +} \ No newline at end of file