tiptapify 0.0.35 → 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 (59) hide show
  1. package/README.md +12 -6
  2. package/package.json +7 -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/Toolbar/media.ts +5 -0
  7. package/src/components/editorExtensions.ts +15 -6
  8. package/src/components/index.ts +2 -1
  9. package/src/extensions/PickerEventBus.ts +32 -0
  10. package/src/extensions/charmap/arrows.ts +1227 -0
  11. package/src/extensions/charmap/box_drawing.ts +324 -0
  12. package/src/extensions/charmap/currency.ts +157 -0
  13. package/src/extensions/charmap/cyrillic.ts +646 -0
  14. package/src/extensions/charmap/diacritics.ts +107 -0
  15. package/src/extensions/charmap/extended_letters.ts +1311 -0
  16. package/src/extensions/charmap/greek.ts +443 -0
  17. package/src/extensions/charmap/hebrew.ts +177 -0
  18. package/src/extensions/charmap/index.ts +75 -0
  19. package/src/extensions/charmap/math.ts +2949 -0
  20. package/src/extensions/charmap/punctuation.ts +121 -0
  21. package/src/extensions/charmap/symbols.ts +506 -0
  22. package/src/extensions/charmap/typography.ts +499 -0
  23. package/src/extensions/components/media/charmap/Button.vue +27 -0
  24. package/src/extensions/components/media/charmap/Picker.vue +229 -0
  25. package/src/extensions/components/media/emoji/Button.vue +6 -147
  26. package/src/extensions/components/media/emoji/Picker.vue +225 -0
  27. package/src/extensions/components/media/image/ImageDialog.vue +69 -27
  28. package/src/extensions/components/slashCommands/CommandsList.vue +65 -22
  29. package/src/extensions/components/slashCommands/PickerDialog.vue +44 -0
  30. package/src/extensions/components/slashCommands/suggestion.ts +152 -105
  31. package/src/extensions/slash-commands.ts +169 -9
  32. package/src/i18n/locales/ar.json +37 -14
  33. package/src/i18n/locales/ch.json +37 -14
  34. package/src/i18n/locales/cz.json +37 -14
  35. package/src/i18n/locales/de.json +37 -14
  36. package/src/i18n/locales/en.json +34 -12
  37. package/src/i18n/locales/es.json +37 -14
  38. package/src/i18n/locales/fi.json +37 -14
  39. package/src/i18n/locales/fr.json +37 -14
  40. package/src/i18n/locales/hu.json +37 -14
  41. package/src/i18n/locales/it.json +37 -14
  42. package/src/i18n/locales/ja.json +37 -14
  43. package/src/i18n/locales/ko.json +37 -14
  44. package/src/i18n/locales/la.json +37 -14
  45. package/src/i18n/locales/lt.json +37 -14
  46. package/src/i18n/locales/nl.json +37 -14
  47. package/src/i18n/locales/pl.json +37 -14
  48. package/src/i18n/locales/pt.json +37 -14
  49. package/src/i18n/locales/ru.json +37 -14
  50. package/src/i18n/locales/se.json +37 -14
  51. package/src/i18n/locales/th.json +37 -14
  52. package/src/i18n/locales/tr.json +37 -14
  53. package/src/i18n/locales/{ua.json → uk.json} +37 -14
  54. package/src/i18n/locales/vi.json +37 -14
  55. package/src/types/slashCommandsTypes.ts +19 -0
  56. package/src/types/toolbarTypes.ts +1 -1
  57. package/dist/tiptapify.css +0 -1
  58. package/dist/tiptapify.mjs +0 -82239
  59. package/dist/tiptapify.umd.js +0 -202
@@ -18,6 +18,7 @@ const { t } = inject('tiptapifyI18n') as any
18
18
  const generateImageAttrs = () => ({
19
19
  src: '',
20
20
  alt: '',
21
+ title: null,
21
22
  height: null,
22
23
  width: null
23
24
  })
@@ -63,13 +64,17 @@ function updateSizeRatio(dim: string) {
63
64
  }
64
65
 
