Add ink integration UI and media playback
This commit is contained in:
@@ -18,6 +18,7 @@ class UIInputHandlerModule extends BaseModule {
|
||||
this.historyIndex = -1;
|
||||
this.commandHistory = [];
|
||||
this.inputBuffer = '';
|
||||
this.inputMode = 'text';
|
||||
|
||||
// Bind methods using the parent class bindMethods utility
|
||||
this.bindMethods([
|
||||
@@ -28,11 +29,14 @@ class UIInputHandlerModule extends BaseModule {
|
||||
'handleKeyboardInput',
|
||||
'submitCommand',
|
||||
'addToHistory',
|
||||
'bindHistoryToTurn',
|
||||
'highlightHistoryTurn',
|
||||
'formatCommandHistory',
|
||||
'resetCursorPosition',
|
||||
'focusInput',
|
||||
'setProcessState',
|
||||
'setInputAvailability',
|
||||
'setMode',
|
||||
'clearHistory'
|
||||
]);
|
||||
|
||||
@@ -61,6 +65,15 @@ class UIInputHandlerModule extends BaseModule {
|
||||
this.addEventListener(document, 'story:process-state', (event) => {
|
||||
this.setProcessState(event.detail?.state || 'ready', event.detail || {});
|
||||
});
|
||||
this.addEventListener(document, 'story:input-mode', (event) => {
|
||||
this.setMode(event.detail || 'text');
|
||||
});
|
||||
this.addEventListener(document, 'story:turn-start', (event) => {
|
||||
this.bindHistoryToTurn(event.detail?.turnId);
|
||||
});
|
||||
this.addEventListener(document, 'story:visible-turn', (event) => {
|
||||
this.highlightHistoryTurn(event.detail?.turnId);
|
||||
});
|
||||
|
||||
this.reportProgress(100, 'UI Input Handler ready');
|
||||
return true;
|
||||
@@ -87,7 +100,7 @@ class UIInputHandlerModule extends BaseModule {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === ' ' && this.isPlaybackActive()) {
|
||||
if (event.key === ' ' && (this.isPlaybackActive() || this.isSkippablePauseActive())) {
|
||||
document.dispatchEvent(new CustomEvent('ui:command', {
|
||||
detail: { type: 'continue', source: 'spacebar' }
|
||||
}));
|
||||
@@ -95,7 +108,7 @@ class UIInputHandlerModule extends BaseModule {
|
||||
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
if (document.body.dataset.gameRunning !== 'true') {
|
||||
if (document.body.dataset.gameRunning !== 'true' || this.inputMode !== 'text') {
|
||||
return;
|
||||
}
|
||||
this.submitCommand();
|
||||
@@ -110,6 +123,9 @@ class UIInputHandlerModule extends BaseModule {
|
||||
if (document.body.dataset.gameRunning !== 'true') {
|
||||
return;
|
||||
}
|
||||
if (this.inputMode !== 'text') {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this.focusInput();
|
||||
const start = this.playerInput.selectionStart ?? this.playerInput.value.length;
|
||||
@@ -176,8 +192,6 @@ class UIInputHandlerModule extends BaseModule {
|
||||
playerInput.id = 'player_input';
|
||||
playerInput.rows = 1;
|
||||
playerInput.placeholder = 'What will you do?';
|
||||
playerInput.setAttribute('autocomplete', 'off');
|
||||
playerInput.setAttribute('spellcheck', 'true');
|
||||
|
||||
// Fix horizontal scrolling by ensuring the textbox wraps text
|
||||
playerInput.style.overflowX = 'hidden';
|
||||
@@ -187,6 +201,7 @@ class UIInputHandlerModule extends BaseModule {
|
||||
inputWrapper.appendChild(playerInput);
|
||||
}
|
||||
this.playerInput = playerInput;
|
||||
this.applyTextInputAttributes(playerInput);
|
||||
|
||||
// Create the cursor if needed
|
||||
let cursor = document.getElementById('cursor');
|
||||
@@ -260,7 +275,7 @@ class UIInputHandlerModule extends BaseModule {
|
||||
}
|
||||
|
||||
setInputAvailability(enabled) {
|
||||
this.inputEnabled = Boolean(enabled);
|
||||
this.inputEnabled = Boolean(enabled) && this.inputMode === 'text';
|
||||
const commandInput = document.getElementById('command_input');
|
||||
if (commandInput) {
|
||||
commandInput.classList.toggle('fading', !this.inputEnabled);
|
||||
@@ -276,6 +291,31 @@ class UIInputHandlerModule extends BaseModule {
|
||||
}
|
||||
}
|
||||
|
||||
applyTextInputAttributes(playerInput) {
|
||||
if (!playerInput) return;
|
||||
|
||||
const attributes = {
|
||||
autocomplete: 'off',
|
||||
autocorrect: 'off',
|
||||
autocapitalize: 'sentences',
|
||||
spellcheck: 'true',
|
||||
'aria-autocomplete': 'none',
|
||||
'data-form-type': 'other',
|
||||
'data-1p-ignore': 'true',
|
||||
'data-lpignore': 'true',
|
||||
'data-bwignore': 'true'
|
||||
};
|
||||
|
||||
Object.entries(attributes).forEach(([name, value]) => {
|
||||
playerInput.setAttribute(name, value);
|
||||
});
|
||||
}
|
||||
|
||||
setMode(mode) {
|
||||
this.inputMode = ['text', 'choice', 'end'].includes(mode) ? mode : 'text';
|
||||
this.setInputAvailability(this.inputMode === 'text');
|
||||
}
|
||||
|
||||
applyMouseCursor(state) {
|
||||
const root = document.documentElement;
|
||||
if (!root) {
|
||||
@@ -365,6 +405,7 @@ class UIInputHandlerModule extends BaseModule {
|
||||
submitCommand() {
|
||||
if (!this.playerInput || !this.playerInput.value.trim()) return;
|
||||
if (document.body.dataset.gameRunning !== 'true' || !this.inputEnabled) return;
|
||||
if (this.inputMode !== 'text') return;
|
||||
|
||||
const command = this.playerInput.value.trim();
|
||||
console.log(`UIInputHandler: Submitting command: "${command}"`);
|
||||
@@ -420,7 +461,15 @@ class UIInputHandlerModule extends BaseModule {
|
||||
if (this.commandHistoryElement && this.commandHistoryElement.appendChild) {
|
||||
const historyItem = document.createElement('div');
|
||||
historyItem.className = 'history-item';
|
||||
historyItem.dataset.turnId = 'pending';
|
||||
historyItem.innerHTML = `> ${this.formatCommandHistory(command)}`;
|
||||
historyItem.addEventListener('click', () => {
|
||||
const turnId = historyItem.dataset.turnId;
|
||||
if (!turnId || turnId === 'pending') return;
|
||||
document.dispatchEvent(new CustomEvent('story:scroll-to-turn', {
|
||||
detail: { turnId: Number(turnId) }
|
||||
}));
|
||||
});
|
||||
this.commandHistoryElement.appendChild(historyItem);
|
||||
|
||||
// Limit visible history items
|
||||
@@ -433,6 +482,25 @@ class UIInputHandlerModule extends BaseModule {
|
||||
}
|
||||
}
|
||||
|
||||
bindHistoryToTurn(turnId) {
|
||||
if (!Number.isInteger(Number(turnId))) return;
|
||||
if (!this.commandHistoryElement) {
|
||||
this.commandHistoryElement = document.getElementById('command_history');
|
||||
}
|
||||
const pending = this.commandHistoryElement?.querySelector('.history-item[data-turn-id="pending"]');
|
||||
if (!pending) return;
|
||||
pending.dataset.turnId = String(turnId);
|
||||
pending.classList.remove('history-pending');
|
||||
}
|
||||
|
||||
highlightHistoryTurn(turnId) {
|
||||
if (!this.commandHistoryElement || turnId == null) return;
|
||||
const id = String(turnId);
|
||||
this.commandHistoryElement.querySelectorAll('.history-item').forEach((item) => {
|
||||
item.classList.toggle('active', item.dataset.turnId === id);
|
||||
});
|
||||
}
|
||||
|
||||
formatCommandHistory(command) {
|
||||
const parser = this.getModule('markup-parser') || window.MarkupParser;
|
||||
if (parser && typeof parser.markdownToHtml === 'function') {
|
||||
@@ -450,6 +518,10 @@ class UIInputHandlerModule extends BaseModule {
|
||||
return Boolean(playbackCoordinator && playbackCoordinator.isPlaying);
|
||||
}
|
||||
|
||||
isSkippablePauseActive() {
|
||||
return document.documentElement.dataset.skippablePause === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the cursor position to the start.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user