tiptapify 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,98 @@
1
+ import { VueNodeViewRenderer } from '@tiptap/vue-3'
2
+ import { TextStyleKit } from '@tiptap/extension-text-style'
3
+ import { BulletList, OrderedList, ListItem, ListKeymap, TaskList, TaskItem } from '@tiptap/extension-list'
4
+ import { Selection, Focus, Placeholder, UndoRedo, Dropcursor, CharacterCount } from '@tiptap/extensions'
5
+ import { Document } from '@tiptap/extension-document'
6
+ import { Text } from '@tiptap/extension-text'
7
+ import { Paragraph } from '@tiptap/extension-paragraph'
8
+ import { Heading } from '@tiptap/extension-heading'
9
+ import { Bold } from '@tiptap/extension-bold'
10
+ import { Italic } from '@tiptap/extension-italic'
11
+ import { Strike } from '@tiptap/extension-strike'
12
+ import { Code } from '@tiptap/extension-code'
13
+ import { Blockquote } from '@tiptap/extension-blockquote'
14
+ import { HardBreak } from '@tiptap/extension-hard-break'
15
+ import { HorizontalRule } from '@tiptap/extension-horizontal-rule'
16
+ import { Typography } from '@tiptap/extension-typography'
17
+ import { Highlight } from '@tiptap/extension-highlight'
18
+ import { Image } from '@tiptap/extension-image'
19
+ import { Superscript } from '@tiptap/extension-superscript'
20
+ import { Subscript } from '@tiptap/extension-subscript'
21
+ import { TextAlign } from '@tiptap/extension-text-align'
22
+ import { Underline } from '@tiptap/extension-underline'
23
+ import { TableKit } from '@tiptap/extension-table'
24
+ import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight'
25
+
26
+ import { Link } from '@tiptap/extension-link'
27
+ import CodeBlockComponent from '@tiptapify/components/CodeBlockComponent.vue'
28
+ import SlashCommands from '@tiptapify/components/extensions/slash-commands'
29
+ import suggestion from '@tiptapify/components/extensions/components/slashCommands/suggestion'
30
+
31
+ // load all languages with "all" or common languages with "common"
32
+ import { common, createLowlight } from 'lowlight'
33
+
34
+ // create a lowlight instance
35
+ // using all available languages
36
+ const lowlight = createLowlight(common)
37
+
38
+ // or you can register specific languages
39
+ // const lowlight = createLowlight()
40
+ //
41
+ // import language example
42
+ // import ts from 'highlight.js/lib/languages/typescript'
43
+ //
44
+ // register language example
45
+ // lowlight.register('ts', ts)
46
+
47
+ export const editorExtensions = (placeholder: string = '') => [
48
+ TextStyleKit,
49
+ Document,
50
+ Text,
51
+ Paragraph,
52
+ Heading,
53
+ Bold,
54
+ Italic,
55
+ Strike,
56
+ Blockquote,
57
+ OrderedList,
58
+ BulletList,
59
+ TaskList,
60
+ TaskItem,
61
+ ListItem,
62
+ ListKeymap,
63
+ HardBreak,
64
+ HorizontalRule,
65
+ Dropcursor,
66
+ Typography,
67
+ Underline,
68
+ Highlight,
69
+ Link.configure({
70
+ openOnClick: false,
71
+ defaultProtocol: 'https',
72
+ }),
73
+ Image,
74
+ Superscript,
75
+ Subscript,
76
+ TableKit,
77
+ Code,
78
+ UndoRedo,
79
+ Focus,
80
+ CodeBlockLowlight
81
+ .extend({
82
+ addNodeView() {
83
+ return VueNodeViewRenderer(CodeBlockComponent)
84
+ },
85
+ })
86
+ .configure({ lowlight }),
87
+ Selection.configure({
88
+ className: 'selection',
89
+ }),
90
+ TextAlign.configure({
91
+ types: ['heading', 'paragraph'],
92
+ }),
93
+ Placeholder.configure({ placeholder }),
94
+ SlashCommands.configure({
95
+ suggestion,
96
+ }),
97
+ CharacterCount
98
+ ]
@@ -0,0 +1,98 @@
1
+ <script setup lang="ts">
2
+
3
+ import * as mdi from '@mdi/js'
4
+ import { useEditor } from "@tiptapify/composable/useEditor";
5
+
6
+ import { useI18n } from 'vue-i18n'
7
+ import { computed, ref, watch } from 'vue'
8
+
9
+ defineExpose({ open })
10
+
11
+ interface Props {
12
+ value?: string
13
+ target?: '_self' | '_blank'
14
+ destroy?: () => void
15
+ }
16
+
17
+ const props = withDefaults(defineProps<Props>(), {
18
+ value: undefined,
19
+ target: '_blank',
20
+ destroy: undefined
21
+ })
22
+
23
+ const editor = useEditor().editor
24
+ const editorInstance = editor.getInstance()
25
+
26
+ const { t } = useI18n()
27
+
28
+ const generateLinkAttrs = () => ({
29
+ href: '',
30
+ target: '_blank'
31
+ })
32
+
33
+ const attrs = ref(generateLinkAttrs())
34
+
35
+ const dialog = ref<boolean>(false)
36
+
37
+ const isDisabled = computed(() => {
38
+ const { href, target } = attrs.value
39
+ if (!href) return true
40
+
41
+ return props.value === href && props.target === target
42
+ })
43
+
44
+ function apply() {
45
+ const { href, target } = attrs.value
46
+
47
+ if (href) {
48
+ editorInstance.value.chain().focus().extendMarkRange('link').setLink({ href, target }).run()
49
+ }
50
+ close()
51
+ }
52
+
53
+ function open() {
54
+ dialog.value = true
55
+ }
56
+
57
+ function close() {
58
+ dialog.value = false
59
+ attrs.value = generateLinkAttrs()
60
+
61
+ setTimeout(() => props.destroy?.(), 300)
62
+ }
63
+
64
+ watch(dialog, val => {
65
+ if (!val) return
66
+
67
+ attrs.value = {
68
+ href: props.value,
69
+ target: props.target
70
+ }
71
+ })
72
+ </script>
73
+
74
+ <template>
75
+ <VDialog v-model="dialog" max-width="400" absolute @click:outside="close">
76
+ <VCard>
77
+ <VToolbar class="px-6" density="compact">
78
+ <span class="headline">{{ t('dialog.link.title') }}</span>
79
+
80
+ <VSpacer />
81
+
82
+ <VBtn class="mx-0" icon @click="close">
83
+ <VIcon :icon="mdi.mdiClose" />
84
+ </VBtn>
85
+ </VToolbar>
86
+
87
+ <VCardText>
88
+ <VTextField v-model="attrs.href" :label="t('dialog.link.placeholder')" autofocus />
89
+ </VCardText>
90
+
91
+ <VCardActions>
92
+ <VBtn :disabled="isDisabled" @click="apply">
93
+ {{ t('dialog.link.apply') }}
94
+ </VBtn>
95
+ </VCardActions>
96
+ </VCard>
97
+ </VDialog>
98
+ </template>
@@ -0,0 +1,103 @@
1
+ <script setup lang="ts">
2
+ import { ref, watch } from 'vue'
3
+ import * as mdi from '@mdi/js'
4
+
5
+ const props = defineProps({
6
+ items: {
7
+ type: Array,
8
+ required: true,
9
+ },
10
+ command: {
11
+ type: Function,
12
+ required: true,
13
+ },
14
+ })
15
+
16
+ const selectedIndex = ref(0)
17
+
18
+ watch(() => props.items, () => {
19
+ selectedIndex.value = 0
20
+ }, { deep: true })
21
+
22
+ function onKeyDown({ event }) {
23
+ if (event.key === 'ArrowUp') {
24
+ upHandler()
25
+ return true
26
+ }
27
+
28
+ if (event.key === 'ArrowDown') {
29
+ downHandler()
30
+ return true
31
+ }
32
+
33
+ if (event.key === 'Enter') {
34
+ enterHandler()
35
+ return true
36
+ }
37
+
38
+ return false
39
+ }
40
+
41
+ function upHandler() {
42
+ selectedIndex.value = (selectedIndex.value + props.items.length - 1) % props.items.length
43
+ }
44
+
45
+ function downHandler() {
46
+ selectedIndex.value = (selectedIndex.value + 1) % props.items.length
47
+ }
48
+
49
+ function enterHandler() {
50
+ selectItem(selectedIndex.value)
51
+ }
52
+
53
+ function selectItem(index) {
54
+ const item = props.items[index]
55
+
56
+ if (item) {
57
+ props.command(item)
58
+ }
59
+ }
60
+
61
+ defineExpose({
62
+ onKeyDown,
63
+ upHandler,
64
+ downHandler,
65
+ enterHandler,
66
+ selectItem
67
+ })
68
+ </script>
69
+
70
+ <template>
71
+ <div class="dropdown-menu">
72
+ <template v-if="items.length">
73
+ <VBtn
74
+ variant="text"
75
+ v-for="(item, index) in items"
76
+ :key="index"
77
+ @click="selectItem(index)"
78
+ size="small"
79
+ >
80
+ <VIcon v-if="item.icon" :icon="mdi[`mdi${item.icon}`]" size="16" />
81
+ <span v-else>
82
+ {{ item.title }}
83
+ </span>
84
+ </VBtn>
85
+ </template>
86
+ <div class="item" v-else>No result</div>
87
+ </div>
88
+ </template>
89
+
90
+ <style scoped lang="scss">
91
+ .dropdown-menu {
92
+ background: var(--white);
93
+ border: 1px solid var(--gray-1);
94
+ border-radius: 0.7rem;
95
+ box-shadow: var(--shadow);
96
+ display: flex;
97
+ flex-direction: column;
98
+ gap: 0.1rem;
99
+ overflow: auto;
100
+ padding: 0.4rem;
101
+ position: relative;
102
+ }
103
+ </style>
@@ -0,0 +1,107 @@
1
+ import { computePosition, flip, shift } from '@floating-ui/dom'
2
+ import { posToDOMRect, VueRenderer } from '@tiptap/vue-3'
3
+
4
+ import CommandsList from './CommandsList.vue'
5
+
6
+ const updatePosition = (editor, element) => {
7
+ const virtualElement = {
8
+ getBoundingClientRect: () => posToDOMRect(editor.view, editor.state.selection.from, editor.state.selection.to),
9
+ }
10
+
11
+ computePosition(virtualElement, element, {
12
+ placement: 'bottom-start',
13
+ strategy: 'absolute',
14
+ middleware: [shift(), flip()],
15
+ }).then(({ x, y, strategy }) => {
16
+ element.style.width = 'max-content'
17
+ element.style.position = strategy
18
+ element.style.left = `${x}px`
19
+ element.style.top = `${y}px`
20
+ })
21
+ }
22
+
23
+ export default {
24
+ items: ({ query }) => {
25
+ return [
26
+ {
27
+ title: 'H1',
28
+ icon: 'FormatHeader1',
29
+ command: ({ editor, range }) => {
30
+ editor.chain().focus().deleteRange(range).setNode('heading', { level: 1 }).run()
31
+ },
32
+ },
33
+ {
34
+ title: 'H2',
35
+ icon: 'FormatHeader2',
36
+ command: ({ editor, range }) => {
37
+ editor.chain().focus().deleteRange(range).setNode('heading', { level: 2 }).run()
38
+ },
39
+ },
40
+ {
41
+ title: 'Bold',
42
+ icon: 'FormatBold',
43
+ command: ({ editor, range }) => {
44
+ editor.chain().focus().deleteRange(range).setMark('bold').run()
45
+ },
46
+ },
47
+ {
48
+ title: 'Italic',
49
+ icon: 'FormatItalic',
50
+ command: ({ editor, range }) => {
51
+ editor.chain().focus().deleteRange(range).setMark('italic').run()
52
+ },
53
+ },
54
+ ]
55
+ .filter(item => item.title.toLowerCase().startsWith(query.toLowerCase()))
56
+ .slice(0, 10)
57
+ },
58
+
59
+ render: () => {
60
+ let component
61
+
62
+ return {
63
+ onStart: props => {
64
+ component = new VueRenderer(CommandsList, {
65
+ props,
66
+ editor: props.editor,
67
+ })
68
+
69
+ if (!props.clientRect) {
70
+ return
71
+ }
72
+
73
+ component.element.style.position = 'absolute'
74
+
75
+ document.body.appendChild(component.element)
76
+
77
+ updatePosition(props.editor, component.element)
78
+ },
79
+
80
+ onUpdate(props) {
81
+ component.updateProps(props)
82
+
83
+ if (!props.clientRect) {
84
+ return
85
+ }
86
+
87
+ updatePosition(props.editor, component.element)
88
+ },
89
+
90
+ onKeyDown(props) {
91
+ if (props.event.key === 'Escape') {
92
+ component.destroy()
93
+ component.element.remove()
94
+
95
+ return true
96
+ }
97
+
98
+ return component.ref?.onKeyDown(props)
99
+ },
100
+
101
+ onExit() {
102
+ component.destroy()
103
+ component.element.remove()
104
+ },
105
+ }
106
+ },
107
+ }
@@ -0,0 +1,28 @@
1
+ import { Extension } from '@tiptap/core'
2
+ import Suggestion from '@tiptap/suggestion'
3
+
4
+ export default Extension.create(
5
+ {
6
+ name: 'slash-commands',
7
+
8
+ addOptions() {
9
+ return {
10
+ suggestion: {
11
+ char: '/',
12
+ command: ({editor, range, props}) => {
13
+ props.command({editor, range})
14
+ },
15
+ },
16
+ }
17
+ },
18
+
19
+ addProseMirrorPlugins() {
20
+ return [
21
+ Suggestion(
22
+ {
23
+ editor: this.editor,
24
+ ...this.options.suggestion,
25
+ }),
26
+ ]
27
+ },
28
+ })
@@ -0,0 +1,35 @@
1
+ import { useEditor as useEditorOriginal } from '@tiptap/vue-3'
2
+
3
+ import { editorExtensions } from '@tiptapify/components/editorExtensions'
4
+
5
+ let editorInstance: any = null
6
+
7
+ export function useEditor(content: any = '', placeholder: string = '') {
8
+ class TiptapifyEditor {
9
+ private editor
10
+
11
+ constructor() {
12
+ const extensions = editorExtensions(placeholder)
13
+ this.editor = useEditorOriginal({
14
+ content,
15
+ extensions,
16
+ })
17
+ }
18
+
19
+ getInstance() {
20
+ return this.editor
21
+ }
22
+
23
+ destroy() {
24
+ this.editor.value.destroy()
25
+ }
26
+ }
27
+
28
+ if (!editorInstance) {
29
+ editorInstance = new TiptapifyEditor()
30
+ }
31
+
32
+ return {
33
+ editor: editorInstance
34
+ }
35
+ }
@@ -0,0 +1,22 @@
1
+ import { createI18n } from 'vue-i18n'
2
+
3
+ const messages = Object.fromEntries(
4
+ Object.entries(
5
+ import.meta.glob<{ default: any }>('./locales/*.json', { eager: true }))
6
+ .map(([key, value]) => [key.slice(10, -5), value.default]),
7
+ )
8
+
9
+ let _i18n: any = null
10
+
11
+ export const getI18n = (locale: string) => {
12
+ if (_i18n === null) {
13
+ _i18n = createI18n({
14
+ legacy: false,
15
+ locale: locale,
16
+ fallbackLocale: 'en',
17
+ messages
18
+ })
19
+ }
20
+
21
+ return _i18n
22
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "style": {
3
+ "heading": "heading",
4
+ "fontFamily": "font family",
5
+ "fontSize": "font size",
6
+ "lineHeight": "line height"
7
+ },
8
+ "format": {
9
+ "bold": "bold",
10
+ "italic": "italic",
11
+ "strike": "strikethrough",
12
+ "underline": "underline",
13
+ "sup": "superscript",
14
+ "sub": "subscript",
15
+ "break": "hard break",
16
+ "highlight": "highlight",
17
+ "line": "horizontal line",
18
+ "blockquote": "cite",
19
+ "code": "code",
20
+ "codeblock": "code block",
21
+ "codeblock_syntax": "code block w/ syntax highlighting",
22
+ "link": "external link"
23
+ },
24
+ "action": {
25
+ "undo": "undo",
26
+ "redo": "redo"
27
+ },
28
+ "alignment": "alignment",
29
+ "alignments": {
30
+ "left": "align left",
31
+ "center": "align center",
32
+ "right": "align right",
33
+ "justify": "justify"
34
+ },
35
+ "list": "list",
36
+ "lists": {
37
+ "bullet": "unordered list",
38
+ "numbered": "ordered list",
39
+ "task": "task list",
40
+ "indent": "list item indent",
41
+ "outdent": "list item outdent"
42
+ },
43
+ "dialog": {
44
+ "link": {
45
+ "title": "add/edit link",
46
+ "placeholder": "link address",
47
+ "apply": "apply"
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "style": {
3
+ "heading": "заголовок",
4
+ "fontFamily": "шрифт",
5
+ "fontSize": "размер",
6
+ "lineHeight": "высота строки"
7
+ },
8
+ "format": {
9
+ "bold": "жирный",
10
+ "italic": "курсив",
11
+ "strike": "зачеркивание",
12
+ "underline": "подчеркивание",
13
+ "sup": "верхний индекс",
14
+ "sub": "нижний индекс",
15
+ "break": "разрыв строки",
16
+ "highlight": "выделение",
17
+ "line": "горизонтальная линия",
18
+ "blockquote": "цитата",
19
+ "code": "код",
20
+ "codeblock": "блок кода",
21
+ "codeblock_syntax": "блок кода с подсветкой синтаксиса",
22
+ "link": "внешняя ссылка"
23
+ },
24
+ "action": {
25
+ "undo": "отмена",
26
+ "redo": "повтор"
27
+ },
28
+ "alignment": "выравнивание",
29
+ "alignments": {
30
+ "left": "выравнивание по левому краю",
31
+ "center": "выравнивание по центру",
32
+ "right": "выравнивание по правому краю",
33
+ "justify": "выравнивание по ширине"
34
+ },
35
+ "list": "список",
36
+ "lists": {
37
+ "bullet": "ненумерованный список",
38
+ "numbered": "нумерованный список",
39
+ "task": "список задач",
40
+ "indent": "увеличить отступ элемента списка",
41
+ "outdent": "уменьшить отступ элемента списка"
42
+ },
43
+ "dialog": {
44
+ "link": {
45
+ "title": "добавление/изменение ссылки",
46
+ "placeholder": "адрес ссылки",
47
+ "apply": "применить"
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "style": {
3
+ "heading": "заголовок",
4
+ "fontFamily": "шрифт",
5
+ "fontSize": "розмір",
6
+ "lineHeight": "висота строки"
7
+ },
8
+ "format": {
9
+ "bold": "жирний",
10
+ "italic": "курсив",
11
+ "strike": "закреслення",
12
+ "underline": "підкреслення",
13
+ "sup": "верхній індекс",
14
+ "sub": "нижній індекс",
15
+ "break": "розрив строки",
16
+ "highlight": "виділення",
17
+ "line": "горизонтальна лінія",
18
+ "blockquote": "цитата",
19
+ "code": "код",
20
+ "codeblock": "блок коду",
21
+ "codeblock_syntax": "блок коду з підсвіткою синтаксиса",
22
+ "link": "зовнішнє посилання"
23
+ },
24
+ "action": {
25
+ "undo": "відміна",
26
+ "redo": "повтор"
27
+ },
28
+ "alignment": "вирівнювання",
29
+ "alignments": {
30
+ "left": "вирівнювання по лівому краю",
31
+ "center": "вирівнювання по центру",
32
+ "right": "вирівнювання по правому краю",
33
+ "justify": "вирівнювання по ширині"
34
+ },
35
+ "list": "список",
36
+ "lists": {
37
+ "bullet": "ненумерований список",
38
+ "numbered": "нумерований список",
39
+ "task": "список задач",
40
+ "indent": "збільшити відступ елемента списку",
41
+ "outdent": "зменшити відступ елемента списку"
42
+ },
43
+ "dialog": {
44
+ "link": {
45
+ "title": "додавання/зміна посилання",
46
+ "placeholder": "адреса посилання",
47
+ "apply": "застосувати"
48
+ }
49
+ }
50
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { Plugin } from 'vue';
2
+ import Tiptapify from '@tiptapify/components/Tiptapify.vue';
3
+ import { getI18n } from "@tiptapify/i18n";
4
+
5
+ interface PackageOptions {
6
+ locale?: string;
7
+ }
8
+
9
+ const TiptapifyPlugin: Plugin = {
10
+ install(app, options: PackageOptions = {}) {
11
+ const locale = options.locale || 'en'
12
+ app.use(getI18n(locale));
13
+ app.component('Tiptapify', Tiptapify);
14
+ app.component('v-tiptap', Tiptapify);
15
+ }
16
+ };
17
+
18
+ export { Tiptapify };
19
+ export default TiptapifyPlugin;
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Node",
6
+ "strict": true,
7
+ "jsx": "preserve",
8
+ "esModuleInterop": true,
9
+ "sourceMap": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "lib": ["ESNext", "DOM"],
13
+ "types": ["vite/client", "node"],
14
+ "declaration": true,
15
+ "declarationDir": "lib",
16
+ "baseUrl": ".",
17
+ "paths": {
18
+ "@tiptapify/*": ["src/*"]
19
+ }
20
+ },
21
+ "include": ["src/**/*"]
22
+ }