More work on the new rich text editor...

This commit is contained in:
Scott Idem
2024-12-03 12:39:30 -05:00
parent 971ffbad02
commit 6d94583885
4 changed files with 309 additions and 361 deletions

View File

@@ -1,28 +1,31 @@
<script lang="ts"> <script lang="ts">
import { type Editor } from '@tiptap/core'; import { fade } from 'svelte/transition'
import Undo from './icons/undo.svelte'; import { cubicOut } from 'svelte/easing';
import Redo from './icons/redo.svelte';
// import { Separator } from '$lib/components/ui/separator/index.js'; import { type Editor } from '@tiptap/core';
import Bold from './icons/bold.svelte'; import Undo from './icons/undo.svelte';
import Italic from './icons/italic.svelte'; import Redo from './icons/redo.svelte';
import Underline from './icons/underline.svelte'; // import { Separator } from '$lib/components/ui/separator/index.js';
import Strikethrough from './icons/strikethrough.svelte'; import Bold from './icons/bold.svelte';
import Link from './icons/link.svelte'; import Italic from './icons/italic.svelte';
import Code from './icons/code.svelte'; import Underline from './icons/underline.svelte';
import BlockQuote from './icons/block-quote.svelte'; import Strikethrough from './icons/strikethrough.svelte';
import Subscript from './icons/subscript.svelte'; import Link from './icons/link.svelte';
import BulletList from './icons/buttle-list.svelte'; // Typo in the icon name import Code from './icons/code.svelte';
import OrderedList from './icons/ordered-list.svelte'; import BlockQuote from './icons/block-quote.svelte';
import TaskList from './icons/task-list.svelte'; import Subscript from './icons/subscript.svelte';
import Highlighter from './icons/highlighter.svelte'; import BulletList from './icons/buttle-list.svelte'; // Typo in the icon name
import Superscript from './icons/superscript.svelte'; import OrderedList from './icons/ordered-list.svelte';
import Textcolor from './icons/textcolor.svelte'; import TaskList from './icons/task-list.svelte';
import Align from './icons/textalign.svelte'; import Highlighter from './icons/highlighter.svelte';
import Quickcolor from './icons/quickcolor.svelte'; import Superscript from './icons/superscript.svelte';
import Table from './icons/table.svelte'; import Textcolor from './icons/textcolor.svelte';
// import Image from './icons/image.svelte'; import Align from './icons/textalign.svelte';
import Text from './icons/text.svelte'; import Quickcolor from './icons/quickcolor.svelte';
import SearchReplace from './icons/search-replace.svelte'; import Table from './icons/table.svelte';
// import Image from './icons/image.svelte';
import Text from './icons/text.svelte';
import SearchReplace from './icons/search-replace.svelte';
interface Props { interface Props {
editor: Editor; editor: Editor;
@@ -35,14 +38,14 @@ let show_button_kv_defaults: any = {
undo: true, undo: true,
redo: true, redo: true,
text: true, text: false,
bold: true, bold: true,
italic: true, italic: true,
underline: true, underline: true,
strikethrough: false, strikethrough: false,
align: false, align: false,
link: true, link: false,
code: false, code: false,
blockquote: false, blockquote: false,
subscript: false, subscript: false,
@@ -52,9 +55,9 @@ let show_button_kv_defaults: any = {
task_list: false, task_list: false,
image: false, image: false,
table: false, table: false,
text_color: true, text_color: false,
highlighter: true, highlighter: false,
quick_color: true, quick_color: false,
search_replace: false, search_replace: false,
}; };
console.log('show_button_kv', show_button_kv); console.log('show_button_kv', show_button_kv);
@@ -65,72 +68,105 @@ if (show_button_kv) {
} }
</script> </script>
<div class="flex w-full items-center overflow-auto border-b p-1 *:mx-1"> <div
{#if show_button_kv.undo} transition:fade={{delay: 250, duration: 750, easing: cubicOut}}
<Undo {editor} /> class="
{/if} flex flex-row flex-wrap gap-0.5
{#if show_button_kv.redo} w-full items-center justify-between
<Redo {editor} /> overflow-auto border-b p-1
{/if} transition-all duration-1000
"
>
<span
class:hidden={!show_button_kv.undo && !show_button_kv.redo}
>
{#if show_button_kv.undo}
<Undo {editor} />
{/if}
{#if show_button_kv.redo}
<Redo {editor} />
{/if}
</span>
<!-- <Separator orientation="vertical" class="h-fit" /> --> <!-- <Separator orientation="vertical" class="h-fit" /> -->
{#if show_button_kv.text} <span
<Text {editor} /> class:hidden={!show_button_kv.text}
{/if} >
{#if show_button_kv.bold} {#if show_button_kv.text}
<Bold {editor} /> <Text {editor} />
{/if} {/if}
{#if show_button_kv.italic} </span>
<Italic {editor} /> <span
{/if} class:hidden={!show_button_kv.bold && !show_button_kv.italic && !show_button_kv.underline && !show_button_kv.strikethrough}
{#if show_button_kv.underline} >
<Underline {editor} /> {#if show_button_kv.bold}
{/if} <Bold {editor} />
{#if show_button_kv.strikethrough} {/if}
<Strikethrough {editor} /> {#if show_button_kv.italic}
{/if} <Italic {editor} />
{#if show_button_kv.align} {/if}
<Align {editor} /> {#if show_button_kv.underline}
{/if} <Underline {editor} />
{#if show_button_kv.link} {/if}
<Link {editor} /> {#if show_button_kv.strikethrough}
{/if} <Strikethrough {editor} />
{#if show_button_kv.code} {/if}
<Code {editor} /> </span>
{/if} <span
{#if show_button_kv.blockquote} class:hidden={!show_button_kv.align && !show_button_kv.link && !show_button_kv.code && !show_button_kv.blockquote && !show_button_kv.subscript && !show_button_kv.superscript && !show_button_kv.bullet_list && !show_button_kv.ordered_list && !show_button_kv.task_list && !show_button_kv.image && !show_button_kv.table}
<BlockQuote {editor} /> >
{/if} {#if show_button_kv.align}
{#if show_button_kv.subscript} <Align {editor} />
<Subscript {editor} /> {/if}
{/if} {#if show_button_kv.link}
{#if show_button_kv.superscript} <Link {editor} />
<Superscript {editor} /> {/if}
{/if} {#if show_button_kv.code}
{#if show_button_kv.bullet_list} <Code {editor} />
<BulletList {editor} /> {/if}
{/if} {#if show_button_kv.blockquote}
{#if show_button_kv.ordered_list} <BlockQuote {editor} />
<OrderedList {editor} /> {/if}
{/if} {#if show_button_kv.subscript}
{#if show_button_kv.task_list} <Subscript {editor} />
<TaskList {editor} /> {/if}
{/if} {#if show_button_kv.superscript}
<!-- {#if show_button_kv.image} <Superscript {editor} />
<Image {editor} /> {/if}
{/if} --> {#if show_button_kv.bullet_list}
{#if show_button_kv.table} <BulletList {editor} />
<Table {editor} /> {/if}
{/if} {#if show_button_kv.ordered_list}
{#if show_button_kv.text_color} <OrderedList {editor} />
<Textcolor {editor} /> {/if}
{/if} {#if show_button_kv.task_list}
{#if show_button_kv.highlighter} <TaskList {editor} />
<Highlighter {editor} /> {/if}
{/if} <!-- {#if show_button_kv.image}
{#if show_button_kv.quick_color} <Image {editor} />
<Quickcolor {editor} /> {/if} -->
{/if} {#if show_button_kv.table}
{#if show_button_kv.search_replace} <Table {editor} />
<SearchReplace {editor} /> {/if}
{/if} </span>
<span
class:hidden={!show_button_kv.text_color && !show_button_kv.highlighter && !show_button_kv.quick_color}
>
cxx
{#if show_button_kv.text_color}
<Textcolor {editor} />
{/if}
{#if show_button_kv.highlighter}
<Highlighter {editor} />
{/if}
{#if show_button_kv.quick_color}
<Quickcolor {editor} />
{/if}
</span>
<span
class:hidden={!show_button_kv.search_replace}
>
{#if show_button_kv.search_replace}
<SearchReplace {editor} />
{/if}
</span>
</div> </div>

View File

@@ -1,172 +1,176 @@
<script lang="ts"> <script lang="ts">
import './editor.css'; import './editor.css';
import { Editor, type Content } from '@tiptap/core'; import { Editor, type Content } from '@tiptap/core';
import StarterKit from '@tiptap/starter-kit'; import StarterKit from '@tiptap/starter-kit';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import EditorToolbar from './editor-toolbar.svelte'; import EditorToolbar from './editor-toolbar.svelte';
import { cn } from '$lib/utils.js'; import { cn } from '$lib/utils.js';
import { Subscript } from '@tiptap/extension-subscript'; import { Subscript } from '@tiptap/extension-subscript';
import { Superscript } from '@tiptap/extension-superscript'; import { Superscript } from '@tiptap/extension-superscript';
import { Underline } from '@tiptap/extension-underline'; import { Underline } from '@tiptap/extension-underline';
import { Link } from '@tiptap/extension-link'; import { Link } from '@tiptap/extension-link';
import TaskList from '@tiptap/extension-task-list'; import TaskList from '@tiptap/extension-task-list';
import TaskItem from '@tiptap/extension-task-item'; import TaskItem from '@tiptap/extension-task-item';
import TextStyle from '@tiptap/extension-text-style'; import TextStyle from '@tiptap/extension-text-style';
import Color from '@tiptap/extension-color'; import Color from '@tiptap/extension-color';
import Highlight from '@tiptap/extension-highlight'; import Highlight from '@tiptap/extension-highlight';
import Text from '@tiptap/extension-text'; import Text from '@tiptap/extension-text';
import Typography from '@tiptap/extension-typography'; import Typography from '@tiptap/extension-typography';
import TextAlign from '@tiptap/extension-text-align'; import TextAlign from '@tiptap/extension-text-align';
import { SmilieReplacer } from './custom/Extentions/SmilieReplacer.js'; import { SmilieReplacer } from './custom/Extentions/SmilieReplacer.js';
import { ColorHighlighter } from './custom/Extentions/ColorHighlighter.js'; import { ColorHighlighter } from './custom/Extentions/ColorHighlighter.js';
import Table from '@tiptap/extension-table'; import Table from '@tiptap/extension-table';
import TableRow from '@tiptap/extension-table-row'; import TableRow from '@tiptap/extension-table-row';
import TableHeader from '@tiptap/extension-table-header'; import TableHeader from '@tiptap/extension-table-header';
import TableCell from '@tiptap/extension-table-cell'; import TableCell from '@tiptap/extension-table-cell';
import { ImageExtension } from './custom/Extentions/ImageExtention.js'; import { ImageExtension } from './custom/Extentions/ImageExtention.js';
import { SvelteNodeViewRenderer } from 'svelte-tiptap'; import { SvelteNodeViewRenderer } from 'svelte-tiptap';
import CodeExtended from './custom/code-extended.svelte'; import CodeExtended from './custom/code-extended.svelte';
// Lowlight // Lowlight
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight'; import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
import { all, createLowlight } from 'lowlight'; import { all, createLowlight } from 'lowlight';
import './onedark.css'; import './onedark.css';
import SearchAndReplace from './custom/Extentions/SearchAndReplace.js'; import SearchAndReplace from './custom/Extentions/SearchAndReplace.js';
const lowlight = createLowlight(all); const lowlight = createLowlight(all);
interface Props { interface Props {
class?: string; class?: string;
content?: Content; content?: Content;
showToolbar?: boolean; show_toolbar?: boolean;
// html_text?: string; // html_text?: string;
new_html?: string; new_html?: string;
placeholder?: string; placeholder?: string;
} show_button_kv?: any;
}
let { class: let { class:
className = '', className = '',
content = $bindable(''), content = $bindable(''),
showToolbar = true, show_toolbar = true,
// html_text = '', // html_text = '',
new_html = $bindable(''), new_html = $bindable(''),
placeholder = $bindable('Start typing...') placeholder = $bindable('Start typing...'),
}: Props = $props(); show_button_kv = $bindable({})
}: Props = $props();
let editor = $state<Editor>(); let editor = $state<Editor>();
let element = $state<HTMLElement>(); let element = $state<HTMLElement>();
onMount(() => { onMount(() => {
editor = new Editor({ editor = new Editor({
element, element,
content, content,
editorProps: { editorProps: {
attributes: { attributes: {
class: class:
'm-auto p-2 focus:outline-none flex-1 prose text-foreground min-w-full max-h-full overflow-auto dark:prose-invert *:my-2' 'm-auto p-2 focus:outline-none flex-1 prose text-foreground min-w-full max-h-full overflow-auto dark:prose-invert *:my-2'
}
},
extensions: [
StarterKit.configure({
orderedList: {
HTMLAttributes: {
class: 'list-decimal'
}
},
bulletList: {
HTMLAttributes: {
class: 'list-disc'
}
},
heading: {
levels: [1, 2, 3, 4],
HTMLAttributes: {
class: 'tiptap-heading'
}
}
}),
Typography,
Text,
TextStyle,
TextAlign.configure({
types: ['heading', 'paragraph']
}),
Color,
Highlight.configure({ multicolor: true }),
Underline,
Superscript,
Subscript,
Link.configure({
openOnClick: false,
autolink: true,
defaultProtocol: 'https',
HTMLAttributes: {
target: '_blank',
rel: 'noopener noreferrer'
}
}),
TaskList,
TaskItem.configure({
nested: true
}),
SearchAndReplace,
CodeBlockLowlight.configure({
lowlight
}).extend({
addNodeView() {
return SvelteNodeViewRenderer(CodeExtended);
}
}),
SmilieReplacer,
ColorHighlighter,
Table.configure({
allowTableNodeSelection: true,
resizable: true
}),
TableRow,
TableHeader,
TableCell,
ImageExtension
],
autofocus: true,
onTransaction: (transaction) => {
/**
* Weird behavior of editor.
* If we do not make it undefined, then it looses it's reactivity
* this is because assigning editor directly to `transaction.editor`
* the original object is not mutated.
*/
editor = undefined;
editor = transaction.editor;
// console.log(editor.isActive('bold'));
content = editor.getHTML();
// console.log(content);
let html = editor.getHTML();
if (html == '<p></p>') {
new_html = '';
} else {
new_html = html ?? '';
}
} }
}); },
extensions: [
StarterKit.configure({
orderedList: {
HTMLAttributes: {
class: 'list-decimal'
}
},
bulletList: {
HTMLAttributes: {
class: 'list-disc'
}
},
heading: {
levels: [1, 2, 3, 4],
HTMLAttributes: {
class: 'tiptap-heading'
}
}
}),
Typography,
Text,
TextStyle,
TextAlign.configure({
types: ['heading', 'paragraph']
}),
Color,
Highlight.configure({ multicolor: true }),
Underline,
Superscript,
Subscript,
Link.configure({
openOnClick: false,
autolink: true,
defaultProtocol: 'https',
HTMLAttributes: {
target: '_blank',
rel: 'noopener noreferrer'
}
}),
TaskList,
TaskItem.configure({
nested: true
}),
SearchAndReplace,
CodeBlockLowlight.configure({
lowlight
}).extend({
addNodeView() {
return SvelteNodeViewRenderer(CodeExtended);
}
}),
SmilieReplacer,
ColorHighlighter,
Table.configure({
allowTableNodeSelection: true,
resizable: true
}),
TableRow,
TableHeader,
TableCell,
ImageExtension
],
autofocus: true,
onTransaction: (transaction) => {
/**
* Weird behavior of editor.
* If we do not make it undefined, then it looses it's reactivity
* this is because assigning editor directly to `transaction.editor`
* the original object is not mutated.
*/
editor = undefined;
editor = transaction.editor;
// console.log(editor.isActive('bold'));
content = editor.getHTML();
// console.log(content);
let html = editor.getHTML();
if (html == '<p></p>') {
new_html = '';
} else {
new_html = html ?? '';
}
}
}); });
});
onDestroy(() => { onDestroy(() => {
if (editor) editor.destroy(); if (editor) editor.destroy();
}); });
</script> </script>
<div class={cn('flex flex-col rounded border', className)}> <div
{#if editor && showToolbar} class={cn('flex flex-col rounded border textarea editor', className)}
<EditorToolbar {editor} /> >
{#if editor && show_toolbar}
<EditorToolbar {editor} {show_button_kv} />
{/if} {/if}
<div <div
bind:this={element} bind:this={element}
spellcheck="false" spellcheck="false"
class="tiptap h-full w-full overflow-auto relative"> class="tiptap overflow-auto relative">
<span <span
class="placeholder text-sm text-gray-400 italic absolute p-3" class="placeholder text-sm text-gray-400 italic absolute p-3"
contenteditable="false" contenteditable="false"

View File

@@ -37,11 +37,11 @@ import './element_tiptap_editor.scss';
export let html_text: string = ''; export let html_text: string = '';
export let default_minimal: boolean = false; export let default_minimal: boolean = false;
export let show_menu: boolean = true; export let show_toolbar: boolean = true;
export let placeholder: string = 'Type your text here...'; export let placeholder: string = 'Type your text here...';
if (default_minimal) { if (default_minimal) {
show_menu = false; show_toolbar = false;
} }
// export let html_text: string = ` // export let html_text: string = `
@@ -79,43 +79,9 @@ let element: HTMLDivElement;
let editor: any; let editor: any;
// More default options should be defined later. // More default options should be defined later.
// minimal, basic, full // minimal, basic, full
let show_button_kv_defaults: any = {
bold: true,
italic: true,
strike: true,
code: true,
paragraph: false,
heading__h1: false,
heading__h2: false,
heading__h3: false,
heading__h4: false,
heading__h5: false,
heading__h6: false,
bulletList: false,
orderedList: false,
codeBlock: false,
blockquote: false,
horizontalRule: false,
hardBreak: true,
link: false,
unsetLink: true,
unsetAllMarks: true,
undo: true,
redo: false,
};
export let show_button_kv: any; export let show_button_kv: any;
if (show_button_kv) {
show_button_kv = { ...show_button_kv_defaults, ...show_button_kv };
} else {
show_button_kv = show_button_kv_defaults;
}
// export let new_json = editor?.getJSON(); // export let new_json = editor?.getJSON();
export let new_html: string = ''; export let new_html: string = '';
@@ -133,13 +99,10 @@ let mouse_leave_wait: number = 2000;
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- class:py-1={show_menu} -->
{#if 1==3}
<div <div
on:click={() => { on:click={() => {
if (default_minimal) { if (default_minimal) {
editor.chain().focus().setParagraph().run(); show_toolbar = true;
show_menu = true;
} }
}} }}
on:mouseleave={() => { on:mouseleave={() => {
@@ -147,77 +110,30 @@ let mouse_leave_wait: number = 2000;
mouse_entered_timer = setTimeout(() => { mouse_entered_timer = setTimeout(() => {
if (default_minimal) { if (default_minimal) {
show_menu = false; show_toolbar = false;
} }
}, mouse_leave_wait); }, mouse_leave_wait);
// if (default_minimal) {
// show_menu = false;
// }
}} }}
on:mouseenter={() => { on:mouseenter={() => {
clearTimeout(mouse_entered_timer); clearTimeout(mouse_entered_timer);
mouse_entered_timer = setTimeout(() => { mouse_entered_timer = setTimeout(() => {
if (default_minimal) { if (default_minimal) {
show_menu = true; show_toolbar = true;
} }
}, mouse_enter_wait); }, mouse_enter_wait);
// if (default_minimal) {
// show_menu = true;
// }
}} }}
class="xxxeditor textarea p-1 transition-all duration-1000" class=""
class:hidden={true}
> >
<ShadEditor
<!-- && show_menu --> class="p-1 transition-all duration-1000"
<!-- class:h-0={!show_menu} --> bind:content={html_text}
<!-- class:h-auto={show_menu} --> bind:new_html={new_html}
<!-- class:opacity-0={!show_menu} --> placeholder={placeholder}
<!-- class:opacity-100={show_menu} --> show_toolbar={show_toolbar}
{#if editor && show_menu} show_button_kv={show_button_kv}
<div />
transition:fade={{delay: 250, duration: 750, easing: cubicOut}}
class="
control-group button-group
bg-gray-200
border-b border-gray-400
p-1
transition-all duration-1000
flex flex-row flex-wrap gap-4
items-center justify-between
"
>
<!-- Nothing Here Anymore -->
</div>
{/if} <!-- show_menu -->
<!-- bg-slate-100 px-2 py-2 -->
<div
bind:this={element}
class="tiptap px-2 py-2 relative"
class:font-mono={show_menu}
>
<span
class="placeholder text-sm text-gray-400 italic absolute"
contenteditable="false"
hidden={editor?.getHTML() !== '<p></p>'}
>{placeholder}</span>
</div>
</div> </div>
{/if}
<main class="my-10 flex w-full flex-col items-center justify-center">
<ShadEditor
class="editor textarea p-1 transition-all duration-1000"
bind:content={html_text}
bind:new_html={new_html}
placeholder={placeholder}
/>
</main>
<style lang="scss"> <style lang="scss">
// :global(.ProseMirror) { // :global(.ProseMirror) {

View File

@@ -360,15 +360,11 @@ function send_staff_notification_email() {
default_minimal={true} default_minimal={true}
bind:html_text={$idaa_slct.post_obj.content} bind:html_text={$idaa_slct.post_obj.content}
show_button_kv={{ show_button_kv={{
'heading__h1': true, text: true,
'heading__h2': false, bullet_list: true,
'heading__h3': false, ordered_list: true,
paragraph: false,
bulletList: true,
orderedList: true,
hardBreak: true,
link: true, link: true,
unsetLink: true unset_link: true
}} }}
bind:new_html={$idaa_slct.post_obj.content_new_html} bind:new_html={$idaa_slct.post_obj.content_new_html}
placeholder="Your post content here..." placeholder="Your post content here..."
@@ -378,15 +374,11 @@ function send_staff_notification_email() {
default_minimal={true} default_minimal={true}
bind:html_text={$idaa_slct.post_obj.content} bind:html_text={$idaa_slct.post_obj.content}
show_button_kv={{ show_button_kv={{
'heading__h1': false, // text: true,
'heading__h2': false, // bullet_list: true,
'heading__h3': false, // ordered_list: true,
paragraph: false, // link: true,
bulletList: false, // unset_link: true
orderedList: false,
hardBreak: false,
link: false,
unsetLink: false
}} }}
bind:new_html={$idaa_slct.post_obj.content_new_html} bind:new_html={$idaa_slct.post_obj.content_new_html}
placeholder="Your post content here..." placeholder="Your post content here..."