65
66
  function apply() {
66
- let { src, alt, width, height } = attrs.value
67
+ let { src, alt, title, width, height } = attrs.value
67
68
 
68
- const imageOptions: { src: string, alt: string, width?: number, height?: number} = {
69
+ const imageOptions: { src: string, alt: string, title?: string, width?: number, height?: number} = {
69
70
  src,
70
71
  alt
71
72
  }
72
73
 
74
+ if (title) {
75
+ imageOptions.title = title
76
+ }
77
+
73
78
  if (width) {
74
79
  imageOptions.width = width
75
80
  }
@@ -105,6 +110,7 @@ const showTiptapifyImage = (event: CustomEvent) => {
105
110
 
106
111
  attrs.value.src = event.detail.image?.src
107
112
  attrs.value.alt = event.detail.image?.alt
113
+ attrs.value.title = event.detail.image?.title
108
114
  attrs.value.width = event.detail.image?.width
109
115
  attrs.value.height = event.detail.image?.height
110
116
 
@@ -124,52 +130,68 @@ onUnmounted(() => {
124
130
  <TiptapifyDialog ref="dialog" module="image" :max-width="800">
125
131
  <template #content>
126
132
  <VCardText>
127
- <VRow>
128
- <VCol cols="12">
129
- <VTextField v-model="attrs.src" density="compact" variant="outlined" :label="t('dialog.image.src')" />
130
- </VCol>
131
-
132
- <VCol cols="12" md="5">
133
- <VTextField v-model="attrs.alt" density="compact" variant="outlined" :label="t('dialog.image.alt')" />
134
- </VCol>
135
-
136
- <VCol cols="5" md="3">
133
+ <div class="tiptapify-grid-row">
134
+ <div>
135
+ <VTextField v-model="attrs.src" density="comfortable" :label="t('dialog.image.src')" />
136
+ </div>
137
+ </div>
138
+
139
+ <div class="tiptapify-grid-row">
140
+ <div>
141
+ <VTextField
142
+ v-model="attrs.alt"
143
+ density="comfortable"
144
+ :label="t('dialog.image.alt')" />
145
+ </div>
146
+ </div>
147
+
148
+ <div class="tiptapify-grid-row">
149
+ <div>
150
+ <VTextField
151
+ v-model="attrs.title"
152
+ density="comfortable"
153
+ :label="t('dialog.image.title')" />
154
+ </div>
155
+ </div>
156
+
157
+ <div class="tiptapify-grid-row">
158
+ <div class="tiptapify-image-inputs-container">
137
159
  <VTextField
138
160
  v-model="attrs.width"
139
161
  type="number"
140
- density="compact"
141
- variant="outlined"
162
+ density="comfortable"
142
163
  :precision="0"
143
164
  :min="1"
144
165
  :label="t('dialog.image.width')"
145
166
  @change="setRatio"
146
167
  @update:model-value="updateSizeRatio('width')"
147
168
  />
148
- </VCol>
149
-
150
- <VCol cols="5" md="3">
151
169
  <VTextField
152
170
  v-model="attrs.height"
153
171
  type="number"
154
- density="compact"
155
- variant="outlined"
172
+ density="comfortable"
156
173
  :precision="0"
157
174
  :min="1"
158
175
  :label="t('dialog.image.height')"
159
176
  @change="setRatio"
160
177
  @update:model-value="updateSizeRatio('height')"
161
178
  />
162
- </VCol>
163
-
164
- <VCol cols="2" md="1">
165
- <VBtn size="40" :variant="variantBtn" v-model="keepRatio" @click="keepRatio = !keepRatio">
166
- <VIcon :icon="keepRatio ? `mdiSvg:${mdi.mdiLock}` : `mdiSvg:${mdi.mdiLockOpen}`" />
179
+ </div>
180
+ <div>
181
+ <VBtn
182
+ v-model="keepRatio"
183
+ :color="keepRatio ? 'primary' : 'secondary'"
184
+ size="48"
185
+ :variant="variantBtn"
186
+ @click="keepRatio = !keepRatio"
187
+ >
188
+ <VIcon :icon="keepRatio ? `mdiSvg:${mdi.mdiLock}` : `mdiSvg:${mdi.mdiLockOpenVariant}`" />
167
189
  <VTooltip activator="parent">
168
190
  {{ t('dialog.image.keep_ratio') }}
169
191
  </VTooltip>
170
192
  </VBtn>
171
- </VCol>
172
- </VRow>
193
+ </div>
194
+ </div>
173
195
  </VCardText>
174
196
  </template>
175
197
 
@@ -193,4 +215,24 @@ onUnmounted(() => {
193
215
  </VCardActions>
194
216
  </template>
195
217
  </TiptapifyDialog>
196
- </template>
218
+ </template>
219
+
220
+ <style lang="scss" scoped>
221
+ .tiptapify-grid-row {
222
+ margin-top: 8px;
223
+ gap: 16px;
224
+ display: grid;
225
+ grid-template-columns: 9fr 1fr;
226
+ justify-items: end;
227
+
228
+ :nth-child(1) {
229
+ width: 100%;
230
+ }
231
+
232
+ .tiptapify-image-inputs-container {
233
+ display: grid;
234
+ grid-template-columns: 1fr 1fr;
235
+ gap: 16px;
236
+ }
237
+ }
238
+ </style>
@@ -1,17 +1,12 @@
1
1
  <script setup lang="ts">
2
2
  import { ref, watch } from 'vue'
3
3
  import * as mdi from '@mdi/js'
4
+ import type { SlashCommand } from '@tiptapify/types/slashCommandsTypes'
4
5
 
5
- const props = defineProps({
6
- items: {
7
- type: Array,
8
- required: true,
9
- },
10
- command: {
11
- type: Function,
12
- required: true,
13
- },
14
- })
6
+ const props = defineProps<{
7
+ items: SlashCommand[]
8
+ command: (item: SlashCommand) => void
9
+ }>()
15
10
 
16
11
  const selectedIndex = ref(0)
17
12
 
@@ -19,7 +14,7 @@ watch(() => props.items, () => {
19
14
  selectedIndex.value = 0
20
15
  }, { deep: true })
21
16
 
22
- function onKeyDown({ event }) {
17
+ function onKeyDown({ event }: { event: KeyboardEvent }) {
23
18
  if (event.key === 'ArrowUp') {
24
19
  upHandler()
25
20
  return true
@@ -50,7 +45,7 @@ function enterHandler() {
50
45
  selectItem(selectedIndex.value)
51
46
  }
52
47
 
53
- function selectItem(index) {
48
+ function selectItem(index: number) {
54
49
  const item = props.items[index]
55
50
 
56
51
  if (item) {
@@ -58,6 +53,11 @@ function selectItem(index) {
58
53
  }
59
54
  }
60
55
 
56
+ function getIcon(iconName: string) {
57
+ const iconKey = `mdi${iconName}` as keyof typeof mdi
58
+ return mdi[iconKey] || mdi.mdiImageBrokenVariant
59
+ }
60
+
61
61
  defineExpose({
62
62
  onKeyDown,
63
63
  upHandler,
@@ -70,17 +70,21 @@ defineExpose({
70
70
  <template>
71
71
  <div class="dropdown-menu">
72
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"
73
+ <button
74
+ v-for="(item, index) in items"
75
+ :key="index"
76
+ class="item"
77
+ :class="{ 'is-selected': index === selectedIndex }"
78
+ @click="selectItem(index)"
79
+ @mouseenter="selectedIndex = index"
79
80
  >
80
- <VIcon :icon="`mdiSvg:${mdi[`mdi${item.icon}`]}` || `mdiSvg:${mdi.mdiImageBrokenVariant}`" size="16" />
81
- </VBtn>
81
+ <span class="item-icon">
82
+ <VIcon :icon="`mdiSvg:${getIcon(item.icon)}`" size="18" />
83
+ </span>
84
+ <span class="item-title">{{ item.title }}</span>
85
+ </button>
82
86
  </template>
83
- <div class="item" v-else>No result</div>
87
+ <div v-else class="no-result">No result</div>
84
88
  </div>
85
89
  </template>
86
90
 
@@ -96,5 +100,44 @@ defineExpose({
96
100
  overflow: auto;
97
101
  padding: 0.4rem;
98
102
  position: relative;
103
+ max-height: 300px;
104
+ min-width: 200px;
105
+ }
106
+
107
+ .item {
108
+ align-items: center;
109
+ background: transparent;
110
+ border: none;
111
+ border-radius: 0.4rem;
112
+ cursor: pointer;
113
+ display: flex;
114
+ gap: 0.75rem;
115
+ padding: 0.5rem 0.75rem;
116
+ text-align: left;
117
+ transition: background-color 0.15s ease;
118
+
119
+ &:hover,
120
+ &.is-selected {
121
+ background: var(--gray-1);
122
+ }
123
+ }
124
+
125
+ .item-icon {
126
+ align-items: center;
127
+ display: flex;
128
+ justify-content: center;
129
+ width: 24px;
130
+ }
131
+
132
+ .item-title {
133
+ color: var(--black);
134
+ font-size: 0.875rem;
135
+ font-weight: 500;
136
+ }
137
+
138
+ .no-result {
139
+ color: var(--gray-5);
140
+ font-size: 0.875rem;
141
+ padding: 0.5rem 0.75rem;
99
142
  }
100
- </style>
143
+ </style>
@@ -0,0 +1,44 @@
1
+ <script lang="ts" setup>
2
+ import type { Editor } from '@tiptap/core'
3
+ import EmojiPicker from '@tiptapify/extensions/components/media/emoji/Picker.vue'
4
+ import CharmapPicker from '@tiptapify/extensions/components/media/charmap/Picker.vue'
5
+ import { PickerEventBus } from '@tiptapify/extensions/PickerEventBus'
6
+
7
+ const props = defineProps<{
8
+ editor: Editor
9
+ t: any
10
+ type: 'emoji' | 'charmap'
11
+ }>()
12
+
13
+ const handleClose = () => {
14
+ PickerEventBus.emit('close', { type: props.type })
15
+ }
16
+ </script>
17
+
18
+ <template>
19
+ <div class="tiptapify-picker-dialog" @click.self="handleClose">
20
+ <div class="tiptapify-picker-dialog__content">
21
+ <EmojiPicker v-if="type === 'emoji'" :editor="editor" :t="t" />
22
+ <CharmapPicker v-else-if="type === 'charmap'" :editor="editor" :t="t" />
23
+ </div>
24
+ </div>
25
+ </template>
26
+
27
+ <style scoped>
28
+ .tiptapify-picker-dialog {
29
+ position: fixed;
30
+ inset: 0;
31
+ z-index: 9999;
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: center;
35
+ background-color: rgba(0, 0, 0, 0.5);
36
+ }
37
+
38
+ .tiptapify-picker-dialog__content {
39
+ background: rgb(var(--v-theme-surface));
40
+ border-radius: 8px;
41
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
42
+ overflow: hidden;
43
+ }
44
+ </style>
@@ -1,107 +1,154 @@
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)
1
+ import { SlashCommand, SlashCommandId } from '@tiptapify/types/slashCommandsTypes'
2
+
3
+ export const slashCommandMap: Record<SlashCommandId, SlashCommand> = {
4
+ h1: {
5
+ title: 'H1',
6
+ icon: 'FormatHeader1',
7
+ command: ({ editor, range }) => {
8
+ editor.chain().focus().deleteRange(range).setNode('heading', { level: 1 }).run()
9
+ },
57
10
  },
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
- }
11
+ h2: {
12
+ title: 'H2',
13
+ icon: 'FormatHeader2',
14
+ command: ({ editor, range }) => {
15
+ editor.chain().focus().deleteRange(range).setNode('heading', { level: 2 }).run()
16
+ },
17
+ },
18
+ h3: {
19
+ title: 'H3',
20
+ icon: 'FormatHeader3',
21
+ command: ({ editor, range }) => {
22
+ editor.chain().focus().deleteRange(range).setNode('heading', { level: 3 }).run()
23
+ },
106
24
  },
107
- }
25
+ h4: {
26
+ title: 'H4',
27
+ icon: 'FormatHeader4',
28
+ command: ({ editor, range }) => {
29
+ editor.chain().focus().deleteRange(range).setNode('heading', { level: 4 }).run()
30
+ },
31
+ },
32
+ h5: {
33
+ title: 'H5',
34
+ icon: 'FormatHeader5',
35
+ command: ({ editor, range }) => {
36
+ editor.chain().focus().deleteRange(range).setNode('heading', { level: 5 }).run()
37
+ },
38
+ },
39
+ h6: {
40
+ title: 'H6',
41
+ icon: 'FormatHeader6',
42
+ command: ({ editor, range }) => {
43
+ editor.chain().focus().deleteRange(range).setNode('heading', { level: 6 }).run()
44
+ },
45
+ },
46
+ bulletList: {
47
+ title: 'Bullet List',
48
+ icon: 'FormatListBulleted',
49
+ command: ({ editor, range }) => {
50
+ editor.chain().focus().deleteRange(range).toggleBulletList().run()
51
+ },
52
+ },
53
+ numberedList: {
54
+ title: 'Numbered List',
55
+ icon: 'FormatListNumbered',
56
+ command: ({ editor, range }) => {
57
+ editor.chain().focus().deleteRange(range).toggleOrderedList().run()
58
+ },
59
+ },
60
+ taskList: {
61
+ title: 'Task List',
62
+ icon: 'FormatListCheckbox',
63
+ command: ({ editor, range }) => {
64
+ editor.chain().focus().deleteRange(range).toggleTaskList().run()
65
+ },
66
+ },
67
+ code: {
68
+ title: 'Code',
69
+ icon: 'CodeTags',
70
+ command: ({ editor, range }) => {
71
+ editor.chain().focus().deleteRange(range).toggleCode().run()
72
+ },
73
+ },
74
+ codeBlock: {
75
+ title: 'Code Block',
76
+ icon: 'CodeBracesBox',
77
+ command: ({ editor, range }) => {
78
+ editor.chain().focus().deleteRange(range).toggleCodeBlock().run()
79
+ },
80
+ },
81
+ quote: {
82
+ title: 'Quote',
83
+ icon: 'FormatQuoteClose',
84
+ command: ({ editor, range }) => {
85
+ editor.chain().focus().deleteRange(range).toggleBlockquote().run()
86
+ },
87
+ },
88
+ image: {
89
+ title: 'Image',
90
+ icon: 'Image',
91
+ command: ({ editor, range }) => {
92
+ const url = window.prompt('Enter image URL:')
93
+ if (url) {
94
+ editor.chain().focus().deleteRange(range).setImage({ src: url }).run()
95
+ }
96
+ },
97
+ },
98
+ video: {
99
+ title: 'Video',
100
+ icon: 'Video',
101
+ command: ({ editor, range }) => {
102
+ const url = window.prompt('Enter YouTube video URL:')
103
+ if (url) {
104
+ editor.chain().focus().deleteRange(range).setYoutubeVideo({ src: url }).run()
105
+ }
106
+ },
107
+ },
108
+ table: {
109
+ title: 'Table',
110
+ icon: 'Table',
111
+ command: ({ editor, range }) => {
112
+ editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()
113
+ },
114
+ },
115
+ link: {
116
+ title: 'Link',
117
+ icon: 'Link',
118
+ command: ({ editor, range }) => {
119
+ const previousUrl = editor.getAttributes('link').href
120
+ const url = window.prompt('Enter link URL:', previousUrl)
121
+ if (url === null) return
122
+ if (url === '') {
123
+ editor.chain().focus().deleteRange(range).extendMarkRange('link').unsetLink().run()
124
+ return
125
+ }
126
+ editor.chain().focus().deleteRange(range).extendMarkRange('link').setLink({ href: url }).run()
127
+ },
128
+ },
129
+ divider: {
130
+ title: 'Divider',
131
+ icon: 'Minus',
132
+ command: ({ editor, range }) => {
133
+ editor.chain().focus().deleteRange(range).setHorizontalRule().run()
134
+ },
135
+ },
136
+ emoji: {
137
+ title: 'Emoji',
138
+ icon: 'Emoticon',
139
+ isPicker: true,
140
+ },
141
+ specialChars: {
142
+ title: 'Special Characters',
143
+ icon: 'FormatText',
144
+ isPicker: true,
145
+ },
146
+ }
147
+
148
+ export const defaultSlashCommandIds: SlashCommandId[] = [
149
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
150
+ 'bulletList', 'numberedList', 'taskList',
151
+ 'code', 'codeBlock', 'quote',
152
+ 'image', 'video', 'table', 'link',
153
+ 'divider', 'emoji', 'specialChars'
154
+ ]