diff options
author | 2023-03-26 19:04:42 +0200 | |
---|---|---|
committer | 2023-03-26 20:21:00 +0200 | |
commit | f3b1863f093124309963c3ad7c275142cd370b0f (patch) | |
tree | c8dd861e9d5d253bcb655c2c055965fae7f952c5 /src/tools | |
parent | b1d6bfd2dcb718b486dcd708adcdea65341b3d16 (diff) | |
download | it-tools-f3b1863f093124309963c3ad7c275142cd370b0f.tar.gz it-tools-f3b1863f093124309963c3ad7c275142cd370b0f.tar.zst it-tools-f3b1863f093124309963c3ad7c275142cd370b0f.zip |
feat(new-tool): html wysiwyg editor
Diffstat (limited to 'src/tools')
-rw-r--r-- | src/tools/html-wysiwyg-editor/editor/editor.vue | 135 | ||||
-rw-r--r-- | src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue | 22 | ||||
-rw-r--r-- | src/tools/html-wysiwyg-editor/editor/menu-bar.vue | 171 | ||||
-rw-r--r-- | src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue | 17 | ||||
-rw-r--r-- | src/tools/html-wysiwyg-editor/index.ts | 11 | ||||
-rw-r--r-- | src/tools/index.ts | 2 |
6 files changed, 358 insertions, 0 deletions
diff --git a/src/tools/html-wysiwyg-editor/editor/editor.vue b/src/tools/html-wysiwyg-editor/editor/editor.vue new file mode 100644 index 0000000..346cb10 --- /dev/null +++ b/src/tools/html-wysiwyg-editor/editor/editor.vue @@ -0,0 +1,135 @@ +<template> + <n-card v-if="editor" class="editor"> + <template #header> + <menu-bar class="editor-header" :editor="editor" /> + <n-divider style="margin-top: 0" /> + </template> + + <editor-content class="editor-content" :editor="editor" /> + </n-card> +</template> + +<script setup lang="ts"> +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 MenuBar from './menu-bar.vue'; + +const themeVars = useThemeVars(); +const props = defineProps<{ html: string }>(); +const emit = defineEmits(['update:html']); +const html = useVModel(props, 'html', emit); + +const editor = new Editor({ + content: html.value, + extensions: [StarterKit], +}); + +editor.on('update', ({ editor }) => emit('update:html', editor.getHTML())); + +tryOnBeforeUnmount(() => { + editor.destroy(); +}); +</script> + +<style scoped lang="less"> +::v-deep(.n-card-header) { + padding: 0; +} + +::v-deep(.ProseMirror-focused) { + outline: none; +} +</style> + +<style scoped lang="less"> +::v-deep(.ProseMirror) { + > * + * { + margin-top: 0.75em; + } + + p { + margin: 0; + } + + ul, + ol { + padding: 0 1rem; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + line-height: 1.1; + } + + code { + background-color: v-bind('themeVars.codeColor'); + padding: 2px 4px; + border-radius: 5px; + font-size: 85%; + } + + pre { + background: v-bind('themeVars.codeColor'); + font-family: monospace; + padding: 0.75rem 1rem; + border-radius: 0.5rem; + + code { + color: inherit; + padding: 0; + background: none; + font-size: 0.8rem; + } + } + + mark { + background-color: #faf594; + } + + img { + max-width: 100%; + height: auto; + } + + hr { + margin: 1rem 0; + } + + blockquote { + padding-left: 1rem; + border-left: 2px solid rgba(#0d0d0d, 0.1); + } + + hr { + border: none; + border-top: 2px solid rgba(#0d0d0d, 0.1); + margin: 2rem 0; + } + + ul[data-type='taskList'] { + list-style: none; + padding: 0; + + li { + display: flex; + align-items: center; + + > label { + flex: 0 0 auto; + margin-right: 0.5rem; + user-select: none; + } + + > div { + flex: 1 1 auto; + } + } + } +} +</style> diff --git a/src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue b/src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue new file mode 100644 index 0000000..52bc0d2 --- /dev/null +++ b/src/tools/html-wysiwyg-editor/editor/menu-bar-item.vue @@ -0,0 +1,22 @@ +<template> + <n-tooltip trigger="hover"> + <template #trigger> + <n-button circle quaternary :type="isActive?.() ? 'primary' : 'default'" @click="action"> + <template #icon> + <n-icon :component="icon" /> + </template> + </n-button> + </template> + + {{ title }} + </n-tooltip> +</template> + +<script setup lang="ts"> +import { toRefs, type Component } from 'vue'; + +const props = defineProps<{ icon: Component; title: string; action: () => void; isActive?: () => boolean }>(); +const { icon, title, action, isActive } = toRefs(props); +</script> + +<style scoped></style> diff --git a/src/tools/html-wysiwyg-editor/editor/menu-bar.vue b/src/tools/html-wysiwyg-editor/editor/menu-bar.vue new file mode 100644 index 0000000..5304cd3 --- /dev/null +++ b/src/tools/html-wysiwyg-editor/editor/menu-bar.vue @@ -0,0 +1,171 @@ +<template> + <n-space align="center" :size="0"> + <template v-for="(item, index) in items"> + <n-divider v-if="item.type === 'divider'" :key="`divider${index}`" vertical /> + <menu-bar-item v-else-if="item.type === 'button'" :key="index" v-bind="item" /> + </template> + </n-space> +</template> + +<script setup lang="ts"> +import type { Editor } from '@tiptap/vue-3'; +import { + ArrowBack, + ArrowForwardUp, + Blockquote, + Bold, + ClearFormatting, + Code, + CodePlus, + H1, + H2, + H3, + H4, + Italic, + Link, + List, + ListNumbers, + Separator, + Strikethrough, + TextWrap, +} from '@vicons/tabler'; +import { toRefs, type Component } from 'vue'; +import MenuBarItem from './menu-bar-item.vue'; + +const props = defineProps<{ editor: Editor }>(); +const { editor } = toRefs(props); + +type MenuItem = + | { + icon: Component; + title: string; + action: () => void; + isActive?: () => boolean; + type: 'button'; + } + | { type: 'divider' }; + +const items: MenuItem[] = [ + { + type: 'button', + icon: Bold, + title: 'Bold', + action: () => editor.value.chain().focus().toggleBold().run(), + isActive: () => editor.value.isActive('bold'), + }, + { + type: 'button', + icon: Italic, + title: 'Italic', + action: () => editor.value.chain().focus().toggleItalic().run(), + isActive: () => editor.value.isActive('italic'), + }, + { + type: 'button', + icon: Strikethrough, + title: 'Strike', + action: () => editor.value.chain().focus().toggleStrike().run(), + isActive: () => editor.value.isActive('strike'), + }, + { + type: 'button', + icon: Code, + title: 'Inline code', + action: () => editor.value.chain().focus().toggleCode().run(), + isActive: () => editor.value.isActive('code'), + }, + { + type: 'divider', + }, + { + type: 'button', + icon: H1, + title: 'Heading 1', + action: () => editor.value.chain().focus().toggleHeading({ level: 1 }).run(), + isActive: () => editor.value.isActive('heading', { level: 1 }), + }, + { + type: 'button', + icon: H2, + title: 'Heading 2', + action: () => editor.value.chain().focus().toggleHeading({ level: 2 }).run(), + isActive: () => editor.value.isActive('heading', { level: 2 }), + }, + { + type: 'button', + icon: H3, + title: 'Heading 3', + action: () => editor.value.chain().focus().toggleHeading({ level: 4 }).run(), + isActive: () => editor.value.isActive('heading', { level: 4 }), + }, + { + type: 'button', + icon: H4, + title: 'Heading 4', + action: () => editor.value.chain().focus().toggleHeading({ level: 4 }).run(), + isActive: () => editor.value.isActive('heading', { level: 4 }), + }, + { + type: 'divider', + }, + { + type: 'button', + icon: List, + title: 'Bullet list', + action: () => editor.value.chain().focus().toggleBulletList().run(), + isActive: () => editor.value.isActive('bulletList'), + }, + { + type: 'button', + icon: ListNumbers, + title: 'Ordered list', + action: () => editor.value.chain().focus().toggleOrderedList().run(), + isActive: () => editor.value.isActive('orderedList'), + }, + { + type: 'button', + icon: CodePlus, + title: 'Code block', + action: () => editor.value.chain().focus().toggleCodeBlock().run(), + isActive: () => editor.value.isActive('codeBlock'), + }, + + { + type: 'button', + icon: Blockquote, + title: 'Blockquote', + action: () => editor.value.chain().focus().toggleBlockquote().run(), + isActive: () => editor.value.isActive('blockquote'), + }, + { + type: 'divider', + }, + { + type: 'button', + icon: TextWrap, + title: 'Hard break', + action: () => editor.value.chain().focus().setHardBreak().run(), + }, + { + type: 'button', + icon: ClearFormatting, + title: 'Clear format', + action: () => editor.value.chain().focus().clearNodes().unsetAllMarks().run(), + }, + + { + type: 'button', + icon: ArrowBack, + title: 'Undo', + action: () => editor.value.chain().focus().undo().run(), + }, + { + type: 'button', + icon: ArrowForwardUp, + title: 'Redo', + action: () => editor.value.chain().focus().redo().run(), + }, +]; +</script> + +<style scoped></style> diff --git a/src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue b/src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue new file mode 100644 index 0000000..b153769 --- /dev/null +++ b/src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue @@ -0,0 +1,17 @@ +<template> + <editor v-model:html="html" /> + <textarea-copyable :value="format(html, { parser: 'html', plugins: [htmlParser] })" language="html" /> +</template> + +<script setup lang="ts"> +import TextareaCopyable from '@/components/TextareaCopyable.vue'; +import { ref } from 'vue'; +import { format } from 'prettier'; +import htmlParser from 'prettier/parser-html'; +import { useStorage } from '@vueuse/core'; +import Editor from './editor/editor.vue'; + +const html = useStorage('html-wysiwyg-editor--html', '<h1>Hey!</h1><p>Welcome to this html wysiwyg editor</p>'); +</script> + +<style lang="less" scoped></style> diff --git a/src/tools/html-wysiwyg-editor/index.ts b/src/tools/html-wysiwyg-editor/index.ts new file mode 100644 index 0000000..985f3c1 --- /dev/null +++ b/src/tools/html-wysiwyg-editor/index.ts @@ -0,0 +1,11 @@ +import { Edit } from '@vicons/tabler'; +import { defineTool } from '../tool'; + +export const tool = defineTool({ + name: 'Html wysiwyg editor', + path: '/html-wysiwyg-editor', + description: 'Online HTML editor with feature-rich WYSIWYG editor, get the source code of the content immediately.', + keywords: ['html', 'wysiwyg', 'editor', 'p', 'ul', 'ol', 'converter', 'live'], + component: () => import('./html-wysiwyg-editor.vue'), + icon: Edit, +}); diff --git a/src/tools/index.ts b/src/tools/index.ts index 18208dd..4b4ff22 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,6 +1,7 @@ import { tool as base64FileConverter } from './base64-file-converter'; import { tool as base64StringConverter } from './base64-string-converter'; import { tool as basicAuthGenerator } from './basic-auth-generator'; +import { tool as htmlWysiwygEditor } from './html-wysiwyg-editor'; import { tool as rsaKeyPairGenerator } from './rsa-key-pair-generator'; import { tool as textToNatoAlphabet } from './text-to-nato-alphabet'; import { tool as slugifyString } from './slugify-string'; @@ -74,6 +75,7 @@ export const toolsByCategory: ToolCategory[] = [ jwtParser, keycodeInfo, slugifyString, + htmlWysiwygEditor, ], }, { |