feat(Html WYSYWIG): add colors and tables

Fix #796
This commit is contained in:
sharevb 2024-09-28 11:22:56 +02:00 committed by ShareVB
parent 318fb6efb9
commit e3e8e9d4e4
6 changed files with 2160 additions and 1491 deletions

2
components.d.ts vendored
View file

@ -131,7 +131,9 @@ declare module '@vue/runtime-core' {
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
NCode: typeof import('naive-ui')['NCode']
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
NColorPicker: typeof import('naive-ui')['NColorPicker']
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
NDivider: typeof import('naive-ui')['NDivider']
NEllipsis: typeof import('naive-ui')['NEllipsis']
NForm: typeof import('naive-ui')['NForm']
NFormItem: typeof import('naive-ui')['NFormItem']

View file

@ -38,9 +38,17 @@
"@it-tools/bip39": "^0.0.4",
"@it-tools/oggen": "^1.3.0",
"@sindresorhus/slugify": "^2.2.1",
"@tiptap/pm": "2.1.6",
"@tiptap/starter-kit": "2.1.6",
"@tiptap/vue-3": "2.0.3",
"@tiptap/extension-color": "^2.7.4",
"@tiptap/extension-gapcursor": "^2.7.4",
"@tiptap/extension-highlight": "^2.7.4",
"@tiptap/extension-table": "^2.7.4",
"@tiptap/extension-table-cell": "^2.7.4",
"@tiptap/extension-table-header": "^2.7.4",
"@tiptap/extension-table-row": "^2.7.4",
"@tiptap/extension-text-style": "^2.7.4",
"@tiptap/pm": "2.7.4",
"@tiptap/starter-kit": "2.7.4",
"@tiptap/vue-3": "2.7.4",
"@types/figlet": "^1.5.8",
"@vicons/material": "^0.12.0",
"@vicons/tabler": "^0.12.0",

3348
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -3,6 +3,13 @@ import { tryOnBeforeUnmount, useVModel } from '@vueuse/core';
import { Editor, EditorContent } from '@tiptap/vue-3';
import StarterKit from '@tiptap/starter-kit';
import { useThemeVars } from 'naive-ui';
import { Color } from '@tiptap/extension-color';
import TextStyle from '@tiptap/extension-text-style';
import Highlight from '@tiptap/extension-highlight';
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 MenuBar from './menu-bar.vue';
const props = defineProps<{ html: string }>();
@ -12,7 +19,18 @@ const html = useVModel(props, 'html', emit);
const editor = new Editor({
content: html.value,
extensions: [StarterKit],
extensions: [
StarterKit,
TextStyle,
Color,
Highlight.configure({ multicolor: true }),
Table.configure({
resizable: true,
}),
TableRow,
TableHeader,
TableCell,
],
});
editor.on('update', ({ editor }) => emit('update:html', editor.getHTML()));
@ -63,6 +81,63 @@ tryOnBeforeUnmount(() => {
line-height: 1.1;
}
/* Table-specific styling */
table {
border-collapse: collapse;
margin: 0;
overflow: hidden;
table-layout: fixed;
width: 100%;
td,
th {
border: 1px solid v-bind('themeVars.borderColor');
box-sizing: border-box;
min-width: 1em;
padding: 6px 8px;
position: relative;
vertical-align: top;
> * {
margin-bottom: 0;
}
}
th {
background-color: v-bind('themeVars.tableHeaderColor');
font-weight: bold;
text-align: left;
}
.selectedCell:after {
content: "";
left: 0; right: 0; top: 0; bottom: 0;
pointer-events: none;
position: absolute;
z-index: 2;
}
.column-resize-handle {
background-color: v-bind('themeVars.actionColor');
bottom: -2px;
pointer-events: none;
position: absolute;
right: -2px;
top: 0;
width: 4px;
}
}
.tableWrapper {
margin: 1.5rem 0;
overflow-x: auto;
}
&.resize-cursor {
cursor: ew-resize;
cursor: col-resize;
}
code {
background-color: v-bind('themeVars.codeColor');
padding: 2px 4px;
@ -84,10 +159,6 @@ tryOnBeforeUnmount(() => {
}
}
mark {
background-color: #faf594;
}
img {
max-width: 100%;
height: auto;

View file

@ -1,13 +1,13 @@
<script setup lang="ts">
import type { Component } from 'vue';
const props = defineProps<{ icon: Component; title: string; action: () => void; isActive?: () => boolean }>();
const { icon, title, action, isActive } = toRefs(props);
const props = defineProps<{ icon: Component; title: string; action: () => void; isActive?: () => boolean; enabled?: () => boolean }>();
const { icon, title, action, isActive, enabled } = toRefs(props);
</script>
<template>
<c-tooltip :tooltip="title">
<c-button circle variant="text" :type="isActive?.() ? 'primary' : 'default'" @click="action">
<c-button circle variant="text" :disabled="enabled && !enabled()" :type="isActive?.() ? 'primary' : 'default'" @click="action">
<n-icon :component="icon" />
</c-button>
</c-tooltip>

View file

@ -8,15 +8,25 @@ import {
ClearFormatting,
Code,
CodePlus,
ColorPicker,
ColumnInsertLeft,
ColumnInsertRight,
Cross,
H1,
H2,
H3,
H4,
Heading,
Italic,
LayersIntersect2,
LayersUnion,
LayoutDistributeHorizontal,
LayoutDistributeVertical,
List,
ListNumbers,
Strikethrough,
TextWrap,
RowInsertBottom,
RowInsertTop,
SeparatorVertical, Strikethrough, Table, TableOff, TextWrap, Tool,
} from '@vicons/tabler';
import type { Component } from 'vue';
import MenuBarItem from './menu-bar-item.vue';
@ -29,9 +39,18 @@ type MenuItem =
icon: Component
title: string
action: () => void
value?: () => string
isActive?: () => boolean
enabled?: () => boolean
type: 'button'
}
| {
icon: Component
title: string
action: (color: string) => void
value: () => string
type: 'color'
}
| { type: 'divider' };
const items: MenuItem[] = [
@ -141,7 +160,42 @@ const items: MenuItem[] = [
title: 'Clear format',
action: () => editor.value.chain().focus().clearNodes().unsetAllMarks().run(),
},
{
type: 'divider',
},
{
type: 'color',
title: 'Forecolor',
icon: ColorPicker,
action: color => editor.value.chain().focus().setColor(color).run(),
value: () => editor.value.getAttributes('textStyle').color,
},
{
type: 'button',
icon: ClearFormatting,
title: 'Clear Forecolor',
action: () => editor.value.chain().focus().unsetColor().run(),
},
{
type: 'divider',
},
{
type: 'color',
title: 'Highlight color',
icon: ColorPicker,
action: color => editor.value.chain().focus().setHighlight({ color }).run(),
value: () => '#FAF594',
},
{
type: 'button',
icon: ClearFormatting,
title: 'Clear Highlight',
action: () => editor.value.chain().focus().unsetHighlight().run(),
isActive: () => editor.value.isActive('highlight'),
},
{
type: 'divider',
},
{
type: 'button',
icon: ArrowBack,
@ -154,14 +208,152 @@ const items: MenuItem[] = [
title: 'Redo',
action: () => editor.value.chain().focus().redo().run(),
},
{
type: 'divider',
},
{
type: 'button',
action: () => editor.value.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(),
enabled: () => editor.value.can().insertTable(),
title: 'Insert table',
icon: Table,
},
{
type: 'divider',
},
{
type: 'button',
action: () => editor.value.chain().focus().addColumnBefore().run(),
enabled: () => editor.value.can().addColumnBefore(),
title: 'Add column before',
icon: ColumnInsertLeft,
},
{
type: 'button',
action: () => editor.value.chain().focus().addColumnAfter().run(),
enabled: () => editor.value.can().addColumnAfter(),
title: 'Add column after',
icon: ColumnInsertRight,
},
{
type: 'button',
action: () => editor.value.chain().focus().deleteColumn().run(),
enabled: () => editor.value.can().deleteColumn(),
title: 'Delete column',
icon: Cross,
},
{
type: 'divider',
},
{
type: 'button',
action: () => editor.value.chain().focus().addRowBefore().run(),
enabled: () => editor.value.can().addRowBefore(),
title: 'Add row before',
icon: RowInsertTop,
},
{
type: 'button',
action: () => editor.value.chain().focus().addRowAfter().run(),
enabled: () => editor.value.can().addRowAfter(),
title: 'Add row after',
icon: RowInsertBottom,
},
{
type: 'button',
action: () => editor.value.chain().focus().deleteRow().run(),
enabled: () => editor.value.can().deleteRow(),
title: 'Delete row',
icon: Cross,
},
{
type: 'divider',
},
{
type: 'button',
action: () => editor.value.chain().focus().deleteTable().run(),
enabled: () => editor.value.can().deleteTable(),
title: 'Delete table',
icon: TableOff,
},
{
type: 'divider',
},
{
type: 'button',
action: () => editor.value.chain().focus().mergeCells().run(),
enabled: () => editor.value.can().mergeCells(),
title: 'Merge cells',
icon: LayersUnion,
},
{
type: 'button',
action: () => editor.value.chain().focus().splitCell().run(),
enabled: () => editor.value.can().splitCell(),
title: 'Split cell',
icon: SeparatorVertical,
},
{
type: 'button',
action: () => editor.value.chain().focus().mergeOrSplit().run(),
enabled: () => editor.value.can().mergeOrSplit(),
title: 'Merge or split',
icon: LayersIntersect2,
},
{
type: 'divider',
},
{
type: 'button',
action: () => editor.value.chain().focus().toggleHeaderColumn().run(),
enabled: () => editor.value.can().toggleHeaderColumn(),
title: 'Toggle header column',
icon: LayoutDistributeVertical,
},
{
type: 'button',
action: () => editor.value.chain().focus().toggleHeaderRow().run(),
enabled: () => editor.value.can().toggleHeaderRow(),
title: 'Toggle header row',
icon: LayoutDistributeHorizontal,
},
{
type: 'button',
action: () => editor.value.chain().focus().toggleHeaderCell().run(),
enabled: () => editor.value.can().toggleHeaderCell(),
title: 'Toggle header cell',
icon: Heading,
},
{
type: 'divider',
},
{
type: 'button',
action: () => editor.value.chain().focus().fixTables().run(),
enabled: () => editor.value.can().fixTables(),
title: 'Fix tables',
icon: Tool,
},
];
</script>
<template>
<div flex items-center>
<div flex flex-wrap items-center>
<template v-for="(item, index) in items">
<n-divider v-if="item.type === 'divider'" :key="`divider${index}`" vertical />
<MenuBarItem v-else-if="item.type === 'button'" :key="index" v-bind="item" />
<c-tooltip
v-if="item.type === 'color'" :key="`color${index}`"
:tooltip="item.title"
>
<n-color-picker
style="width: 120px"
:show-alpha="false"
:actions="['confirm']"
:value="item.value()"
@confirm="item.action"
/>
</c-tooltip>
</template>
</div>
</template>