botserver/web/app/editor/editor.page.html
Rodrigo Rodriguez (Pragmatismo) 02eaac783f feat(editor, settings): refactor state handling and enhance validation
Refactored editor.page.html to use a Vue-style `data()` function for reactive state, adding a new `content` property and cleaning up redundant inline styles. Updated profile-form.html to replace single `error` handling with field-specific `errors.<field>` bindings, improving form validation clarity and user feedback.
2025-11-15 21:52:53 -03:00

342 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- Riot.js component for the editor page (converted from app/editor/page.tsx) -->
<template>
<div class="word-clone">
<!-- Quick Access Toolbar -->
<div class="quick-access">
<button class="quick-access-btn" @click={undo}>
<svg><!-- Undo icon (use same SVG as in React) --></svg>
</button>
<button class="quick-access-btn" @click={redo}>
<svg><!-- Redo icon --></svg>
</button>
<div class="title-controls">
<input
type="text"
class="title-input"
placeholder="Document name"
value={fileName}
@input={e => fileName = e.target.value}
/>
<button class="quick-access-btn" @click={saveDocument}>
<svg><!-- Save icon --></svg>
</button>
</div>
</div>
<!-- Ribbon -->
<div class="ribbon">
<div class="ribbon-tabs">
<button class="ribbon-tab-button {activeTab === 'home' ? 'active' : ''}" @click={() => activeTab = 'home'}>Home</button>
<button class="ribbon-tab-button {activeTab === 'insert' ? 'active' : ''}" @click={() => activeTab = 'insert'}>Insert</button>
<button class="ribbon-tab-button {activeTab === 'layout' ? 'active' : ''}" @click={() => activeTab = 'layout'}>Layout</button>
<button class="ribbon-tab-button {activeTab === 'view' ? 'active' : ''}" @click={() => activeTab = 'view'}>View</button>
</div>
{activeTab === 'home' && (
<div class="ribbon-content">
<ribbon-group title="Clipboard">
<ribbon-button icon="copy" label="Copy" size="large" @click={copy} />
<ribbon-button icon="paste" label="Paste" size="large" @click={paste} />
<ribbon-button icon="cut" label="Cut" size="medium" @click={cut} />
</ribbon-group>
<ribbon-group title="Font">
<div style="display:flex;flex-direction:column;gap:4px">
<div style="display:flex;gap:4px">
<select bind={fontFamily} @change={e => setFontFamily(e.target.value)} class="format-select" style="width:120px">
<option value="Calibri">Calibri</option>
<option value="Arial">Arial</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Georgia">Georgia</option>
<option value="Verdana">Verdana</option>
</select>
<select bind={fontSize} @change={e => setFontSize(e.target.value)} class="format-select" style="width:60px">
<option value="8">8</option>
<option value="9">9</option>
<option value="10">10</option>
<option value="11">11</option>
<option value="12">12</option>
<option value="14">14</option>
<option value="16">16</option>
<option value="18">18</option>
<option value="20">20</option>
<option value="24">24</option>
<option value="28">28</option>
<option value="36">36</option>
</select>
</div>
<div style="display:flex;gap:2px">
<ribbon-button icon="bold" label="Bold" @click={toggleBold} active={editor?.isActive('bold')} />
<ribbon-button icon="italic" label="Italic" @click={toggleItalic} active={editor?.isActive('italic')} />
<ribbon-button icon="underline" label="Underline" @click={toggleUnderline} active={editor?.isActive('underline')} />
<div class="color-picker-wrapper">
<ribbon-button icon="type" label="Text Color" />
<input type="color" bind={textColor} @input={e => setTextColor(e.target.value)} class="color-picker" />
<div class="color-indicator" style="background-color:{textColor}"></div>
</div>
<div class="color-picker-wrapper">
<ribbon-button icon="highlighter" label="Highlight" />
<input type="color" bind={highlightColor} @input={e => setHighlightColor(e.target.value)} class="color-picker" />
<div class="color-indicator" style="background-color:{highlightColor}"></div>
</div>
</div>
</div>
</ribbon-group>
<ribbon-group title="Paragraph">
<div style="display:flex;flex-direction:column;gap:4px">
<div style="display:flex;gap:2px">
<ribbon-button icon="align-left" label="Align Left" @click={alignLeft} active={editor?.isActive({textAlign:'left'})} />
<ribbon-button icon="align-center" label="Center" @click={alignCenter} active={editor?.isActive({textAlign:'center'})} />
<ribbon-button icon="align-right" label="Align Right" @click={alignRight} active={editor?.isActive({textAlign:'right'})} />
<ribbon-button icon="align-justify" label="Justify" @click={alignJustify} active={editor?.isActive({textAlign:'justify'})} />
</div>
</div>
</ribbon-group>
</div>
)}
{activeTab === 'insert' && (
<div class="ribbon-content">
<ribbon-group title="Illustrations">
<ribbon-button icon="image" label="Picture" size="large" @click={addImage} />
</ribbon-group>
<ribbon-group title="Tables">
<ribbon-button icon="table" label="Table" size="large" @click={insertTable} />
</ribbon-group>
<ribbon-group title="Links">
<ribbon-button icon="link" label="Link" size="large" @click={addLink} active={editor?.isActive('link')} />
</ribbon-group>
</div>
)}
{activeTab === 'view' && (
<div class="ribbon-content">
<ribbon-group title="Zoom">
<div style="display:flex;flex-direction:column;align-items:center;gap:8px">
<div style="font-size:14px;font-weight:600">{zoom}%</div>
<input type="range" min="50" max="200" bind={zoom} @input={e => zoom = parseInt(e.target.value)} class="zoom-slider" />
</div>
</ribbon-group>
</div>
)}
</div>
<!-- Editor Area -->
<div class="editor-container">
<div class="editor-main">
<div class="pages-container">
<div each={pageNum in pages} class="page">
<div class="page-number">Page {pageNum}</div>
<div class="page-content">
{pageNum === 1 && <editor-content bind={editor} />}
</div>
</div>
</div>
</div>
</div>
<!-- Bubble Menu -->
{editor && (
<bubble-menu bind={editor}>
<ribbon-button icon="bold" label="Bold" @click={toggleBold} active={editor.isActive('bold')} />
<ribbon-button icon="italic" label="Italic" @click={toggleItalic} active={editor.isActive('italic')} />
<ribbon-button icon="underline" label="Underline" @click={toggleUnderline} active={editor.isActive('underline')} />
<ribbon-button icon="link" label="Link" @click={addLink} active={editor.isActive('link')} />
</bubble-menu>
)}
<!-- Status Bar -->
<div class="status-bar">
<div>Page {pages.length} of {pages.length} | Words: {editor?.storage?.characterCount?.words() || 0}</div>
<div class="zoom-controls">
<button @click={() => zoom = Math.max(50, zoom - 10)}>-</button>
<span>{zoom}%</span>
<button @click={() => zoom = Math.min(200, zoom + 10)}>+</button>
</div>
</div>
<!-- Hidden file input -->
<input type="file" bind={fileInputRef} @change={handleImageUpload} accept="image/*" style="display:none" />
</div>
</template>
<script >
<!-- Removed unused Riot imports -->
import { useEditor, EditorContent, BubbleMenu, AnyExtension } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Bold from '@tiptap/extension-bold';
import Italic from '@tiptap/extension-italic';
import History from '@tiptap/extension-history';
import TextStyle from '@tiptap/extension-text-style';
import FontFamily from '@tiptap/extension-font-family';
import Color from '@tiptap/extension-color';
import Highlight from '@tiptap/extension-highlight';
import TextAlign from '@tiptap/extension-text-align';
import Link from '@tiptap/extension-link';
import Image from '@tiptap/extension-image';
import Underline from '@tiptap/extension-underline';
import Table from '@tiptap/extension-table';
import TableCell from '@tiptap/extension-table-cell';
import TableHeader from '@tiptap/extension-table-header';
import TableRow from '@tiptap/extension-table-row';
import './style.css';
export default {
// Reactive state
data() {
return {
fileName: 'Document 1',
fontSize: '12',
fontFamily: 'Calibri',
textColor: '#000000',
highlightColor: '#ffff00',
activeTab: 'home',
zoom: 100,
pages: [1],
editor: null,
fileInputRef: null,
content: '',
activeTab: 'home'
}
},
// Lifecycle
async mounted() {
this.editor = useEditor({
extensions: [
StarterKit.configure({ history: false }),
Bold,
Italic,
History,
TextStyle,
FontFamily,
Color,
Highlight.configure({ multicolor: true }),
TextAlign.configure({ types: ['heading', 'paragraph', 'tableCell'] }),
Link.configure({ openOnClick: false }),
Image,
Underline,
Table.configure({
resizable: true,
HTMLAttributes: { class: 'editor-table' },
}),
TableRow,
TableHeader,
TableCell,
],
content: `
<h1 style="text-align: center; font-size: 24px; margin-bottom: 20px;">${this.fileName}</h1>
<p><br></p>
<p>Start typing your document here...</p>
<p><br></p>
`,
onUpdate: ({ editor }) => {
const element = document.querySelector('.ProseMirror');
if (element) {
const contentHeight = element.scrollHeight;
const pageHeight = 1123; // A4 height in pixels at 96 DPI
const neededPages = Math.max(1, Math.ceil(contentHeight / pageHeight));
if (neededPages !== this.pages.length) {
this.pages = Array.from({ length: neededPages }, (_, i) => i + 1);
}
}
},
});
},
// Methods
undo() {
this.editor?.chain().focus().undo().run();
},
redo() {
this.editor?.chain().focus().redo().run();
},
saveDocument() {
if (!this.editor) return;
const content = this.editor.getHTML();
const blob = new Blob([content], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${this.fileName}.html`;
a.click();
URL.revokeObjectURL(url);
},
setFontFamily(value) {
this.fontFamily = value;
this.editor?.chain().focus().setFontFamily(value).run();
},
setFontSize(value) {
this.fontSize = value;
// TipTap does not have a builtin fontsize extension; you may need a custom one.
},
setTextColor(value) {
this.textColor = value;
this.editor?.chain().focus().setColor(value).run();
},
setHighlightColor(value) {
this.highlightColor = value;
this.editor?.chain().focus().setHighlight({ color: value }).run();
},
toggleBold() {
this.editor?.chain().focus().toggleBold().run();
},
toggleItalic() {
this.editor?.chain().focus().toggleItalic().run();
},
toggleUnderline() {
this.editor?.chain().focus().toggleUnderline().run();
},
alignLeft() {
this.editor?.chain().focus().setTextAlign('left').run();
},
alignCenter() {
this.editor?.chain().focus().setTextAlign('center').run();
},
alignRight() {
this.editor?.chain().focus().setTextAlign('right').run();
},
alignJustify() {
this.editor?.chain().focus().setTextAlign('justify').run();
},
addImage() {
if (this.fileInputRef) this.fileInputRef.click();
},
handleImageUpload(e) {
const file = e.target.files?.[0];
if (file && this.editor) {
const reader = new FileReader();
reader.onload = (event) => {
const imageUrl = event.target?.result;
this.editor.chain().focus().setImage({ src: imageUrl }).run();
};
reader.readAsDataURL(file);
}
},
insertTable() {
this.editor?.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
},
addLink() {
const previousUrl = this.editor?.getAttributes('link').href;
const url = window.prompt('URL', previousUrl);
if (url === null) return;
if (url === '') {
this.editor?.chain().focus().extendMarkRange('link').unsetLink().run();
return;
}
this.editor?.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
},
copy() {
document.execCommand('copy');
},
paste() {
document.execCommand('paste');
},
cut() {
document.execCommand('cut');
},
};
</script>