tiptapify 0.3.1 → 0.3.3
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/dist/tiptapify.css +1 -1
- package/dist/tiptapify.mjs +54124 -48070
- package/dist/tiptapify.umd.js +59 -59
- package/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/Footer.vue +12 -2
- package/src/components/ImageBubbleMenu.vue +130 -0
- package/src/components/MenuBubble.vue +36 -21
- package/src/components/Tiptapify.vue +189 -72
- package/src/components/Toolbar/Index.test.ts +96 -0
- package/src/components/Toolbar/Index.vue +225 -14
- package/src/components/Toolbar/Items.test.ts +49 -3
- package/src/components/Toolbar/Items.vue +25 -26
- package/src/components/Toolbar/alignment.ts +5 -0
- package/src/components/UI/TiptapifyDialog.vue +78 -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/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 +68 -0
- package/src/extensions/components/list/bullet/Button.vue +16 -1
- 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 +75 -35
- 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/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -57,16 +57,16 @@ function showFooter() {
|
|
|
57
57
|
<VRow>
|
|
58
58
|
<VCol class="d-flex align-center" :class="`justify-${alignment}`">
|
|
59
59
|
<template v-if="limit > 0">
|
|
60
|
-
<VTooltip :open-delay="
|
|
60
|
+
<VTooltip :open-delay="200" location="top">
|
|
61
61
|
<template #default>
|
|
62
62
|
<span v-html="limitText" />
|
|
63
63
|
</template>
|
|
64
64
|
<template #activator="{ props }">
|
|
65
|
+
<span class="tiptapify-footer--limit" v-bind="props" :aria-label="limitText.replace('<br>', ': ')">
|
|
65
66
|
<svg
|
|
66
67
|
height="25"
|
|
67
68
|
width="25"
|
|
68
69
|
viewBox="0 0 20 20"
|
|
69
|
-
v-bind="props"
|
|
70
70
|
:class="{
|
|
71
71
|
'character-count': true,
|
|
72
72
|
'character-count--alert': percentage > 75,
|
|
@@ -86,6 +86,8 @@ function showFooter() {
|
|
|
86
86
|
/>
|
|
87
87
|
<circle r="6" cx="10" cy="10" fill="white" />
|
|
88
88
|
</svg>
|
|
89
|
+
<span class="tiptapify-footer--status-item">{{ Math.min(percentage, 999) }}%</span>
|
|
90
|
+
</span>
|
|
89
91
|
</template>
|
|
90
92
|
</VTooltip>
|
|
91
93
|
|
|
@@ -113,6 +115,14 @@ function showFooter() {
|
|
|
113
115
|
color: #999;
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
/* The limit ring with its percentage; the tooltip spells out the limit */
|
|
119
|
+
.tiptapify-footer--limit {
|
|
120
|
+
display: inline-flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
gap: 4px;
|
|
123
|
+
cursor: help;
|
|
124
|
+
}
|
|
125
|
+
|
|
116
126
|
.tiptapify-footer--divider:not(:last-child) {
|
|
117
127
|
margin: 0 8px;
|
|
118
128
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import * as mdi from '@mdi/js'
|
|
3
|
+
import { Editor } from '@tiptap/vue-3'
|
|
4
|
+
import { BubbleMenu } from '@tiptap/vue-3/menus'
|
|
5
|
+
import { NodeSelection } from '@tiptap/pm/state'
|
|
6
|
+
import BtnIcon from '@tiptapify/components/UI/BtnIcon.vue'
|
|
7
|
+
import Tooltip from '@tiptapify/components/UI/Tooltip.vue'
|
|
8
|
+
import { ImageAlign } from '@tiptapify/extensions/components/media/image/caption'
|
|
9
|
+
import { inject, Ref, ref } from 'vue'
|
|
10
|
+
import { ComposerTranslation } from 'vue-i18n'
|
|
11
|
+
|
|
12
|
+
// The toolbar on a selected image: float toggles (or, for a slide, its place in the slider), download, edit, removal
|
|
13
|
+
const editor = inject('tiptapifyEditor') as Ref<Editor>
|
|
14
|
+
|
|
15
|
+
// The menu follows its text or image when the editor scrolls inside its own box; it is created once the
|
|
16
|
+
// box is there, as the menu reads its options only when it is created
|
|
17
|
+
const scrollTarget = inject<Ref<HTMLElement | null>>('tiptapifyScrollContainer', ref(null))
|
|
18
|
+
|
|
19
|
+
const { t } = inject('tiptapifyI18n') as { t: ComposerTranslation }
|
|
20
|
+
|
|
21
|
+
// Without a float the image stands centered on its own line; a second click on a float removes it
|
|
22
|
+
const alignments: Array<{ value: ImageAlign, icon: string, label: string }> = [
|
|
23
|
+
{ value: 'left', icon: mdi.mdiFormatFloatLeft, label: 'media.image_toolbar.align_left' },
|
|
24
|
+
{ value: 'right', icon: mdi.mdiFormatFloatRight, label: 'media.image_toolbar.align_right' },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
function isImageSelected({ state }: { state: Editor['state'] }): boolean {
|
|
28
|
+
return state.selection instanceof NodeSelection && state.selection.node.type.name === 'image'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function toggleAlign(align: ImageAlign) {
|
|
32
|
+
const current = editor.value.getAttributes('image').align ?? null
|
|
33
|
+
editor.value.chain().focus().updateAttributes('image', { align: current === align ? null : align }).run()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// A slide of a slider (extensions/components/media/image/slider.ts) moves among the slides instead of floating
|
|
37
|
+
function isSlide(): boolean {
|
|
38
|
+
if (!isImageSelected(editor.value)) {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The selection may sit in text while the menu hides, where the node at it is a text node
|
|
43
|
+
const dom = editor.value.view.nodeDOM(editor.value.state.selection.from)
|
|
44
|
+
|
|
45
|
+
return dom instanceof HTMLElement && dom.classList.contains('tiptapify-slide')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function download() {
|
|
49
|
+
const { src } = editor.value.getAttributes('image')
|
|
50
|
+
if (src) {
|
|
51
|
+
window.open(src, '_blank', 'noopener')
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<template>
|
|
58
|
+
<BubbleMenu
|
|
59
|
+
v-if="editor && scrollTarget"
|
|
60
|
+
:editor="editor"
|
|
61
|
+
plugin-key="tiptapifyImageMenu"
|
|
62
|
+
:options="{ placement: 'top', offset: -52, flip: false, scrollTarget }"
|
|
63
|
+
:should-show="isImageSelected"
|
|
64
|
+
>
|
|
65
|
+
<div class="tiptapify-image-menu tiptapify-quiet-buttons">
|
|
66
|
+
<template v-if="isSlide()">
|
|
67
|
+
<VBtn size="32" variant="text" color="" :disabled="!editor.can().moveImage(-1)" @click="editor.chain().focus().moveImage(-1).run()">
|
|
68
|
+
<Tooltip :label="t('media.image_toolbar.move_back')" />
|
|
69
|
+
<BtnIcon :icon="`mdiSvg:${mdi.mdiArrowLeft}`" />
|
|
70
|
+
</VBtn>
|
|
71
|
+
<VBtn size="32" variant="text" color="" :disabled="!editor.can().moveImage(1)" @click="editor.chain().focus().moveImage(1).run()">
|
|
72
|
+
<Tooltip :label="t('media.image_toolbar.move_forward')" />
|
|
73
|
+
<BtnIcon :icon="`mdiSvg:${mdi.mdiArrowRight}`" />
|
|
74
|
+
</VBtn>
|
|
75
|
+
</template>
|
|
76
|
+
<template v-else>
|
|
77
|
+
<VBtn
|
|
78
|
+
v-for="alignment in alignments"
|
|
79
|
+
:key="alignment.label"
|
|
80
|
+
size="32"
|
|
81
|
+
variant="text"
|
|
82
|
+
color=""
|
|
83
|
+
:class="{ 'tiptapify-btn--active': (editor.getAttributes('image').align ?? null) === alignment.value }"
|
|
84
|
+
@click="toggleAlign(alignment.value)"
|
|
85
|
+
>
|
|
86
|
+
<Tooltip :label="t(alignment.label)" />
|
|
87
|
+
<BtnIcon :icon="`mdiSvg:${alignment.icon}`" />
|
|
88
|
+
</VBtn>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<div class="tiptapify-image-menu__divider" />
|
|
92
|
+
|
|
93
|
+
<VBtn size="32" variant="text" color="" @click="download">
|
|
94
|
+
<Tooltip :label="t('media.image_toolbar.download')" />
|
|
95
|
+
<BtnIcon :icon="`mdiSvg:${mdi.mdiTrayArrowDown}`" />
|
|
96
|
+
</VBtn>
|
|
97
|
+
<VBtn size="32" variant="text" color="" @click="editor.commands.showTiptapifyImage()">
|
|
98
|
+
<Tooltip :label="t('media.image_toolbar.edit')" />
|
|
99
|
+
<BtnIcon :icon="`mdiSvg:${mdi.mdiImageEditOutline}`" />
|
|
100
|
+
</VBtn>
|
|
101
|
+
|
|
102
|
+
<div class="tiptapify-image-menu__divider" />
|
|
103
|
+
|
|
104
|
+
<VBtn size="32" variant="text" color="" @click="editor.chain().focus().deleteSelection().run()">
|
|
105
|
+
<Tooltip :label="t('media.image_toolbar.delete')" />
|
|
106
|
+
<BtnIcon :icon="`mdiSvg:${mdi.mdiTrashCanOutline}`" />
|
|
107
|
+
</VBtn>
|
|
108
|
+
</div>
|
|
109
|
+
</BubbleMenu>
|
|
110
|
+
</template>
|
|
111
|
+
|
|
112
|
+
<style scoped lang="scss">
|
|
113
|
+
.tiptapify-image-menu {
|
|
114
|
+
display: flex;
|
|
115
|
+
align-items: center;
|
|
116
|
+
gap: 2px;
|
|
117
|
+
padding: 4px;
|
|
118
|
+
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
119
|
+
border-radius: 10px;
|
|
120
|
+
background: rgb(var(--v-theme-surface));
|
|
121
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.tiptapify-image-menu__divider {
|
|
125
|
+
width: 1px;
|
|
126
|
+
height: 20px;
|
|
127
|
+
margin: 0 4px;
|
|
128
|
+
background: rgba(var(--v-border-color), var(--v-border-opacity));
|
|
129
|
+
}
|
|
130
|
+
</style>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Editor } from '@tiptap/vue-3'
|
|
3
3
|
import { computed, inject, Ref, ref } from 'vue'
|
|
4
4
|
import { BubbleMenu } from '@tiptap/vue-3/menus'
|
|
5
|
+
import { NodeSelection } from '@tiptap/pm/state'
|
|
5
6
|
import * as mdi from '@mdi/js'
|
|
6
7
|
|
|
7
8
|
defineProps({
|
|
@@ -11,6 +12,10 @@ defineProps({
|
|
|
11
12
|
|
|
12
13
|
const editor = inject('tiptapifyEditor') as Ref<Editor>
|
|
13
14
|
|
|
15
|
+
// The menu follows its text or image when the editor scrolls inside its own box; it is created once the
|
|
16
|
+
// box is there, as the menu reads its options only when it is created
|
|
17
|
+
const scrollTarget = inject<Ref<HTMLElement | null>>('tiptapifyScrollContainer', ref(null))
|
|
18
|
+
|
|
14
19
|
const items = ref([
|
|
15
20
|
{
|
|
16
21
|
name: 'bold',
|
|
@@ -85,11 +90,16 @@ const items = ref([
|
|
|
85
90
|
|
|
86
91
|
<template>
|
|
87
92
|
<BubbleMenu
|
|
88
|
-
v-if="editor"
|
|
93
|
+
v-if="editor && scrollTarget"
|
|
89
94
|
:editor="editor"
|
|
90
|
-
:options="{ placement: 'bottom' }"
|
|
91
|
-
:should-show="({ editor,
|
|
92
|
-
|
|
95
|
+
:options="{ placement: 'bottom', scrollTarget }"
|
|
96
|
+
:should-show="({ editor, state, from, to }) => {
|
|
97
|
+
// A selected block (image, paywall, widget) is not text to format
|
|
98
|
+
if (state.selection instanceof NodeSelection) {
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (editor.isActive('code') || editor.isActive('codeBlock')) {
|
|
93
103
|
return false
|
|
94
104
|
}
|
|
95
105
|
|
|
@@ -99,27 +109,32 @@ const items = ref([
|
|
|
99
109
|
return (from !== to) && !isAllSelected
|
|
100
110
|
}"
|
|
101
111
|
>
|
|
102
|
-
<div class="bubble-menu">
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
</VCard>
|
|
112
|
+
<div class="tiptapify-bubble-menu tiptapify-quiet-buttons">
|
|
113
|
+
<VBtn
|
|
114
|
+
v-for="(item, key) in items"
|
|
115
|
+
:key="key"
|
|
116
|
+
size="32"
|
|
117
|
+
variant="text"
|
|
118
|
+
color=""
|
|
119
|
+
:disabled="item.props?.disabled"
|
|
120
|
+
:class="{ 'tiptapify-btn--active': item.props?.color === 'primary' }"
|
|
121
|
+
@click="item.click"
|
|
122
|
+
>
|
|
123
|
+
<VIcon :icon="item.icon" size="20" />
|
|
124
|
+
</VBtn>
|
|
116
125
|
</div>
|
|
117
126
|
</BubbleMenu>
|
|
118
127
|
</template>
|
|
119
128
|
|
|
120
129
|
<style scoped lang="scss">
|
|
121
|
-
.bubble-menu {
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
.tiptapify-bubble-menu {
|
|
131
|
+
display: flex;
|
|
132
|
+
align-items: center;
|
|
133
|
+
gap: 2px;
|
|
134
|
+
padding: 4px;
|
|
135
|
+
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
136
|
+
border-radius: 10px;
|
|
137
|
+
background: rgb(var(--v-theme-surface));
|
|
138
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
|
|
124
139
|
}
|
|
125
140
|
</style>
|
|
@@ -9,6 +9,7 @@ import { Editor, EditorContent } from '@tiptap/vue-3'
|
|
|
9
9
|
import { TiptapifyAiOptions, TiptapifyAiResolvedOptions, TiptapifyEditor, TiptapifyFooterAlignment, variantBtnTypes, variantFieldTypes } from '@tiptapify/types/editor'
|
|
10
10
|
import MenuBubble from '@tiptapify/components/MenuBubble.vue'
|
|
11
11
|
import MenuFloating from '@tiptapify/components/MenuFloating.vue'
|
|
12
|
+
import ImageBubbleMenu from '@tiptapify/components/ImageBubbleMenu.vue'
|
|
12
13
|
|
|
13
14
|
import { useI18n } from 'vue-i18n'
|
|
14
15
|
|
|
@@ -49,6 +50,8 @@ const props = defineProps({
|
|
|
49
50
|
loading: { type: Boolean, default() { return false } },
|
|
50
51
|
loadingColor: { type: String, default() { return 'default' } },
|
|
51
52
|
loadingHeight: { type: String, default() { return '1px' } },
|
|
53
|
+
// From this many images in a row the editor shows them as a slider grid; 0 turns it off
|
|
54
|
+
imageSlider: { type: Number, default() { return 0 } },
|
|
52
55
|
})
|
|
53
56
|
|
|
54
57
|
const emit = defineEmits(['update:modelValue', 'editor-ready', 'content-changed'])
|
|
@@ -135,9 +138,17 @@ const editor: ShallowRef<Editor | undefined> = getTiptapEditor(
|
|
|
135
138
|
getJSON: () => editorInstance.getJSON(),
|
|
136
139
|
})
|
|
137
140
|
},
|
|
141
|
+
{
|
|
142
|
+
captionPlaceholder: t('dialog.image.caption_placeholder'),
|
|
143
|
+
sliderMinImages: props.imageSlider,
|
|
144
|
+
sliderLabel: t('media.slider'),
|
|
145
|
+
},
|
|
138
146
|
)
|
|
139
147
|
|
|
140
148
|
provide('tiptapifyEditor', editor)
|
|
149
|
+
|
|
150
|
+
const scrollContainer = ref<HTMLElement | null>(null)
|
|
151
|
+
provide('tiptapifyScrollContainer', scrollContainer)
|
|
141
152
|
provide('tiptapifyI18n', { t })
|
|
142
153
|
provide('tiptapifyAi', tiptapifyAi)
|
|
143
154
|
provide(TIPTAPIFY_CONFIG_KEY, {
|
|
@@ -163,7 +174,7 @@ onBeforeUnmount(() => {
|
|
|
163
174
|
|
|
164
175
|
<template>
|
|
165
176
|
<div :id="`tiptapify-editor-${editor?.instanceId}`">
|
|
166
|
-
<div :class="`
|
|
177
|
+
<div :class="`tiptapify-frame rounded-${rounded}`">
|
|
167
178
|
<template v-if="toolbar">
|
|
168
179
|
<Toolbar
|
|
169
180
|
v-if="editor"
|
|
@@ -179,11 +190,13 @@ onBeforeUnmount(() => {
|
|
|
179
190
|
|
|
180
191
|
<VProgressLinear v-model="loadingProgress" :color="loadingColor" :height="loadingHeight" :indeterminate="loading" />
|
|
181
192
|
|
|
182
|
-
<div class="pa-2 tiptapify-container resizable" :style="computeResizableHeight" @click="onContainerClick">
|
|
193
|
+
<div ref="scrollContainer" class="pa-2 tiptapify-container resizable" :style="computeResizableHeight" @click="onContainerClick">
|
|
183
194
|
<MenuFloating v-if="floatingMenu" :variant="variantBtn" :theme="currentTheme" />
|
|
184
195
|
|
|
185
196
|
<MenuBubble v-if="bubbleMenu" :variant="variantBtn" :theme="currentTheme" />
|
|
186
197
|
|
|
198
|
+
<ImageBubbleMenu />
|
|
199
|
+
|
|
187
200
|
<EditorContent :editor="editor" class="tiptapify-editor" />
|
|
188
201
|
</div>
|
|
189
202
|
|
|
@@ -232,12 +245,21 @@ onBeforeUnmount(() => {
|
|
|
232
245
|
--border: 1px solid var(--gray-2);
|
|
233
246
|
}
|
|
234
247
|
|
|
248
|
+
.tiptapify-frame {
|
|
249
|
+
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
250
|
+
border-bottom-color: rgba(var(--v-border-color), calc(var(--v-border-opacity) * 1.6));
|
|
251
|
+
background: rgb(var(--v-theme-surface));
|
|
252
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
253
|
+
}
|
|
254
|
+
|
|
235
255
|
.resizable {
|
|
236
256
|
resize: vertical;
|
|
237
257
|
overflow: auto;
|
|
238
258
|
}
|
|
239
259
|
|
|
240
260
|
.tiptapify-container {
|
|
261
|
+
/* The containing block of the bubble menus, so they scroll with the text they belong to */
|
|
262
|
+
position: relative;
|
|
241
263
|
display: flex;
|
|
242
264
|
flex-direction: column;
|
|
243
265
|
}
|
|
@@ -281,6 +303,8 @@ onBeforeUnmount(() => {
|
|
|
281
303
|
|
|
282
304
|
/* List styles */
|
|
283
305
|
ul, ol {
|
|
306
|
+
/* Next to a floated image the markers stay out of it */
|
|
307
|
+
display: flow-root;
|
|
284
308
|
padding: 0 1rem;
|
|
285
309
|
margin: 1.25rem 1rem 1.25rem 0.4rem;
|
|
286
310
|
|
|
@@ -518,92 +542,185 @@ onBeforeUnmount(() => {
|
|
|
518
542
|
display: block;
|
|
519
543
|
}
|
|
520
544
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
background: rgba(0, 0, 0, 0.5);
|
|
525
|
-
border: 1px solid rgba(255, 255, 255, 0.8);
|
|
526
|
-
border-radius: 2px;
|
|
527
|
-
z-index: 10;
|
|
545
|
+
/* Image with the caption under it (see extensions/components/media/image/caption.ts) */
|
|
546
|
+
.tiptapify-image {
|
|
547
|
+
--tiptapify-image-max-height: 400px;
|
|
528
548
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
549
|
+
flex-direction: column;
|
|
550
|
+
gap: 4px;
|
|
551
|
+
width: fit-content;
|
|
552
|
+
max-width: 100%;
|
|
553
|
+
margin: 1rem 0;
|
|
532
554
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
555
|
+
/* Never wider than the text, never taller than the max height: big photos stay out of the way */
|
|
556
|
+
img {
|
|
557
|
+
max-width: 100%;
|
|
558
|
+
height: auto !important;
|
|
559
|
+
margin: 0;
|
|
560
|
+
border-radius: 4px;
|
|
561
|
+
background: rgba(var(--v-theme-on-surface), 0.04);
|
|
562
|
+
}
|
|
541
563
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
564
|
+
[data-resize-wrapper] {
|
|
565
|
+
max-width: min(100%, calc(var(--tiptapify-image-max-height) * var(--tiptapify-image-ratio, 100)));
|
|
566
|
+
border-radius: 4px;
|
|
567
|
+
outline: 2px solid transparent;
|
|
568
|
+
outline-offset: 2px;
|
|
569
|
+
transition: outline-color 0.15s;
|
|
570
|
+
}
|
|
547
571
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
}
|
|
572
|
+
/* One block: hovering outlines it, a click selects it and shows its toolbar */
|
|
573
|
+
img {
|
|
574
|
+
cursor: pointer;
|
|
575
|
+
}
|
|
553
576
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
cursor: nesw-resize;
|
|
558
|
-
}
|
|
577
|
+
&:hover [data-resize-wrapper] {
|
|
578
|
+
outline-color: rgba(var(--v-theme-primary), 0.35);
|
|
579
|
+
}
|
|
559
580
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
cursor: nwse-resize;
|
|
564
|
-
}
|
|
581
|
+
&.ProseMirror-selectednode [data-resize-wrapper] {
|
|
582
|
+
outline-color: rgb(var(--v-theme-primary));
|
|
583
|
+
}
|
|
565
584
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
left: 8px;
|
|
571
|
-
right: 8px;
|
|
572
|
-
}
|
|
585
|
+
/* Centered unless floated; a floated image lets the text wrap around it */
|
|
586
|
+
&:not([data-align]):not(.tiptapify-slide) {
|
|
587
|
+
align-items: center;
|
|
588
|
+
margin-inline: auto;
|
|
573
589
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
cursor: ns-resize;
|
|
590
|
+
.tiptapify-image-caption {
|
|
591
|
+
text-align: center;
|
|
577
592
|
}
|
|
593
|
+
}
|
|
578
594
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
595
|
+
&[data-align='left'],
|
|
596
|
+
&[data-align='right'] {
|
|
597
|
+
max-width: 50%;
|
|
598
|
+
margin-top: 0.25rem;
|
|
599
|
+
}
|
|
583
600
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
bottom: 8px;
|
|
589
|
-
}
|
|
601
|
+
&[data-align='left'] {
|
|
602
|
+
float: left;
|
|
603
|
+
margin-right: 1.25rem;
|
|
604
|
+
}
|
|
590
605
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
606
|
+
&[data-align='right'] {
|
|
607
|
+
float: right;
|
|
608
|
+
margin-left: 1.25rem;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
595
611
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
612
|
+
/* Images in a row shown as a slider (extensions/components/media/image/slider.ts): a numbered grid */
|
|
613
|
+
.tiptapify-image.tiptapify-slide {
|
|
614
|
+
position: relative;
|
|
615
|
+
display: inline-flex !important;
|
|
616
|
+
box-sizing: border-box;
|
|
617
|
+
width: calc(100% / 3);
|
|
618
|
+
margin: 0;
|
|
619
|
+
padding: 8px 6px;
|
|
620
|
+
vertical-align: top;
|
|
621
|
+
background: rgba(var(--v-theme-primary), 0.06);
|
|
622
|
+
|
|
623
|
+
[data-resize-wrapper] {
|
|
624
|
+
width: 100%;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
img {
|
|
628
|
+
width: 100% !important;
|
|
629
|
+
max-width: none;
|
|
630
|
+
aspect-ratio: 4 / 3;
|
|
631
|
+
object-fit: cover;
|
|
600
632
|
}
|
|
601
633
|
|
|
602
|
-
[data-resize-
|
|
603
|
-
|
|
604
|
-
|
|
634
|
+
[data-resize-handle] {
|
|
635
|
+
display: none !important;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
&::before {
|
|
639
|
+
position: absolute;
|
|
640
|
+
top: 14px;
|
|
641
|
+
left: 12px;
|
|
642
|
+
z-index: 1;
|
|
643
|
+
padding: 0 6px;
|
|
644
|
+
border-radius: 4px;
|
|
645
|
+
background: rgba(0, 0, 0, 0.6);
|
|
646
|
+
color: #fff;
|
|
647
|
+
content: attr(data-slide-label);
|
|
648
|
+
font-size: 12px;
|
|
649
|
+
font-weight: 600;
|
|
650
|
+
line-height: 20px;
|
|
651
|
+
pointer-events: none;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
&:has(.tiptapify-slide) {
|
|
656
|
+
container-type: inline-size;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/* The caret between two slides: a vertical line at the edge of the slide before it */
|
|
660
|
+
.tiptapify-slide + .ProseMirror-gapcursor:has(+ .tiptapify-slide) {
|
|
661
|
+
display: inline-block;
|
|
662
|
+
|
|
663
|
+
&::after {
|
|
664
|
+
top: 8px;
|
|
665
|
+
width: 0;
|
|
666
|
+
height: calc((100cqw / 3 - 12px) * 0.75);
|
|
667
|
+
border-top: 0;
|
|
668
|
+
border-left: 2px solid currentColor;
|
|
605
669
|
}
|
|
606
670
|
}
|
|
671
|
+
|
|
672
|
+
.tiptapify-image-caption {
|
|
673
|
+
width: 100%;
|
|
674
|
+
min-width: 0;
|
|
675
|
+
padding: 2px 0 3px;
|
|
676
|
+
border: 0;
|
|
677
|
+
border-bottom: 1px dashed transparent;
|
|
678
|
+
background: transparent;
|
|
679
|
+
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
680
|
+
font: inherit;
|
|
681
|
+
font-size: 0.875em;
|
|
682
|
+
outline: none;
|
|
683
|
+
transition: border-color 0.15s;
|
|
684
|
+
|
|
685
|
+
&::placeholder {
|
|
686
|
+
color: rgba(var(--v-theme-on-surface), var(--v-disabled-opacity));
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
&:hover:not(:disabled),
|
|
690
|
+
&:focus {
|
|
691
|
+
border-bottom-color: rgba(var(--v-border-color), 0.4);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
&:disabled:placeholder-shown {
|
|
695
|
+
display: none;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/* Resize handles of a selected image: dots on its corners and edge middles */
|
|
700
|
+
[data-node='image'] [data-resize-handle] {
|
|
701
|
+
display: none;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
[data-node='image'].ProseMirror-selectednode [data-resize-handle] {
|
|
705
|
+
position: absolute;
|
|
706
|
+
z-index: 10;
|
|
707
|
+
display: block;
|
|
708
|
+
width: 10px;
|
|
709
|
+
height: 10px;
|
|
710
|
+
border: 2px solid rgb(var(--v-theme-surface));
|
|
711
|
+
border-radius: 50%;
|
|
712
|
+
background: rgb(var(--v-theme-primary));
|
|
713
|
+
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15);
|
|
714
|
+
|
|
715
|
+
&[data-resize-handle='top-left'] { top: -5px !important; left: -5px !important; cursor: nwse-resize; }
|
|
716
|
+
&[data-resize-handle='top-right'] { top: -5px !important; right: -5px !important; left: auto !important; cursor: nesw-resize; }
|
|
717
|
+
&[data-resize-handle='bottom-left'] { bottom: -5px !important; left: -5px !important; top: auto !important; cursor: nesw-resize; }
|
|
718
|
+
&[data-resize-handle='bottom-right'] { right: -5px !important; bottom: -5px !important; top: auto !important; left: auto !important; cursor: nwse-resize; }
|
|
719
|
+
&[data-resize-handle='top'] { top: -5px !important; left: calc(50% - 5px) !important; right: auto !important; cursor: ns-resize; }
|
|
720
|
+
&[data-resize-handle='bottom'] { bottom: -5px !important; top: auto !important; left: calc(50% - 5px) !important; right: auto !important; cursor: ns-resize; }
|
|
721
|
+
&[data-resize-handle='left'] { left: -5px !important; top: calc(50% - 5px) !important; bottom: auto !important; cursor: ew-resize; }
|
|
722
|
+
&[data-resize-handle='right'] { right: -5px !important; left: auto !important; top: calc(50% - 5px) !important; bottom: auto !important; cursor: ew-resize; }
|
|
723
|
+
}
|
|
607
724
|
}
|
|
608
725
|
|
|
609
726
|
/*
|
|
@@ -622,7 +739,7 @@ onBeforeUnmount(() => {
|
|
|
622
739
|
*/
|
|
623
740
|
.tiptapify-dialog-grid {
|
|
624
741
|
display: grid;
|
|
625
|
-
gap:
|
|
742
|
+
gap: 16px;
|
|
626
743
|
grid-template-columns: repeat(12, minmax(0, 1fr));
|
|
627
744
|
|
|
628
745
|
> .tiptapify-dialog-col {
|