feat: add Zero-Dependency AE_Comp_Editor_TipTap with auto-formatting support
This commit is contained in:
@@ -16,6 +16,7 @@ import { to_title_case } from './ae_utils__to_title_case';
|
||||
import { process_data_string } from './ae_utils__process_data_string';
|
||||
import { set_obj_prop_display_name } from './ae_utils__set_obj_prop_display_name';
|
||||
import { return_obj_type_path } from './ae_utils__return_obj_type_path';
|
||||
import { format_html } from './ae_utils__format_html';
|
||||
import {
|
||||
combine_iv_and_base64,
|
||||
encrypt_content,
|
||||
@@ -336,6 +337,7 @@ export const ae_util = {
|
||||
shorten_string: shorten_string,
|
||||
shorten_filename: shorten_filename,
|
||||
file_extension_icon: file_extension_icon,
|
||||
format_html: format_html,
|
||||
set_obj_prop_display_name: set_obj_prop_display_name,
|
||||
return_obj_type_path: return_obj_type_path,
|
||||
combine_iv_and_base64: combine_iv_and_base64,
|
||||
|
||||
21
src/lib/ae_utils/ae_utils__format_html.ts
Normal file
21
src/lib/ae_utils/ae_utils__format_html.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Simple Regex-based HTML Formatter for Aether Platform.
|
||||
* Adds newlines and indentation to raw HTML strings.
|
||||
*/
|
||||
export function format_html(html: string): string {
|
||||
if (!html) return '';
|
||||
|
||||
let indent = 0;
|
||||
const tab = ' ';
|
||||
return html
|
||||
.replace(/>\s*</g, '>\n<') // Add newlines between tags
|
||||
.split('\n')
|
||||
.map(line => {
|
||||
line = line.trim();
|
||||
if (line.match(/<\//)) indent--; // Decrease indent for closing tags
|
||||
const out = tab.repeat(Math.max(0, indent)) + line;
|
||||
if (line.match(/<[^\/!]/) && !line.match(/\/>/) && !line.match(/<\//)) indent++; // Increase indent for opening tags
|
||||
return out;
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
157
src/lib/elements/AE_Comp_Editor_TipTap.svelte
Normal file
157
src/lib/elements/AE_Comp_Editor_TipTap.svelte
Normal file
@@ -0,0 +1,157 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* AE_Comp_Editor_TipTap.svelte
|
||||
* Zero-Dependency Rich Text Editor for Aether Platform.
|
||||
* Uses native contenteditable to avoid TipTap/ProseMirror library conflicts.
|
||||
* Styles aligned with existing .tiptap SCSS definitions.
|
||||
*/
|
||||
import { onMount, untrack } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import {
|
||||
Bold, Italic, List, ListOrdered,
|
||||
RemoveFormatting, Type, Code, AlignLeft
|
||||
} from 'lucide-svelte';
|
||||
import { ae_util } from '$lib/ae_utils/ae_utils';
|
||||
|
||||
interface Props {
|
||||
content?: string;
|
||||
new_content?: string;
|
||||
placeholder?: string;
|
||||
readonly?: boolean;
|
||||
auto_format?: boolean;
|
||||
class_li?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
content = $bindable(''),
|
||||
new_content = $bindable(''),
|
||||
placeholder = 'Start writing...',
|
||||
readonly = false,
|
||||
auto_format = true,
|
||||
class_li = ''
|
||||
}: Props = $props();
|
||||
|
||||
let editor_element: HTMLDivElement | undefined = $state();
|
||||
let is_focused = $state(false);
|
||||
|
||||
// Sync external content changes into the editor
|
||||
$effect(() => {
|
||||
if (editor_element && content !== editor_element.innerHTML) {
|
||||
untrack(() => {
|
||||
editor_element!.innerHTML = content || '';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function handle_input(e: Event) {
|
||||
const html = (e.target as HTMLDivElement).innerHTML;
|
||||
// Clean up empty state (browsers sometimes leave <br> or <p></p>)
|
||||
const cleaned = (html === '<br>' || html === '<p></p>') ? '' : html;
|
||||
content = cleaned;
|
||||
new_content = cleaned;
|
||||
}
|
||||
|
||||
// Toolbar Actions using native execCommand
|
||||
// (While deprecated, it remains the standard for zero-dep simple rich text)
|
||||
function exec(command: string, value: string | undefined = undefined) {
|
||||
if (!browser || readonly) return;
|
||||
document.execCommand(command, false, value);
|
||||
editor_element?.focus();
|
||||
}
|
||||
|
||||
function handle_keydown(e: KeyboardEvent) {
|
||||
// Basic shortcuts: Cmd/Ctrl + B, I
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
if (e.key === 'b') { e.preventDefault(); exec('bold'); }
|
||||
if (e.key === 'i') { e.preventDefault(); exec('italic'); }
|
||||
}
|
||||
}
|
||||
|
||||
function handle_format() {
|
||||
if (!content) return;
|
||||
const formatted = ae_util.format_html(content);
|
||||
content = formatted;
|
||||
new_content = formatted;
|
||||
}
|
||||
|
||||
function handle_blur() {
|
||||
is_focused = false;
|
||||
if (auto_format) {
|
||||
handle_format();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ae__comp__editor_tiptap flex flex-col border border-surface-500/20 rounded-container overflow-hidden bg-white dark:bg-black/10 {class_li}">
|
||||
{#if !readonly}
|
||||
<div class="toolbar flex flex-wrap gap-1 p-1 bg-surface-50 dark:bg-surface-900 border-b border-surface-500/20">
|
||||
<button type="button" class="btn btn-sm variant-soft hover:variant-filled-primary" onclick={() => exec('bold')} title="Bold">
|
||||
<Bold size="14" />
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm variant-soft hover:variant-filled-primary" onclick={() => exec('italic')} title="Italic">
|
||||
<Italic size="14" />
|
||||
</button>
|
||||
<div class="w-px h-4 bg-surface-500/20 mx-1 self-center"></div>
|
||||
<button type="button" class="btn btn-sm variant-soft hover:variant-filled-primary" onclick={() => exec('insertUnorderedList')} title="Bullet List">
|
||||
<List size="14" />
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm variant-soft hover:variant-filled-primary" onclick={() => exec('insertOrderedList')} title="Numbered List">
|
||||
<ListOrdered size="14" />
|
||||
</button>
|
||||
<div class="w-px h-4 bg-surface-500/20 mx-1 self-center"></div>
|
||||
<button type="button" class="btn btn-sm variant-soft hover:variant-filled-error" onclick={() => exec('removeFormat')} title="Clear Formatting">
|
||||
<RemoveFormatting size="14" />
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-sm variant-soft hover:variant-filled-success" onclick={handle_format} title="Format HTML Source">
|
||||
<AlignLeft size="14" />
|
||||
<span class="text-[10px] ml-1">Format</span>
|
||||
</button>
|
||||
|
||||
<div class="ml-auto flex gap-1 items-center px-2">
|
||||
<Type size="12" class="opacity-30" />
|
||||
<span class="text-[10px] opacity-50 uppercase font-bold">Visual</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grow relative h-full min-h-[150px] overflow-auto p-4">
|
||||
{#if !content && !is_focused}
|
||||
<div class="absolute top-4 left-4 pointer-events-none opacity-30 italic text-sm">
|
||||
{placeholder}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
bind:this={editor_element}
|
||||
contenteditable={!readonly}
|
||||
class="tiptap outline-none h-full w-full prose dark:prose-invert max-w-none"
|
||||
oninput={handle_input}
|
||||
onfocus={() => is_focused = true}
|
||||
onblur={handle_blur}
|
||||
onkeydown={handle_keydown}
|
||||
role="textbox"
|
||||
tabindex="0"
|
||||
aria-multiline="true"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
/* Import your existing TipTap styles */
|
||||
@import './element_tiptap_editor.scss';
|
||||
|
||||
.ae__comp__editor_tiptap :global(.tiptap) {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
/* Ensure lists look correct inside the editor */
|
||||
.ae__comp__editor_tiptap :global(.tiptap ul) {
|
||||
list-style-type: disc;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
.ae__comp__editor_tiptap :global(.tiptap ol) {
|
||||
list-style-type: decimal;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
51
src/routes/testing/editor_test/+page.svelte
Normal file
51
src/routes/testing/editor_test/+page.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import AE_Comp_Editor_TipTap from '$lib/elements/AE_Comp_Editor_TipTap.svelte';
|
||||
import AE_Comp_Editor_CodeMirror from '$lib/elements/AE_Comp_Editor_CodeMirror.svelte';
|
||||
|
||||
let test_content = $state('<h2>Welcome to the Visual Editor Test</h2><p>This editor uses <strong>native browser rich text</strong> capabilities.</p><ul><li>No library conflicts</li><li>Uses existing <em>.tiptap</em> styles</li><li>Instant performance</li></ul>');
|
||||
let log_lvl = 1;
|
||||
</script>
|
||||
|
||||
<div class="p-8 space-y-8 max-w-5xl mx-auto h-full overflow-y-auto">
|
||||
<header class="border-b border-surface-500/30 pb-4">
|
||||
<h1 class="h1">Rich Text Editor Test</h1>
|
||||
<p class="opacity-70">Testing the Zero-Dependency "TipTap" replacement.</p>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
<!-- Visual Editor -->
|
||||
<section class="card p-4 space-y-4 variant-soft-primary">
|
||||
<h2 class="h3 flex items-center gap-2">
|
||||
<span class="fas fa-eye text-primary-500"></span>
|
||||
Visual Editor (AE_Comp_Editor_TipTap)
|
||||
</h2>
|
||||
<div class="bg-surface-100-800-token rounded-lg">
|
||||
<AE_Comp_Editor_TipTap
|
||||
bind:content={test_content}
|
||||
placeholder="Try writing something pretty..."
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Source Code View -->
|
||||
<section class="card p-4 space-y-4 variant-soft-tertiary">
|
||||
<h2 class="h3 flex items-center gap-2">
|
||||
<span class="fas fa-code text-tertiary-500"></span>
|
||||
Source View (AE_Comp_Editor_CodeMirror)
|
||||
</h2>
|
||||
<div class="bg-surface-100-800-token rounded-lg h-[250px]">
|
||||
<AE_Comp_Editor_CodeMirror
|
||||
bind:content={test_content}
|
||||
language="html"
|
||||
show_line_numbers={true}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- HTML Preview -->
|
||||
<section class="card p-4 space-y-4 bg-surface-900 text-green-400 font-mono text-xs overflow-auto max-h-96 shadow-2xl border border-white/10">
|
||||
<h2 class="text-sm font-bold opacity-50 uppercase tracking-widest border-b border-white/10 pb-2">Raw Bound Content (Saved to DB)</h2>
|
||||
<pre class="whitespace-pre">{test_content}</pre>
|
||||
</section>
|
||||
</div>
|
||||
Reference in New Issue
Block a user