tiptapify 0.0.36 → 0.1.0

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.
Files changed (44) hide show
  1. package/README.md +11 -5
  2. package/package.json +6 -3
  3. package/src/components/Tiptapify.vue +95 -2
  4. package/src/components/Toolbar/Index.vue +10 -0
  5. package/src/components/Toolbar/Items.vue +17 -13
  6. package/src/components/editorExtensions.ts +15 -6
  7. package/src/components/index.ts +2 -1
  8. package/src/extensions/PickerEventBus.ts +32 -0
  9. package/src/extensions/components/media/charmap/Button.vue +5 -143
  10. package/src/extensions/components/media/charmap/Picker.vue +229 -0
  11. package/src/extensions/components/media/emoji/Button.vue +5 -141
  12. package/src/extensions/components/media/emoji/Picker.vue +225 -0
  13. package/src/extensions/components/media/image/ImageDialog.vue +69 -27
  14. package/src/extensions/components/slashCommands/CommandsList.vue +65 -22
  15. package/src/extensions/components/slashCommands/PickerDialog.vue +44 -0
  16. package/src/extensions/components/slashCommands/suggestion.ts +152 -105
  17. package/src/extensions/slash-commands.ts +169 -9
  18. package/src/i18n/locales/ar.json +3 -2
  19. package/src/i18n/locales/ch.json +3 -2
  20. package/src/i18n/locales/cz.json +3 -2
  21. package/src/i18n/locales/de.json +3 -2
  22. package/src/i18n/locales/es.json +3 -2
  23. package/src/i18n/locales/fi.json +3 -2
  24. package/src/i18n/locales/fr.json +3 -2
  25. package/src/i18n/locales/hu.json +3 -2
  26. package/src/i18n/locales/it.json +3 -2
  27. package/src/i18n/locales/ja.json +3 -2
  28. package/src/i18n/locales/ko.json +3 -2
  29. package/src/i18n/locales/la.json +3 -2
  30. package/src/i18n/locales/lt.json +3 -2
  31. package/src/i18n/locales/nl.json +3 -2
  32. package/src/i18n/locales/pl.json +3 -2
  33. package/src/i18n/locales/pt.json +3 -2
  34. package/src/i18n/locales/ru.json +3 -2
  35. package/src/i18n/locales/se.json +3 -2
  36. package/src/i18n/locales/th.json +3 -2
  37. package/src/i18n/locales/tr.json +3 -2
  38. package/src/i18n/locales/{ua.json → uk.json} +3 -2
  39. package/src/i18n/locales/vi.json +3 -2
  40. package/src/types/slashCommandsTypes.ts +19 -0
  41. package/src/types/toolbarTypes.ts +1 -1
  42. package/dist/tiptapify.css +0 -1
  43. package/dist/tiptapify.mjs +0 -91388
  44. package/dist/tiptapify.umd.js +0 -202
