This commit is contained in:
sharevb 2024-10-24 15:42:29 -07:00 committed by GitHub
commit ddc78fcf81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 658 additions and 391 deletions

1
components.d.ts vendored
View file

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

View file

@ -40,9 +40,17 @@
"@it-tools/oggen": "^1.3.0", "@it-tools/oggen": "^1.3.0",
"@regexper/render": "^1.0.0", "@regexper/render": "^1.0.0",
"@sindresorhus/slugify": "^2.2.1", "@sindresorhus/slugify": "^2.2.1",
"@tiptap/pm": "2.1.6", "@tiptap/extension-color": "^2.7.4",
"@tiptap/starter-kit": "2.1.6", "@tiptap/extension-gapcursor": "^2.7.4",
"@tiptap/vue-3": "2.0.3", "@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", "@types/figlet": "^1.5.8",
"@types/markdown-it": "^13.0.7", "@types/markdown-it": "^13.0.7",
"@vicons/material": "^0.12.0", "@vicons/material": "^0.12.0",

747
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 { Editor, EditorContent } from '@tiptap/vue-3';
import StarterKit from '@tiptap/starter-kit'; import StarterKit from '@tiptap/starter-kit';
import { useThemeVars } from 'naive-ui'; 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'; import MenuBar from './menu-bar.vue';
const props = defineProps<{ html: string }>(); const props = defineProps<{ html: string }>();
@ -12,7 +19,18 @@ const html = useVModel(props, 'html', emit);
const editor = new Editor({ const editor = new Editor({
content: html.value, 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())); editor.on('update', ({ editor }) => emit('update:html', editor.getHTML()));
@ -63,6 +81,63 @@ tryOnBeforeUnmount(() => {
line-height: 1.1; 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 { code {
background-color: v-bind('themeVars.codeColor'); background-color: v-bind('themeVars.codeColor');
padding: 2px 4px; padding: 2px 4px;
@ -84,10 +159,6 @@ tryOnBeforeUnmount(() => {
} }
} }
mark {
background-color: #faf594;
}
img { img {
max-width: 100%; max-width: 100%;
height: auto; height: auto;

View file

@ -1,13 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Component } from 'vue'; import type { Component } from 'vue';
const props = defineProps<{ icon: Component; title: string; action: () => void; isActive?: () => boolean }>(); const props = defineProps<{ icon: Component; title: string; action: () => void; isActive?: () => boolean; enabled?: () => boolean }>();
const { icon, title, action, isActive } = toRefs(props); const { icon, title, action, isActive, enabled } = toRefs(props);
</script> </script>
<template> <template>
<c-tooltip :tooltip="title"> <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" /> <n-icon :component="icon" />
</c-button> </c-button>
</c-tooltip> </c-tooltip>

View file

@ -8,15 +8,25 @@ import {
ClearFormatting, ClearFormatting,
Code, Code,
CodePlus, CodePlus,
ColorPicker,
ColumnInsertLeft,
ColumnInsertRight,
Cross,
H1, H1,
H2, H2,
H3, H3,
H4, H4,
Heading,
Italic, Italic,
LayersIntersect2,
LayersUnion,
LayoutDistributeHorizontal,
LayoutDistributeVertical,
List, List,
ListNumbers, ListNumbers,
Strikethrough, RowInsertBottom,
TextWrap, RowInsertTop,
SeparatorVertical, Strikethrough, Table, TableOff, TextWrap, Tool,
} from '@vicons/tabler'; } from '@vicons/tabler';
import type { Component } from 'vue'; import type { Component } from 'vue';
import MenuBarItem from './menu-bar-item.vue'; import MenuBarItem from './menu-bar-item.vue';
@ -29,9 +39,18 @@ type MenuItem =
icon: Component icon: Component
title: string title: string
action: () => void action: () => void
value?: () => string
isActive?: () => boolean isActive?: () => boolean
enabled?: () => boolean
type: 'button' type: 'button'
} }
| {
icon: Component
title: string
action: (color: string) => void
value: () => string
type: 'color'
}
| { type: 'divider' }; | { type: 'divider' };
const items: MenuItem[] = [ const items: MenuItem[] = [
@ -141,7 +160,42 @@ const items: MenuItem[] = [
title: 'Clear format', title: 'Clear format',
action: () => editor.value.chain().focus().clearNodes().unsetAllMarks().run(), 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', type: 'button',
icon: ArrowBack, icon: ArrowBack,
@ -154,14 +208,152 @@ const items: MenuItem[] = [
title: 'Redo', title: 'Redo',
action: () => editor.value.chain().focus().redo().run(), 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> </script>
<template> <template>
<div flex items-center> <div flex flex-wrap items-center>
<template v-for="(item, index) in items"> <template v-for="(item, index) in items">
<n-divider v-if="item.type === 'divider'" :key="`divider${index}`" vertical /> <n-divider v-if="item.type === 'divider'" :key="`divider${index}`" vertical />
<MenuBarItem v-else-if="item.type === 'button'" :key="index" v-bind="item" /> <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> </template>
</div> </div>
</template> </template>