Checkpoint current interactive fiction state
This commit is contained in:
@@ -5,7 +5,7 @@ class UIInputHandlerModule extends BaseModule {
|
||||
super('ui-input-handler', 'UI Input Handler');
|
||||
|
||||
// Explicitly declare ui-display-handler as a dependency
|
||||
this.dependencies = ['ui-display-handler'];
|
||||
this.dependencies = ['ui-display-handler', 'markup-parser', 'playback-coordinator'];
|
||||
|
||||
// Input elements
|
||||
this.inputArea = null;
|
||||
@@ -28,7 +28,10 @@ class UIInputHandlerModule extends BaseModule {
|
||||
'handleKeyboardInput',
|
||||
'submitCommand',
|
||||
'addToHistory',
|
||||
'resetCursorPosition'
|
||||
'formatCommandHistory',
|
||||
'resetCursorPosition',
|
||||
'focusInput',
|
||||
'setProcessState'
|
||||
]);
|
||||
|
||||
console.log('UIInputHandler: Constructor initialized');
|
||||
@@ -53,6 +56,9 @@ class UIInputHandlerModule extends BaseModule {
|
||||
this.reportProgress(60, 'Setting up input elements');
|
||||
|
||||
this.setupInputElements();
|
||||
this.addEventListener(document, 'story:process-state', (event) => {
|
||||
this.setProcessState(event.detail?.state || 'ready', event.detail || {});
|
||||
});
|
||||
|
||||
this.reportProgress(100, 'UI Input Handler ready');
|
||||
return true;
|
||||
@@ -67,14 +73,47 @@ class UIInputHandlerModule extends BaseModule {
|
||||
* @param {KeyboardEvent} event - The keyboard event
|
||||
*/
|
||||
handleKeyboardInput(event) {
|
||||
// Handle global keyboard shortcuts here
|
||||
// This is different from the input field's specific key handling
|
||||
|
||||
// For example: Escape key to blur the input
|
||||
if (!this.playerInput) return;
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
if (document.activeElement === this.playerInput) {
|
||||
this.playerInput.blur();
|
||||
this.playerInput.blur();
|
||||
return;
|
||||
}
|
||||
|
||||
const optionsModal = document.getElementById('options-modal');
|
||||
if (optionsModal && optionsModal.style.display !== 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === ' ' && this.isPlaybackActive()) {
|
||||
document.dispatchEvent(new CustomEvent('ui:command', {
|
||||
detail: { type: 'continue', source: 'spacebar' }
|
||||
}));
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
if (document.body.dataset.gameRunning !== 'true') {
|
||||
return;
|
||||
}
|
||||
this.submitCommand();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.ctrlKey || event.metaKey || event.altKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key.length === 1 && document.activeElement !== this.playerInput) {
|
||||
if (document.body.dataset.gameRunning !== 'true') {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this.focusInput();
|
||||
const start = this.playerInput.selectionStart ?? this.playerInput.value.length;
|
||||
const end = this.playerInput.selectionEnd ?? start;
|
||||
this.playerInput.setRangeText(event.key, start, end, 'end');
|
||||
this.playerInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,8 +183,8 @@ class UIInputHandlerModule extends BaseModule {
|
||||
playerInput.style.whiteSpace = 'pre-wrap';
|
||||
|
||||
inputWrapper.appendChild(playerInput);
|
||||
this.playerInput = playerInput;
|
||||
}
|
||||
this.playerInput = playerInput;
|
||||
|
||||
// Create the cursor if needed
|
||||
let cursor = document.getElementById('cursor');
|
||||
@@ -153,8 +192,8 @@ class UIInputHandlerModule extends BaseModule {
|
||||
cursor = document.createElement('span');
|
||||
cursor.id = 'cursor';
|
||||
inputWrapper.appendChild(cursor);
|
||||
this.cursor = cursor;
|
||||
}
|
||||
this.cursor = cursor;
|
||||
|
||||
// Set up input event handlers
|
||||
if (playerInput) {
|
||||
@@ -171,16 +210,94 @@ class UIInputHandlerModule extends BaseModule {
|
||||
// Position the cursor
|
||||
if (playerInput && cursor) {
|
||||
this.positionCursor(playerInput, cursor);
|
||||
|
||||
// Focus the input to let user start typing immediately
|
||||
setTimeout(() => {
|
||||
playerInput.focus();
|
||||
}, 100);
|
||||
this.setProcessState('ready', { reason: 'input-initialized' });
|
||||
this.focusInput();
|
||||
requestAnimationFrame(() => this.focusInput());
|
||||
setTimeout(() => this.focusInput(), 250);
|
||||
}
|
||||
|
||||
console.log('UIInputHandler: Input elements setup complete');
|
||||
}
|
||||
|
||||
focusInput() {
|
||||
if (document.body.dataset.gameRunning !== 'true') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.playerInput) {
|
||||
this.playerInput = document.getElementById('player_input');
|
||||
}
|
||||
|
||||
if (this.playerInput && !this.playerInput.disabled) {
|
||||
this.playerInput.focus();
|
||||
}
|
||||
}
|
||||
|
||||
setProcessState(state, detail = {}) {
|
||||
const knownStates = [
|
||||
'ready',
|
||||
'command-waiting',
|
||||
'waiting-generating',
|
||||
'playing-generating',
|
||||
'playing-ready'
|
||||
];
|
||||
const nextState = knownStates.includes(state) ? state : 'ready';
|
||||
|
||||
this.applyMouseCursor(nextState);
|
||||
|
||||
if (this.cursor) {
|
||||
knownStates.forEach(value => this.cursor.classList.remove(`cursor-${value}`));
|
||||
this.cursor.classList.add(`cursor-${nextState}`);
|
||||
this.cursor.dataset.processState = nextState;
|
||||
this.cursor.setAttribute('aria-label', 'text input cursor');
|
||||
this.cursor.innerHTML = '';
|
||||
}
|
||||
|
||||
console.log(`Cursor process state: ${nextState}`, detail);
|
||||
}
|
||||
|
||||
applyMouseCursor(state) {
|
||||
const root = document.documentElement;
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
|
||||
root.dataset.processState = state;
|
||||
const cursor = this.getMouseCursor(state);
|
||||
if (cursor) {
|
||||
root.style.setProperty('--process-cursor', cursor);
|
||||
} else {
|
||||
root.style.removeProperty('--process-cursor');
|
||||
}
|
||||
}
|
||||
|
||||
getMouseCursor(state) {
|
||||
if (state === 'ready') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const svg = this.getMouseCursorSvg(state);
|
||||
const fallback = state === 'command-waiting' ? 'wait' : 'progress';
|
||||
return `url("${this.toCursorDataUrl(svg)}") 12 12, ${fallback}`;
|
||||
}
|
||||
|
||||
getMouseCursorSvg(state) {
|
||||
const stroke = '#222222';
|
||||
const common = `xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="${stroke}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"`;
|
||||
const icons = {
|
||||
'command-waiting': `<svg ${common}><path d="M5 22h14"/><path d="M5 2h14"/><path d="M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22"/><path d="M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2"/></svg>`,
|
||||
'waiting-generating': `<svg ${common}><path d="M12 2v4"/><path d="M12 18v4"/><path d="m4.93 4.93 2.83 2.83"/><path d="m16.24 16.24 2.83 2.83"/><path d="M2 12h4"/><path d="M18 12h4"/><path d="m4.93 19.07 2.83-2.83"/><path d="m16.24 7.76 2.83-2.83"/></svg>`,
|
||||
'playing-generating': `<svg ${common}><path d="M11 5 6 9H2v6h4l5 4z"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/><path d="M12 2v3"/><path d="M12 19v3"/></svg>`,
|
||||
'playing-ready': `<svg ${common}><path d="M11 5 6 9H2v6h4l5 4z"/><path d="M15.54 8.46a5 5 0 0 1 0 7.07"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14"/></svg>`
|
||||
};
|
||||
|
||||
return icons[state] || icons['waiting-generating'];
|
||||
}
|
||||
|
||||
toCursorDataUrl(svg) {
|
||||
return `data:image/svg+xml,${encodeURIComponent(svg.replace(/\s+/g, ' ').trim())}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle player input changes
|
||||
* @param {Event} e - Input event
|
||||
@@ -271,7 +388,7 @@ class UIInputHandlerModule extends BaseModule {
|
||||
if (this.commandHistoryElement && this.commandHistoryElement.appendChild) {
|
||||
const historyItem = document.createElement('div');
|
||||
historyItem.className = 'history-item';
|
||||
historyItem.textContent = `> ${command}`;
|
||||
historyItem.innerHTML = `> ${this.formatCommandHistory(command)}`;
|
||||
this.commandHistoryElement.appendChild(historyItem);
|
||||
|
||||
// Limit visible history items
|
||||
@@ -284,6 +401,23 @@ class UIInputHandlerModule extends BaseModule {
|
||||
}
|
||||
}
|
||||
|
||||
formatCommandHistory(command) {
|
||||
const parser = this.getModule('markup-parser') || window.MarkupParser;
|
||||
if (parser && typeof parser.markdownToHtml === 'function') {
|
||||
return parser.markdownToHtml(command);
|
||||
}
|
||||
|
||||
return String(command)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
isPlaybackActive() {
|
||||
const playbackCoordinator = this.getModule('playback-coordinator') || window.PlaybackCoordinator;
|
||||
return Boolean(playbackCoordinator && playbackCoordinator.isPlaying);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the cursor position to the start.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user