package/README.md CHANGED
@@ -1,13 +1,19 @@
1
1
  # Tiptapify
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/tiptapify.svg)](https://www.npmjs.com/package/tiptapify)
4
+ [![npm](https://img.shields.io/npm/dw/tiptapify.svg)](https://www.npmjs.com/package/tiptapify)
5
+ [![GitHub](https://img.shields.io/github/license/ivoyt/tiptapify)](./LICENSE)
6
+
3
7
  ---
4
8
 
5
- [Tiptap](https://tiptap.dev) 3 editor for Vue3 with [Vuetify](https://vuetifyjs.com) toolbar implementation
9
+ [Tiptap 3 Editor](https://tiptap.dev) [Vuetify](https://vuetifyjs.com) toolbar implementation
6
10
 
7
11
  ## Status
8
- *Alpha*
12
+ *Beta*
13
+
14
+ ## Live Demo
9
15
 
10
- *Not production ready (yet) - may contain bugs and internal logic may change*
16
+ [View Documentation & Demo](https://ivoyt.github.io/tiptapify)
11
17
 
12
18
  ## Requirements
13
19
  - Vue 3.x
@@ -156,10 +162,10 @@ Found a bug or have ideas on improvement? Feel free to [create a ticket](https:/
156
162
  - [x] option to provide custom extension
157
163
  - [x] iframe extension
158
164
  - [x] charmap extension
165
+ - [x] demo
166
+ - [x] documentation
159
167
  - [ ] extended video extensions
160
168
  - [ ] print hotkey in a tooltip
161
- - [ ] demo
162
- - [ ] documentation
163
169
  - [ ] AI features
164
170
 
165
171
  ## Licence
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "tiptapify",
3
3
  "types": "./index.d.ts",
4
- "version": "0.0.36",
4
+ "version": "0.1.0",
5
5
  "description": "Tiptap3 editor with Vuetify3 menu implementation",
6
6
  "exports": {
7
7
  ".": {
@@ -31,7 +31,10 @@
31
31
  "build": "vite build",
32
32
  "test": "echo \"Error: no test specified\" && exit 1",
33
33
  "build:charmap": "tsx build-charmap.ts",
34
- "build:emojis": "tsx build-emojis.ts"
34
+ "build:emojis": "tsx build-emojis.ts",
35
+ "docs:dev": "pnpm --filter tiptapify-docs docs:dev",
36
+ "docs:build": "pnpm --filter tiptapify-docs docs:build",
37
+ "docs:preview": "pnpm --filter tiptapify-docs docs:preview"
35
38
  },
36
39
  "keywords": [
37
40
  "vue",
@@ -58,7 +61,7 @@
58
61
  "author": "Igor Voytovich",
59
62
  "license": "MIT",
60
63
  "repository": "https://github.com/IVoyt/tiptapify",
61
- "packageManager": "pnpm@10.32.1",
64
+ "packageManager": "pnpm@10.33.4",
62
65
  "dependencies": {
63
66
  "@tiptap/core": "^3.20.4",
64
67
  "@tiptap/extension-blockquote": "^3.20.4",
@@ -2,6 +2,7 @@
2
2
 
3
3
  import defaults from "@tiptapify/constants/defaults";
4
4
  import { itemsPropType, toolbarSections } from "@tiptapify/types/toolbarTypes";
5
+ import { SlashCommandsConfig } from "@tiptapify/types/slashCommandsTypes";
5
6
  import { computed, onBeforeUnmount, PropType, provide, ref, ShallowRef, watch } from "vue";
6
7
  import { default as Toolbar } from "@tiptapify/components/Toolbar/Index.vue";
7
8
  import { Editor, EditorContent } from '@tiptap/vue-3'
@@ -20,7 +21,7 @@ const { t, i18n, setLocale } = useLocale();
20
21
  const props = defineProps({
21
22
  locale: { type: String, default () { return 'en' } },
22
23
  content: String|Object,
23
- height: { type: Number, default () { return null } },
24
+ height: { type: [Number,String], default () { return null } },
24
25
  variantBtn: { type: String, default () { return defaults.variantBtn } },
25
26
  variantField: { type: String, default () { return defaults.variantField } },
26
27
  toolbar: { type: Boolean, default () { return true } },
@@ -28,7 +29,7 @@ const props = defineProps({
28
29
  itemsExclude: { type: Boolean, default() { return false } },
29
30
  bubbleMenu: { type: Boolean, default () { return true } },
30
31
  floatingMenu: { type: Boolean, default () { return true } },
31
- slashCommands: { type: Boolean, default () { return true } },
32
+ slashCommands: { type: [Boolean, Array] as PropType<SlashCommandsConfig>, default () { return true } },
32
33
  placeholder: { type: String, default () { return '' } },
33
34
  showWordsCount: { type: Boolean, default () { return true } },
34
35
  showCharactersCount: { type: Boolean, default () { return true } },
@@ -71,6 +72,7 @@ watch(() => editor.value, (editorInstance) => {
71
72
  editor.value.interactiveStyles = interactiveStyles.value
72
73
  emit('editor-ready', {
73
74
  editor: editorInstance,
75
+ setLocale,
74
76
  getHTML: () => editorInstance.getHTML(),
75
77
  getJSON: () => editorInstance.getJSON(),
76
78
  });
@@ -421,5 +423,96 @@ onBeforeUnmount(() => {
421
423
  ul.list-style-square {
422
424
  list-style-type: square !important;
423
425
  }
426
+
427
+ img {
428
+ display: block;
429
+ }
430
+
431
+ [data-node="image"].has-focus {
432
+ [data-resize-handle] {
433
+ position: absolute;
434
+ background: rgba(0, 0, 0, 0.5);
435
+ border: 1px solid rgba(255, 255, 255, 0.8);
436
+ border-radius: 2px;
437
+ z-index: 10;
438
+
439
+ &:hover {
440
+ background: rgba(0, 0, 0, 0.8);
441
+ }
442
+
443
+ /* Corner handles */
444
+ &[data-resize-handle='top-left'],
445
+ &[data-resize-handle='top-right'],
446
+ &[data-resize-handle='bottom-left'],
447
+ &[data-resize-handle='bottom-right'] {
448
+ width: 8px;
449
+ height: 8px;
450
+ }
451
+
452
+ &[data-resize-handle='top-left'] {
453
+ top: -4px;
454
+ left: -4px;
455
+ cursor: nwse-resize;
456
+ }
457
+
458
+ &[data-resize-handle='top-right'] {
459
+ top: -4px;
460
+ right: -4px;
461
+ cursor: nesw-resize;
462
+ }
463
+
464
+ &[data-resize-handle='bottom-left'] {
465
+ bottom: -4px;
466
+ left: -4px;
467
+ cursor: nesw-resize;
468
+ }
469
+
470
+ &[data-resize-handle='bottom-right'] {
471
+ bottom: -4px;
472
+ right: -4px;
473
+ cursor: nwse-resize;
474
+ }
475
+
476
+ /* Edge handles */
477
+ &[data-resize-handle='top'],
478
+ &[data-resize-handle='bottom'] {
479
+ height: 6px;
480
+ left: 8px;
481
+ right: 8px;
482
+ }
483
+
484
+ &[data-resize-handle='top'] {
485
+ top: -3px;
486
+ cursor: ns-resize;
487
+ }
488
+
489
+ &[data-resize-handle='bottom'] {
490
+ bottom: -3px;
491
+ cursor: ns-resize;
492
+ }
493
+
494
+ &[data-resize-handle='left'],
495
+ &[data-resize-handle='right'] {
496
+ width: 6px;
497
+ top: 8px;
498
+ bottom: 8px;
499
+ }
500
+
501
+ &[data-resize-handle='left'] {
502
+ left: -3px;
503
+ cursor: ew-resize;
504
+ }
505
+
506
+ &[data-resize-handle='right'] {
507
+ right: -3px;
508
+ cursor: ew-resize;
509
+ }
510
+ }
511
+
512
+ [data-resize-state='true'] [data-resize-wrapper] {
513
+ outline: 1px solid rgba(0, 0, 0, 0.25);
514
+ border-radius: 0.125rem;
515
+ }
516
+ }
424
517
  }
425
518
  </style>
@@ -54,6 +54,16 @@ function prepareToolbarItems() {
54
54
  }
55
55
  } else if (propsItems.value.length > 0) {
56
56
  for (const propsItem of propsItems.value as string[]) {
57
+ if (propsItem === '|') {
58
+ if (typeof _toolbarItems['__separator__'] === 'undefined') {
59
+ _toolbarItems['__separator__'] = {
60
+ section: '__separator__',
61
+ group: false,
62
+ components: []
63
+ }
64
+ }
65
+ continue
66
+ }
57
67
  const item = propsItem.split(':')
58
68
  if (!availableItemsKeys.includes(item[0])) {
59
69
  throw new Error(`The ${propsItem} is unknown extension. Please use one of the following: ${availableItemsKeys.join(', ')}`)
@@ -15,21 +15,25 @@ defineProps({
15
15
  <template>
16
16
  <VToolbarItems class="py-2">
17
17
  <template v-for="item in items" :key="item.section">
18
- <VBtnGroup v-if="item.group" elevation="4">
19
- <template v-for="sectionItem in item.components" :key="sectionItem.name">
20
- <component :is="sectionItem.component" v-bind="{ ...sectionItem.props ?? {} }" />
21
- </template>
22
- </VBtnGroup>
18
+ <template v-if="item.section === '__separator__'">
19
+ <div class="menu-divider"></div>
20
+ </template>
23
21
  <template v-else>
24
- <component
25
- v-for="sectionItem in item.components"
26
- :key="sectionItem.name"
27
- :is="sectionItem.component"
28
- v-bind="{ variantBtn, ...sectionItem.props ?? {} }"
29
- />
22
+ <VBtnGroup v-if="item.group" elevation="4">
23
+ <template v-for="sectionItem in item.components" :key="sectionItem.name">
24
+ <component :is="sectionItem.component" v-bind="{ ...sectionItem.props ?? {} }" />
25
+ </template>
26
+ </VBtnGroup>
27
+ <template v-else>
28
+ <component
29
+ v-for="sectionItem in item.components"
30
+ :key="sectionItem.name"
31
+ :is="sectionItem.component"
32
+ v-bind="{ variantBtn, ...sectionItem.props ?? {} }"
33
+ />
34
+ </template>
35
+ <div class="menu-divider"></div>
30
36
  </template>
31
-
32
- <div class="menu-divider"></div>
33
37
  </template>
34
38
  </VToolbarItems>
35
39
  </template>
@@ -31,8 +31,8 @@ import { TiptapifyImage } from '@tiptapify/extensions/components/media/image'
31
31
  import { TiptapifyLink } from '@tiptapify/extensions/components/media/link'
32
32
  import { TiptapifyVideo } from '@tiptapify/extensions/components/media/video'
33
33
  import CodeBlockComponent from '@tiptapify/extensions/components/CodeBlockComponent.vue'
34
- import SlashCommands from '@tiptapify/extensions/slash-commands'
35
- import suggestion from '@tiptapify/extensions/components/slashCommands/suggestion'
34
+ import SlashCommands, { SlashCommandsExtensionOptions } from '@tiptapify/extensions/slash-commands'
35
+ import { SlashCommandsConfig, SlashCommandId } from '@tiptapify/types/slashCommandsTypes'
36
36
  import { toolbarSections } from "@tiptapify/types/toolbarTypes";
37
37
 
38
38
  // load all languages with "all" or common languages with "common"
@@ -51,7 +51,7 @@ const lowlight = createLowlight(common)
51
51
  // register language example
52
52
  // lowlight.register('ts', ts)
53
53
 
54
- export function editorExtensions (placeholder: string, slashCommands: boolean, customExtensions: toolbarSections) {
54
+ export function editorExtensions (placeholder: string, slashCommands: SlashCommandsConfig, customExtensions: toolbarSections) {
55
55
  const extensions = [
56
56
  TextStyleKit,
57
57
  Document,
@@ -78,7 +78,12 @@ export function editorExtensions (placeholder: string, slashCommands: boolean, c
78
78
  openOnClick: false,
79
79
  defaultProtocol: 'https'
80
80
  }),
81
- Image,
81
+ Image.configure({
82
+ resize: {
83
+ enabled: true,
84
+ alwaysPreserveAspectRatio: true,
85
+ },
86
+ }),
82
87
  Youtube.configure({
83
88
  controls: true,
84
89
  nocookie: true,
@@ -110,8 +115,12 @@ export function editorExtensions (placeholder: string, slashCommands: boolean, c
110
115
  BulletListSquare
111
116
  ]
112
117
 
113
- if (slashCommands) {
114
- extensions.push(SlashCommands.configure({ suggestion }))
118
+ if (slashCommands !== false) {
119
+ const config: SlashCommandsExtensionOptions = {}
120
+ if (Array.isArray(slashCommands)) {
121
+ config.suggestion = { allowedCommands: slashCommands }
122
+ }
123
+ extensions.push(SlashCommands.configure(config))
115
124
  }
116
125
 
117
126
  if (customExtensions.length) {
@@ -1,12 +1,13 @@
1
1
  import { Editor, useEditor } from "@tiptap/vue-3";
2
2
  import { editorExtensions } from "@tiptapify/components/editorExtensions";
3
3
  import { toolbarSections } from "@tiptapify/types/toolbarTypes";
4
+ import { SlashCommandsConfig } from "@tiptapify/types/slashCommandsTypes";
4
5
  import { ShallowRef } from "vue";
5
6
 
6
7
  export function getTiptapEditor (
7
8
  content: any,
8
9
  placeholder: string,
9
- slashCommands: boolean = true,
10
+ slashCommands: SlashCommandsConfig = true,
10
11
  customExtensions: toolbarSections,
11
12
  onUpdate: Function = () => {}
12
13
  ): ShallowRef<Editor | undefined> {
@@ -0,0 +1,32 @@
1
+ import { reactive } from 'vue'
2
+
3
+ type PickerCloseEvent = {
4
+ type: 'emoji' | 'charmap'
5
+ }
6
+
7
+ type PickerEvents = {
8
+ close: PickerCloseEvent
9
+ }
10
+
11
+ type EventCallback<T> = (data: T) => void
12
+
13
+ class PickerEventBusClass {
14
+ private listeners: Map<keyof PickerEvents, Set<EventCallback<any>>> = new Map()
15
+
16
+ on<K extends keyof PickerEvents>(event: K, callback: EventCallback<PickerEvents[K]>) {
17
+ if (!this.listeners.has(event)) {
18
+ this.listeners.set(event, new Set())
19
+ }
20
+ this.listeners.get(event)!.add(callback)
21
+ }
22
+
23
+ off<K extends keyof PickerEvents>(event: K, callback: EventCallback<PickerEvents[K]>) {
24
+ this.listeners.get(event)?.delete(callback)
25
+ }
26
+
27
+ emit<K extends keyof PickerEvents>(event: K, data: PickerEvents[K]) {
28
+ this.listeners.get(event)?.forEach(callback => callback(data))
29
+ }
30
+ }
31
+
32
+ export const PickerEventBus = reactive(new PickerEventBusClass())
@@ -1,72 +1,18 @@
1
1
  <script lang="ts" setup>
2
- import { Editor } from '@tiptap/vue-3'
3
- import tiptapifyCharMap from "@tiptapify/extensions/charmap"
4
- import { computed, inject, Ref, ref, watch } from 'vue'
5
2
  import * as mdi from '@mdi/js'
3
+ import { inject, Ref } from 'vue'
4
+ import CharmapPicker from './Picker.vue';
6
5
 
7
6
  defineProps({
8
7
  variantBtn: { type: String, default: 'flat' },
9
8
  })
10
9
 
11
- const editor = inject('tiptapifyEditor') as Ref<Editor>
10
+ const editor = inject('tiptapifyEditor') as Ref<any>
12
11
  const { t } = inject('tiptapifyI18n') as any
13
-
14
- const filter = ref(null)
15
- const tab = ref('punctuation')
16
- const sorted = tiptapifyCharMap.sort((previous, current) => {
17
- if (previous.order < current.order) {
18
- return -1
19
- }
20
- if (previous.order > current.order) {
21
- return 1
22
- }
23
-
24
- return 0
25
- })
26
-
27
- if (!tab.value && sorted.length > 0) {
28
- tab.value = sorted[0].name
29
- }
30
- const chars = computed(() => sorted.map(item => { return { group: item.name, emojis: item.items } }))
31
- const charMapRef = ref(JSON.parse(JSON.stringify(chars.value)))
32
-
33
- const handleCharacterClick = (charItem: any) => {
34
- editor.value.chain().focus().insertContent(charItem.char).run()
35
- }
36
-
37
- const filterChars = (filterValue: string) => {
38
- resetFilter()
39
- if (!filterValue) {
40
- return
41
- }
42
-
43
- const filtered: any[] = []
44
-
45
- const groupItems = charMapRef.value.find((item: any) => item.group === tab.value)
46
- if (groupItems?.emojis) {
47
- groupItems.emojis.forEach((item: any) => {
48
- if (item.name.toLowerCase().match(filterValue)) {
49
- filtered.push(item)
50
- }
51
- })
52
-
53
- groupItems.emojis = filtered
54
- }
55
- }
56
-
57
- const resetFilter = () => {
58
- charMapRef.value = JSON.parse(JSON.stringify(chars.value))
59
- }
60
-
61
- watch(() => tab.value, () => {
62
- resetFilter()
63
- filter.value = null
64
- })
65
-
66
12
  </script>
67
13
 
68
14
  <template>
69
- <VBtn :id="`tiptapify-charmap-button-${editor.instanceId}`" :variant="variantBtn" size="32" @click="console.log('click charmap')">
15
+ <VBtn :id="`tiptapify-charmap-button-${editor.instanceId}`" :variant="variantBtn" size="32">
70
16
  <VTooltip activator="parent">
71
17
  {{ t('media.charmap.title') }}
72
18
  </VTooltip>
@@ -75,91 +21,7 @@ watch(() => tab.value, () => {
75
21
 
76
22
  <VMenu :activator="`#tiptapify-charmap-button-${editor.instanceId}`" :close-on-content-click="false">
77
23
  <VSheet class="pa-2" max-width="580">
78
- <div class="d-flex flex-row">
79
- <VTabs v-model="tab" mandatory direction="vertical" color="primary" density="compact">
80
- <VTab
81
- v-for="item of charMapRef"
82
- :text="item.group"
83
- :value="item.group"
84
- :key="item.group"
85
- density="compact"
86
- rounded
87
- size="small"
88
- >
89
- {{ t(`media.charmap.categories.${item.group}`) }}
90
- </VTab>
91
- </VTabs>
92
-
93
- <div class="d-flex flex-column">
94
- <VTextField
95
- v-model="filter"
96
- :label="t('media.charmap.search')"
97
- density="compact"
98
- variant="solo"
99
- :prepend-inner-icon="`mdiSvg:${mdi.mdiMagnify}`"
100
- class="mb-2 tiptapify-charmap-search"
101
- hide-details
102
- clearable
103
- @update:model-value="filterChars($event)"
104
- @click:clear="resetFilter"
105
- />
106
- <div class="tiptapify-charmap-container">
107
- <VWindow v-model="tab" direction="vertical">
108
- <VWindowItem v-for="item of charMapRef" :value="item.group" :transition="false" :reverse-transition="false">
109
- <div
110
- v-for="charItem in item.emojis"
111
- class="tiptapify-charmap-container-item"
112
- @click="handleCharacterClick(charItem)"
113
- :title="charItem.name"
114
- >
115
- <div class="tiptapify-charmap-container-item__overlay">
116
- <span>
117
- {{ charItem.char }}
118
- </span>
119
- </div>
120
- </div>
121
- </VWindowItem>
122
- </VWindow>
123
- </div>
124
- </div>
125
- </div>
24
+ <CharmapPicker :editor="editor" :t="t" />
126
25
  </VSheet>
127
26
  </VMenu>
128
27
  </template>
129
-
130
- <style scoped lang="scss">
131
- .tiptapify-charmap-container {
132
- width: 360px;
133
- height: 550px;
134
- overflow-y: auto;
135
-
136
- border: 1px solid rgba(var(--v-theme-on-background), calc(var(--v-border-opacity)));
137
- border-radius: 8px;
138
- box-shadow: 0 0 5px rgba(var(--v-theme-on-background), calc(var(--v-border-opacity))) inset;
139
- }
140
-
141
- .tiptapify-charmap-container-item__overlay {
142
- display: flex;
143
- justify-content: center;
144
- align-items: center;
145
- width: 32px;
146
- height: 32px;
147
- border-radius: 5px;
148
- z-index: 1;
149
- }
150
-
151
- .tiptapify-charmap-container-item {
152
- position: relative;
153
- display: inline-flex;
154
- justify-content: center;
155
- align-items: center;
156
- cursor: pointer;
157
- width: 32px;
158
- height: 32px;
159
- transition: background-color 0.2s ease-in-out;
160
- }
161
-
162
- .tiptapify-charmap-container-item:hover .tiptapify-emoji-container-item__overlay {
163
- background-color: rgba(var(--v-theme-on-background), calc(var(--v-hover-opacity) * var(--v-theme-overlay-multiplier)));
164
- }
165
- </style>