tiptapify 0.3.2 → 0.3.4
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.
- package/README.md +1 -0
- package/dist/tiptapify.css +1 -1
- package/dist/tiptapify.mjs +54477 -48112
- package/dist/tiptapify.umd.js +68 -68
- package/index.d.ts +96 -0
- package/package.json +2 -1
- package/src/components/Footer.vue +12 -2
- package/src/components/ImageBubbleMenu.vue +139 -0
- package/src/components/MenuBubble.vue +60 -7
- package/src/components/SkinScope.test.ts +74 -0
- package/src/components/SkinScope.vue +18 -0
- package/src/components/Tiptapify.skin.test.ts +186 -0
- package/src/components/Tiptapify.vue +285 -74
- package/src/components/Toolbar/Index.test.ts +96 -0
- package/src/components/Toolbar/Index.vue +199 -14
- package/src/components/Toolbar/Items.test.ts +82 -2
- package/src/components/Toolbar/Items.vue +104 -19
- package/src/components/Toolbar/alignment.ts +5 -0
- package/src/components/UI/TiptapifyDialog.test.ts +132 -0
- package/src/components/UI/TiptapifyDialog.vue +128 -12
- package/src/components/editorExtensions.test.ts +10 -0
- package/src/components/editorExtensions.ts +34 -5
- package/src/components/index.ts +3 -2
- package/src/composables/useTiptapifyConfig.test.ts +25 -2
- package/src/composables/useTiptapifyConfig.ts +21 -2
- package/src/constants/defaults.ts +2 -1
- package/src/extensions/components/actions/redo/Button.vue +1 -1
- package/src/extensions/components/actions/undo/Button.vue +1 -1
- package/src/extensions/components/alignment/menu/Button.vue +72 -0
- package/src/extensions/components/list/bullet/Button.vue +35 -16
- package/src/extensions/components/list/numbered/Button.vue +19 -0
- package/src/extensions/components/list/styles/ListStyleButton.vue +156 -0
- package/src/extensions/components/list/styles/index.ts +76 -0
- package/src/extensions/components/list/styles/listStyle.test.ts +44 -0
- package/src/extensions/components/media/image/ImageDialog.vue +1 -1
- package/src/extensions/components/media/image/caption.test.ts +122 -0
- package/src/extensions/components/media/image/caption.ts +268 -0
- package/src/extensions/components/media/image/slider.test.ts +106 -0
- package/src/extensions/components/media/image/slider.ts +87 -0
- package/src/extensions/components/style/heading/Button.vue +45 -30
- package/src/i18n/locales/ar.json +22 -0
- package/src/i18n/locales/bg.json +22 -0
- package/src/i18n/locales/ch.json +22 -0
- package/src/i18n/locales/cz.json +22 -0
- package/src/i18n/locales/de.json +22 -0
- package/src/i18n/locales/el.json +22 -0
- package/src/i18n/locales/en.json +23 -1
- package/src/i18n/locales/es.json +22 -0
- package/src/i18n/locales/et.json +22 -0
- package/src/i18n/locales/fi.json +22 -0
- package/src/i18n/locales/fr.json +22 -0
- package/src/i18n/locales/he.json +22 -0
- package/src/i18n/locales/hi.json +22 -0
- package/src/i18n/locales/hr.json +22 -0
- package/src/i18n/locales/hu.json +22 -0
- package/src/i18n/locales/id.json +22 -0
- package/src/i18n/locales/it.json +22 -0
- package/src/i18n/locales/ja.json +22 -0
- package/src/i18n/locales/ko.json +22 -0
- package/src/i18n/locales/la.json +22 -0
- package/src/i18n/locales/lt.json +22 -0
- package/src/i18n/locales/nl.json +22 -0
- package/src/i18n/locales/no.json +22 -0
- package/src/i18n/locales/pl.json +22 -0
- package/src/i18n/locales/pt-BR.json +22 -0
- package/src/i18n/locales/pt.json +22 -0
- package/src/i18n/locales/ro.json +22 -0
- package/src/i18n/locales/ru.json +23 -1
- package/src/i18n/locales/se.json +22 -0
- package/src/i18n/locales/sk.json +22 -0
- package/src/i18n/locales/sl.json +22 -0
- package/src/i18n/locales/sr.json +22 -0
- package/src/i18n/locales/th.json +22 -0
- package/src/i18n/locales/tr.json +22 -0
- package/src/i18n/locales/uk.json +23 -1
- package/src/i18n/locales/vi.json +22 -0
- package/src/index.test.ts +88 -0
- package/src/index.ts +15 -0
- package/src/skins/__fixtures__/skin-baseline.json +805 -0
- package/src/skins/builtins.ts +81 -0
- package/src/skins/menu.css +87 -0
- package/src/skins/menu.ts +10 -0
- package/src/skins/registry.test.ts +180 -0
- package/src/skins/registry.ts +132 -0
- package/src/skins/tokens.reachability.test.ts +43 -0
- package/src/skins/tokens.ts +92 -0
- package/src/types/editor.ts +99 -0
|
@@ -3,19 +3,25 @@
|
|
|
3
3
|
import * as mdi from '@mdi/js'
|
|
4
4
|
import { Editor } from '@tiptap/vue-3'
|
|
5
5
|
import BtnIcon from '@tiptapify/components/UI/BtnIcon.vue'
|
|
6
|
+
import SkinScope from '@tiptapify/components/SkinScope.vue'
|
|
6
7
|
import Tooltip from '@tiptapify/components/UI/Tooltip.vue'
|
|
7
8
|
import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
|
|
9
|
+
import { menuContentClass } from '@tiptapify/skins/menu'
|
|
8
10
|
import { computed, inject, ref, Ref, watch } from 'vue'
|
|
9
11
|
|
|
10
12
|
import { ComposerTranslation } from 'vue-i18n'
|
|
13
|
+
import ListStyleButton from '@tiptapify/extensions/components/list/styles/ListStyleButton.vue'
|
|
14
|
+
import { bulletListStyles } from '@tiptapify/extensions/components/list/styles'
|
|
11
15
|
|
|
12
16
|
const props = defineProps({
|
|
17
|
+
// The marker styles dropdown (`bulletList:styles`) instead of the separate list types below
|
|
18
|
+
withStyles: { type: Boolean, default: false },
|
|
13
19
|
withDisc: { type: Boolean, default: true },
|
|
14
20
|
withCircle: { type: Boolean, default: true },
|
|
15
21
|
withSquare: { type: Boolean, default: true }
|
|
16
22
|
})
|
|
17
23
|
|
|
18
|
-
const { variantBtn } = useTiptapifyConfig()
|
|
24
|
+
const { variantBtn, shape } = useTiptapifyConfig()
|
|
19
25
|
|
|
20
26
|
const editor = inject('tiptapifyEditor') as Ref<Editor>
|
|
21
27
|
|
|
@@ -83,7 +89,18 @@ watch(() => bulletLists.value, () => {
|
|
|
83
89
|
</script>
|
|
84
90
|
|
|
85
91
|
<template>
|
|
86
|
-
<
|
|
92
|
+
<ListStyleButton
|
|
93
|
+
v-if="withStyles"
|
|
94
|
+
list-type="bulletList"
|
|
95
|
+
:icon="mdi.mdiFormatListBulleted"
|
|
96
|
+
:label="t('lists.bullet')"
|
|
97
|
+
hotkey="Mod-Shift-8"
|
|
98
|
+
:presets="bulletListStyles"
|
|
99
|
+
:disabled="!editor.can().chain().focus().toggleBulletList().run()"
|
|
100
|
+
@toggle="editor.commands.toggleBulletList()"
|
|
101
|
+
/>
|
|
102
|
+
|
|
103
|
+
<VMenu v-else-if="isMenu" :content-class="menuContentClass(shape.menu === 'soft')">
|
|
87
104
|
<template #activator="{ props: menuProps }">
|
|
88
105
|
<VBtn
|
|
89
106
|
:color="buttonActive ? 'primary' : ''"
|
|
@@ -97,20 +114,22 @@ watch(() => bulletLists.value, () => {
|
|
|
97
114
|
</VBtn>
|
|
98
115
|
</template>
|
|
99
116
|
|
|
100
|
-
<
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
<SkinScope>
|
|
118
|
+
<VList>
|
|
119
|
+
<VListItem
|
|
120
|
+
v-for="(bulletList, key) in menuItems"
|
|
121
|
+
:key="key"
|
|
122
|
+
link
|
|
123
|
+
:active="editor.isActive(bulletList.name)"
|
|
124
|
+
@click="toggleList(bulletList.name)"
|
|
125
|
+
>
|
|
126
|
+
<Tooltip :label="t('lists.bullet')" />
|
|
127
|
+
<VListItemTitle class="d-flex justify-center align-center">
|
|
128
|
+
<BtnIcon :icon="bulletList.icon" />
|
|
129
|
+
</VListItemTitle>
|
|
130
|
+
</VListItem>
|
|
131
|
+
</VList>
|
|
132
|
+
</SkinScope>
|
|
114
133
|
</VMenu>
|
|
115
134
|
|
|
116
135
|
<VBtn
|
|
@@ -8,6 +8,13 @@ import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
|
|
|
8
8
|
import { computed, inject, Ref } from 'vue'
|
|
9
9
|
|
|
10
10
|
import { ComposerTranslation } from 'vue-i18n'
|
|
11
|
+
import ListStyleButton from '@tiptapify/extensions/components/list/styles/ListStyleButton.vue'
|
|
12
|
+
import { orderedListStyles } from '@tiptapify/extensions/components/list/styles'
|
|
13
|
+
|
|
14
|
+
defineProps({
|
|
15
|
+
// The numbering styles dropdown (`orderedList:styles`)
|
|
16
|
+
withStyles: { type: Boolean, default: false },
|
|
17
|
+
})
|
|
11
18
|
|
|
12
19
|
const { variantBtn } = useTiptapifyConfig()
|
|
13
20
|
|
|
@@ -25,7 +32,19 @@ const buttonDisabled = computed(() => {
|
|
|
25
32
|
</script>
|
|
26
33
|
|
|
27
34
|
<template>
|
|
35
|
+
<ListStyleButton
|
|
36
|
+
v-if="withStyles"
|
|
37
|
+
list-type="orderedList"
|
|
38
|
+
:icon="mdi.mdiFormatListNumbered"
|
|
39
|
+
:label="t('lists.numbered')"
|
|
40
|
+
hotkey="Mod-Shift-7"
|
|
41
|
+
:presets="orderedListStyles"
|
|
42
|
+
:disabled="buttonDisabled"
|
|
43
|
+
@toggle="editor.commands.toggleOrderedList()"
|
|
44
|
+
/>
|
|
45
|
+
|
|
28
46
|
<VBtn
|
|
47
|
+
v-else
|
|
29
48
|
:color="editor.isActive('orderedList') ? 'primary' : ''"
|
|
30
49
|
:disabled="buttonDisabled"
|
|
31
50
|
:variant="variantBtn"
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
|
|
3
|
+
import * as mdi from '@mdi/js'
|
|
4
|
+
import { Editor } from '@tiptap/vue-3'
|
|
5
|
+
import BtnIcon from '@tiptapify/components/UI/BtnIcon.vue'
|
|
6
|
+
import SkinScope from '@tiptapify/components/SkinScope.vue'
|
|
7
|
+
import Tooltip from '@tiptapify/components/UI/Tooltip.vue'
|
|
8
|
+
import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
|
|
9
|
+
import { menuContentClass } from '@tiptapify/skins/menu'
|
|
10
|
+
import { ListStylePreset, ListType } from '@tiptapify/extensions/components/list/styles'
|
|
11
|
+
import { inject, PropType, Ref } from 'vue'
|
|
12
|
+
import { ComposerTranslation } from 'vue-i18n'
|
|
13
|
+
|
|
14
|
+
// A list button with a dropdown of marker styles next to it, like TinyMCE's advlist
|
|
15
|
+
const props = defineProps({
|
|
16
|
+
listType: { type: String as PropType<ListType>, required: true },
|
|
17
|
+
icon: { type: String, required: true },
|
|
18
|
+
label: { type: String, required: true },
|
|
19
|
+
hotkey: { type: String, default: '' },
|
|
20
|
+
presets: { type: Array as PropType<ListStylePreset[]>, required: true },
|
|
21
|
+
disabled: { type: Boolean, default: false },
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const emit = defineEmits(['toggle'])
|
|
25
|
+
|
|
26
|
+
const { variantBtn, shape } = useTiptapifyConfig()
|
|
27
|
+
|
|
28
|
+
const editor = inject('tiptapifyEditor') as Ref<Editor>
|
|
29
|
+
|
|
30
|
+
const { t } = inject('tiptapifyI18n') as { t: ComposerTranslation }
|
|
31
|
+
|
|
32
|
+
function isPresetActive(preset: ListStylePreset): boolean {
|
|
33
|
+
return editor.value.isActive(props.listType) && (editor.value.getAttributes(props.listType).listStyleType ?? null) === preset.value
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function presetLabel(preset: ListStylePreset): string {
|
|
37
|
+
return t(`lists.styles.${(preset.value ?? 'default').replace('-', '_')}`)
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<div class="tiptapify-list-style-button">
|
|
43
|
+
<VBtn
|
|
44
|
+
:color="editor.isActive(listType) ? 'primary' : ''"
|
|
45
|
+
:disabled="disabled"
|
|
46
|
+
:variant="variantBtn"
|
|
47
|
+
size="32"
|
|
48
|
+
@click="emit('toggle')"
|
|
49
|
+
>
|
|
50
|
+
<Tooltip :label="label" :hotkey="hotkey" />
|
|
51
|
+
<BtnIcon :icon="`mdiSvg:${icon}`" />
|
|
52
|
+
</VBtn>
|
|
53
|
+
|
|
54
|
+
<VMenu :content-class="menuContentClass(shape.menu === 'soft')">
|
|
55
|
+
<template #activator="{ props: menuProps }">
|
|
56
|
+
<VBtn
|
|
57
|
+
class="tiptapify-list-style-button__chevron"
|
|
58
|
+
:disabled="disabled"
|
|
59
|
+
:variant="variantBtn"
|
|
60
|
+
height="32"
|
|
61
|
+
width="16"
|
|
62
|
+
v-bind="menuProps"
|
|
63
|
+
>
|
|
64
|
+
<BtnIcon :icon="`mdiSvg:${mdi.mdiChevronDown}`" />
|
|
65
|
+
</VBtn>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<SkinScope>
|
|
69
|
+
<div class="tiptapify-list-style-grid">
|
|
70
|
+
<button
|
|
71
|
+
v-for="preset in presets"
|
|
72
|
+
:key="preset.value ?? 'default'"
|
|
73
|
+
type="button"
|
|
74
|
+
class="tiptapify-list-style-grid__item"
|
|
75
|
+
:class="{ 'tiptapify-list-style-grid__item--active': isPresetActive(preset) }"
|
|
76
|
+
:title="presetLabel(preset)"
|
|
77
|
+
:aria-label="presetLabel(preset)"
|
|
78
|
+
@click="editor.chain().focus().setListStyle(listType, preset.value).run()"
|
|
79
|
+
>
|
|
80
|
+
<span v-for="(marker, index) in preset.markers" :key="index" class="tiptapify-list-style-grid__line">
|
|
81
|
+
<span class="tiptapify-list-style-grid__marker">{{ marker }}</span>
|
|
82
|
+
<span class="tiptapify-list-style-grid__bar" />
|
|
83
|
+
</span>
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
</SkinScope>
|
|
87
|
+
</VMenu>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<style lang="scss" scoped>
|
|
92
|
+
.tiptapify-list-style-button {
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
|
|
96
|
+
:deep(.tiptapify-list-style-button__chevron) {
|
|
97
|
+
min-width: 16px !important;
|
|
98
|
+
padding: 0 !important;
|
|
99
|
+
|
|
100
|
+
.v-icon {
|
|
101
|
+
width: 16px;
|
|
102
|
+
height: 16px;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
</style>
|
|
107
|
+
|
|
108
|
+
<style lang="scss">
|
|
109
|
+
/* Teleported with the menu, so not scoped. The pastel skin of the grid lives in src/skins/menu.css. */
|
|
110
|
+
.tiptapify-list-style-grid {
|
|
111
|
+
display: grid;
|
|
112
|
+
grid-template-columns: repeat(3, 64px);
|
|
113
|
+
gap: 6px;
|
|
114
|
+
padding: 4px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.tiptapify-list-style-grid__item {
|
|
118
|
+
display: flex;
|
|
119
|
+
flex-direction: column;
|
|
120
|
+
gap: 4px;
|
|
121
|
+
padding: 8px 6px;
|
|
122
|
+
border: 1px solid transparent;
|
|
123
|
+
border-radius: 6px;
|
|
124
|
+
color: rgba(var(--v-theme-on-surface), var(--v-high-emphasis-opacity));
|
|
125
|
+
cursor: pointer;
|
|
126
|
+
|
|
127
|
+
&:hover {
|
|
128
|
+
background: rgba(var(--v-theme-on-surface), 0.05);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
&--active {
|
|
132
|
+
border-color: rgb(var(--v-theme-primary));
|
|
133
|
+
background: rgba(var(--v-theme-primary), 0.06);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.tiptapify-list-style-grid__line {
|
|
138
|
+
display: flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
gap: 4px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.tiptapify-list-style-grid__marker {
|
|
144
|
+
min-width: 16px;
|
|
145
|
+
font-size: 10px;
|
|
146
|
+
line-height: 10px;
|
|
147
|
+
text-align: right;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.tiptapify-list-style-grid__bar {
|
|
151
|
+
flex: 1;
|
|
152
|
+
height: 4px;
|
|
153
|
+
border-radius: 2px;
|
|
154
|
+
background: rgba(var(--v-theme-on-surface), 0.18);
|
|
155
|
+
}
|
|
156
|
+
</style>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core'
|
|
2
|
+
|
|
3
|
+
export type ListType = 'bulletList' | 'orderedList'
|
|
4
|
+
|
|
5
|
+
export interface ListStylePreset {
|
|
6
|
+
// CSS `list-style-type`; null is the browser default (disc / decimal)
|
|
7
|
+
value: string | null
|
|
8
|
+
markers: [string, string, string]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const bulletListStyles: ListStylePreset[] = [
|
|
12
|
+
{ value: null, markers: ['●', '●', '●'] },
|
|
13
|
+
{ value: 'circle', markers: ['○', '○', '○'] },
|
|
14
|
+
{ value: 'square', markers: ['■', '■', '■'] },
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
export const orderedListStyles: ListStylePreset[] = [
|
|
18
|
+
{ value: null, markers: ['1.', '2.', '3.'] },
|
|
19
|
+
{ value: 'lower-alpha', markers: ['a.', 'b.', 'c.'] },
|
|
20
|
+
{ value: 'lower-greek', markers: ['α.', 'β.', 'γ.'] },
|
|
21
|
+
{ value: 'lower-roman', markers: ['i.', 'ii.', 'iii.'] },
|
|
22
|
+
{ value: 'upper-alpha', markers: ['A.', 'B.', 'C.'] },
|
|
23
|
+
{ value: 'upper-roman', markers: ['I.', 'II.', 'III.'] },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
declare module '@tiptap/core' {
|
|
27
|
+
interface Commands<ReturnType> {
|
|
28
|
+
listStyle: {
|
|
29
|
+
// Turns the selection into a list of this type, if it is not one yet, with this marker style
|
|
30
|
+
setListStyle: (listType: ListType, listStyleType: string | null) => ReturnType
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The marker style of bullet and numbered lists, saved as inline `list-style-type` the way TinyMCE
|
|
37
|
+
* did, so lists styled there keep their style and every site renders it without extra CSS.
|
|
38
|
+
*/
|
|
39
|
+
export const ListStyle = Extension.create({
|
|
40
|
+
name: 'listStyle',
|
|
41
|
+
|
|
42
|
+
addGlobalAttributes() {
|
|
43
|
+
return [
|
|
44
|
+
{
|
|
45
|
+
types: ['bulletList', 'orderedList'],
|
|
46
|
+
attributes: {
|
|
47
|
+
listStyleType: {
|
|
48
|
+
default: null,
|
|
49
|
+
parseHTML: (element: HTMLElement) => element.style.listStyleType || null,
|
|
50
|
+
renderHTML: (attributes: Record<string, unknown>) => (
|
|
51
|
+
attributes.listStyleType ? { style: `list-style-type: ${attributes.listStyleType}` } : {}
|
|
52
|
+
),
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
addCommands() {
|
|
60
|
+
return {
|
|
61
|
+
setListStyle: (listType: ListType, listStyleType: string | null) => ({ editor, chain }) => {
|
|
62
|
+
const command = chain().focus()
|
|
63
|
+
|
|
64
|
+
if (!editor.isActive(listType)) {
|
|
65
|
+
if (listType === 'bulletList') {
|
|
66
|
+
command.toggleBulletList()
|
|
67
|
+
} else {
|
|
68
|
+
command.toggleOrderedList()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return command.updateAttributes(listType, { listStyleType }).run()
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { Editor } from '@tiptap/vue-3'
|
|
3
|
+
import { editorExtensions } from '@tiptapify/components/editorExtensions'
|
|
4
|
+
|
|
5
|
+
let editor: Editor | null = null
|
|
6
|
+
|
|
7
|
+
function createEditor(content: string) {
|
|
8
|
+
editor = new Editor({ element: document.createElement('div'), content, extensions: editorExtensions('', false, []) })
|
|
9
|
+
|
|
10
|
+
return editor
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('list styles', () => {
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
editor?.destroy()
|
|
16
|
+
editor = null
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('turns a paragraph into a numbered list with the chosen numbering', () => {
|
|
20
|
+
createEditor('<p>one</p>')
|
|
21
|
+
editor!.commands.setTextSelection(2)
|
|
22
|
+
|
|
23
|
+
editor!.commands.setListStyle('orderedList', 'lower-roman')
|
|
24
|
+
|
|
25
|
+
expect(editor!.getHTML()).toBe('<ol style="list-style-type: lower-roman;"><li><p>one</p></li></ol>')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('changes the marker of an existing bullet list and resets it to the default', () => {
|
|
29
|
+
createEditor('<ul><li><p>one</p></li></ul>')
|
|
30
|
+
editor!.commands.setTextSelection(3)
|
|
31
|
+
|
|
32
|
+
editor!.commands.setListStyle('bulletList', 'square')
|
|
33
|
+
expect(editor!.getHTML()).toContain('<ul style="list-style-type: square;">')
|
|
34
|
+
|
|
35
|
+
editor!.commands.setListStyle('bulletList', null)
|
|
36
|
+
expect(editor!.getHTML()).toBe('<ul><li><p>one</p></li></ul>')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('reads the list style TinyMCE saved', () => {
|
|
40
|
+
createEditor('<ol style="list-style-type: upper-alpha;"><li><p>one</p></li></ol>')
|
|
41
|
+
|
|
42
|
+
expect(editor!.getJSON().content![0].attrs).toMatchObject({ listStyleType: 'upper-alpha' })
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { Editor } from '@tiptap/vue-3'
|
|
3
|
+
import { Document } from '@tiptap/extension-document'
|
|
4
|
+
import { Paragraph } from '@tiptap/extension-paragraph'
|
|
5
|
+
import { Text } from '@tiptap/extension-text'
|
|
6
|
+
import { TiptapifyImage } from '@tiptapify/extensions/components/media/image'
|
|
7
|
+
import { ImageWithCaption } from '@tiptapify/extensions/components/media/image/caption'
|
|
8
|
+
|
|
9
|
+
let editor: Editor | null = null
|
|
10
|
+
|
|
11
|
+
function createEditor(content: string) {
|
|
12
|
+
editor = new Editor({
|
|
13
|
+
element: document.createElement('div'),
|
|
14
|
+
content,
|
|
15
|
+
extensions: [
|
|
16
|
+
Document,
|
|
17
|
+
Paragraph,
|
|
18
|
+
Text,
|
|
19
|
+
TiptapifyImage,
|
|
20
|
+
ImageWithCaption.configure({
|
|
21
|
+
resize: { enabled: true, alwaysPreserveAspectRatio: true },
|
|
22
|
+
captionPlaceholder: 'Add a caption',
|
|
23
|
+
}),
|
|
24
|
+
],
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
return editor
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function caption(): HTMLInputElement {
|
|
31
|
+
return editor!.view.dom.querySelector('.tiptapify-image-caption') as HTMLInputElement
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function typeCaption(value: string) {
|
|
35
|
+
caption().value = value
|
|
36
|
+
caption().dispatchEvent(new Event('input'))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('image caption', () => {
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
editor?.destroy()
|
|
42
|
+
editor = null
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('shows the image title as the caption under the image', () => {
|
|
46
|
+
createEditor('<img src="https://example.com/a.png" title="Kyiv at night">')
|
|
47
|
+
|
|
48
|
+
expect(caption().value).toBe('Kyiv at night')
|
|
49
|
+
expect(caption().placeholder).toBe('Add a caption')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('saves the typed caption as the figure caption', () => {
|
|
53
|
+
createEditor('<img src="https://example.com/a.png">')
|
|
54
|
+
|
|
55
|
+
typeCaption('Kyiv at night')
|
|
56
|
+
|
|
57
|
+
expect(editor!.getHTML()).toBe('<figure><img src="https://example.com/a.png"><figcaption>Kyiv at night</figcaption></figure>')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('saves an image without a caption as a figure without figcaption', () => {
|
|
61
|
+
createEditor('<img src="https://example.com/a.png" title="Kyiv at night">')
|
|
62
|
+
|
|
63
|
+
typeCaption(' ')
|
|
64
|
+
|
|
65
|
+
expect(editor!.getHTML()).toBe('<figure><img src="https://example.com/a.png"></figure>')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('loads a figure with its caption and image attributes', () => {
|
|
69
|
+
createEditor('<figure><img src="https://example.com/a.png" alt="Kyiv" width="640"><figcaption> Kyiv at night </figcaption></figure>')
|
|
70
|
+
|
|
71
|
+
expect(caption().value).toBe('Kyiv at night')
|
|
72
|
+
expect(editor!.getJSON().content![0].attrs).toMatchObject({ src: 'https://example.com/a.png', alt: 'Kyiv', width: 640, title: 'Kyiv at night' })
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('keeps consecutive figures as separate images', () => {
|
|
76
|
+
createEditor('<figure><img src="https://example.com/a.png"></figure><figure><img src="https://example.com/b.png"></figure>')
|
|
77
|
+
|
|
78
|
+
expect(editor!.getJSON().content!.map(node => node.type)).toEqual(['image', 'image'])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('updates the caption when the title is changed elsewhere', () => {
|
|
82
|
+
createEditor('<img src="https://example.com/a.png">')
|
|
83
|
+
|
|
84
|
+
editor!.chain().setNodeSelection(0).updateAttributes('image', { title: 'From the dialog' }).run()
|
|
85
|
+
|
|
86
|
+
expect(caption().value).toBe('From the dialog')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('opens the image dialog for this image on double click', () => {
|
|
90
|
+
createEditor('<p>text</p><img src="https://example.com/a.png" alt="Kyiv">')
|
|
91
|
+
const listener = vi.fn()
|
|
92
|
+
window.addEventListener('tiptapify-show-tiptapifyImage', listener)
|
|
93
|
+
|
|
94
|
+
editor!.view.dom.querySelector('[data-resize-wrapper]')!.dispatchEvent(new MouseEvent('dblclick', { bubbles: true }))
|
|
95
|
+
|
|
96
|
+
window.removeEventListener('tiptapify-show-tiptapifyImage', listener)
|
|
97
|
+
expect(listener).toHaveBeenCalledOnce()
|
|
98
|
+
expect(listener.mock.calls[0][0].detail.image).toMatchObject({ src: 'https://example.com/a.png', alt: 'Kyiv' })
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('moves the cursor to the text after the image on Enter', () => {
|
|
102
|
+
createEditor('<img src="https://example.com/a.png"><p>after</p>')
|
|
103
|
+
|
|
104
|
+
caption().dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }))
|
|
105
|
+
|
|
106
|
+
const { $from } = editor!.state.selection
|
|
107
|
+
expect($from.parent.textContent).toBe('after')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('floats the image with the align attribute', () => {
|
|
111
|
+
createEditor('<img src="https://example.com/a.png" style="float: right">')
|
|
112
|
+
const nodeView = editor!.view.nodeDOM(0) as HTMLElement
|
|
113
|
+
|
|
114
|
+
expect(nodeView.dataset.align).toBe('right')
|
|
115
|
+
expect(editor!.getHTML()).toBe('<figure><img src="https://example.com/a.png" style="float: right;"></figure>')
|
|
116
|
+
|
|
117
|
+
editor!.chain().setNodeSelection(0).updateAttributes('image', { align: null }).run()
|
|
118
|
+
|
|
119
|
+
expect(nodeView.dataset.align).toBeUndefined()
|
|
120
|
+
expect(editor!.getHTML()).toBe('<figure><img src="https://example.com/a.png"></figure>')
|
|
121
|
+
})
|
|
122
|
+
})
|