mirror of
https://github.com/remsky/Kokoro-FastAPI.git
synced 2025-08-05 16:48:53 +00:00
251 lines
No EOL
8.6 KiB
JavaScript
251 lines
No EOL
8.6 KiB
JavaScript
export default class TextEditor {
|
|
constructor(container, options = {}) {
|
|
this.options = {
|
|
charsPerPage: 500, // Default to 500 chars per page
|
|
onTextChange: null,
|
|
...options
|
|
};
|
|
|
|
this.container = container;
|
|
this.currentPage = 1;
|
|
this.pages = [''];
|
|
this.charCount = 0;
|
|
this.fullText = '';
|
|
this.isTyping = false;
|
|
|
|
this.setupDOM();
|
|
this.bindEvents();
|
|
}
|
|
|
|
setupDOM() {
|
|
this.container.innerHTML = `
|
|
<div class="text-editor">
|
|
<div class="editor-view">
|
|
<div class="page-navigation">
|
|
<div class="pagination">
|
|
<button class="prev-btn">← Previous</button>
|
|
<span class="page-info">Page 1 of 1</span>
|
|
<button class="next-btn">Next →</button>
|
|
</div>
|
|
</div>
|
|
<textarea
|
|
class="page-content"
|
|
placeholder="Enter text to convert to speech..."
|
|
></textarea>
|
|
<div class="editor-footer">
|
|
<div class="file-controls">
|
|
<input type="file" class="file-input" accept=".txt" style="display: none;">
|
|
<button class="upload-btn">Upload Text</button>
|
|
<button class="clear-btn">Clear Text</button>
|
|
</div>
|
|
<div class="chars-per-page">
|
|
<input
|
|
type="number"
|
|
class="chars-input"
|
|
value="500"
|
|
min="100"
|
|
max="2000"
|
|
title="Characters per page"
|
|
>
|
|
<span class="chars-label">chars/page</span>
|
|
<button class="format-btn">Format Pages</button>
|
|
</div>
|
|
<div class="char-count">0 characters</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Cache DOM elements
|
|
this.elements = {
|
|
pageContent: this.container.querySelector('.page-content'),
|
|
prevBtn: this.container.querySelector('.prev-btn'),
|
|
nextBtn: this.container.querySelector('.next-btn'),
|
|
pageInfo: this.container.querySelector('.page-info'),
|
|
fileInput: this.container.querySelector('.file-input'),
|
|
uploadBtn: this.container.querySelector('.upload-btn'),
|
|
clearBtn: this.container.querySelector('.clear-btn'),
|
|
charCount: this.container.querySelector('.char-count'),
|
|
charsPerPage: this.container.querySelector('.chars-input'),
|
|
formatBtn: this.container.querySelector('.format-btn')
|
|
};
|
|
|
|
// Set initial chars per page value
|
|
this.elements.charsPerPage.value = this.options.charsPerPage;
|
|
}
|
|
|
|
bindEvents() {
|
|
// Handle page content changes
|
|
this.elements.pageContent.addEventListener('input', (e) => {
|
|
const newContent = e.target.value;
|
|
this.pages[this.currentPage - 1] = newContent;
|
|
|
|
// Only handle empty pages, otherwise just update the text
|
|
if (!newContent.trim() && this.pages.length > 1) {
|
|
// Remove the empty page and adjust
|
|
this.pages.splice(this.currentPage - 1, 1);
|
|
this.currentPage = Math.min(this.currentPage, this.pages.length);
|
|
this.updatePageDisplay();
|
|
}
|
|
|
|
// Update full text and char count - join with space since pages are just for UI
|
|
this.fullText = this.pages.join(' ');
|
|
this.updateCharCount();
|
|
|
|
if (this.options.onTextChange) {
|
|
this.options.onTextChange(this.fullText);
|
|
}
|
|
});
|
|
|
|
// Navigation
|
|
this.elements.prevBtn.addEventListener('click', () => {
|
|
if (this.currentPage > 1) {
|
|
this.currentPage--;
|
|
this.updatePageDisplay();
|
|
}
|
|
});
|
|
|
|
this.elements.nextBtn.addEventListener('click', () => {
|
|
if (this.currentPage < this.pages.length) {
|
|
this.currentPage++;
|
|
this.updatePageDisplay();
|
|
}
|
|
});
|
|
|
|
// File upload
|
|
this.elements.uploadBtn.addEventListener('click', () => {
|
|
this.elements.fileInput.click();
|
|
});
|
|
|
|
this.elements.fileInput.addEventListener('change', (e) => {
|
|
const file = e.target.files[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
this.setText(e.target.result);
|
|
if (this.options.onTextChange) {
|
|
this.options.onTextChange(this.getText());
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
});
|
|
|
|
// Clear text
|
|
this.elements.clearBtn.addEventListener('click', () => {
|
|
this.clear();
|
|
if (this.options.onTextChange) {
|
|
this.options.onTextChange('');
|
|
}
|
|
});
|
|
|
|
// Cache format button
|
|
this.elements.formatBtn = this.container.querySelector('.format-btn');
|
|
|
|
// Characters per page control - just update the value
|
|
this.elements.charsPerPage.addEventListener('change', (e) => {
|
|
const value = parseInt(e.target.value);
|
|
if (value >= 100 && value <= 2000) {
|
|
this.options.charsPerPage = value;
|
|
}
|
|
});
|
|
|
|
// Format pages button - trigger the split
|
|
this.elements.formatBtn.addEventListener('click', () => {
|
|
const value = parseInt(this.elements.charsPerPage.value);
|
|
if (value >= 100 && value <= 2000) {
|
|
this.options.charsPerPage = value;
|
|
this.splitIntoPages(this.fullText);
|
|
}
|
|
});
|
|
}
|
|
|
|
splitIntoPages(text) {
|
|
if (!text || !text.trim()) {
|
|
this.pages = [''];
|
|
this.fullText = '';
|
|
this.currentPage = 1;
|
|
this.updatePageDisplay();
|
|
this.updateCharCount();
|
|
return;
|
|
}
|
|
|
|
// Store original text to preserve natural line breaks
|
|
this.fullText = text.trim();
|
|
const words = text.trim().split(/\s+/);
|
|
this.pages = [];
|
|
let currentPage = '';
|
|
|
|
for (let i = 0; i < words.length; i++) {
|
|
const word = words[i];
|
|
const potentialPage = currentPage + (currentPage ? ' ' : '') + word;
|
|
|
|
if (potentialPage.length >= this.options.charsPerPage && currentPage) {
|
|
this.pages.push(currentPage);
|
|
currentPage = word;
|
|
} else {
|
|
currentPage = potentialPage;
|
|
}
|
|
}
|
|
|
|
if (currentPage) {
|
|
this.pages.push(currentPage);
|
|
}
|
|
|
|
if (this.pages.length === 0) {
|
|
this.pages = [''];
|
|
this.currentPage = 1;
|
|
} else {
|
|
// Keep current page in bounds
|
|
this.currentPage = Math.min(this.currentPage, this.pages.length);
|
|
}
|
|
|
|
this.updatePageDisplay();
|
|
this.updateCharCount();
|
|
}
|
|
|
|
setText(text) {
|
|
// Just set the text without splitting into pages
|
|
this.fullText = text;
|
|
this.pages = [text];
|
|
this.currentPage = 1;
|
|
this.updatePageDisplay();
|
|
this.updateCharCount();
|
|
}
|
|
|
|
updatePageDisplay() {
|
|
this.elements.pageContent.value = this.pages[this.currentPage - 1] || '';
|
|
this.elements.pageInfo.textContent = `Page ${this.currentPage} of ${this.pages.length}`;
|
|
|
|
// Update button states
|
|
this.elements.prevBtn.disabled = this.currentPage === 1;
|
|
this.elements.nextBtn.disabled = this.currentPage === this.pages.length;
|
|
}
|
|
|
|
updateCharCount() {
|
|
const totalChars = this.fullText.length;
|
|
this.elements.charCount.textContent = `${totalChars} characters`;
|
|
}
|
|
|
|
prevPage() {
|
|
if (this.currentPage > 1) {
|
|
this.currentPage--;
|
|
this.updatePageDisplay();
|
|
}
|
|
}
|
|
|
|
nextPage() {
|
|
if (this.currentPage < this.pages.length) {
|
|
this.currentPage++;
|
|
this.updatePageDisplay();
|
|
}
|
|
}
|
|
|
|
getText() {
|
|
return this.fullText;
|
|
}
|
|
|
|
clear() {
|
|
this.setText('');
|
|
}
|
|
} |