tiptapify 0.3.5 → 0.4.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.
- package/dist/content.css +28 -0
- package/dist/tiptapify.css +1 -1
- package/dist/tiptapify.mjs +110770 -65623
- package/dist/tiptapify.umd.js +106 -89
- package/index.d.ts +34 -0
- package/package.json +3 -2
- package/src/components/ImageBubbleMenu.vue +5 -33
- package/src/components/MenuBubble.test.ts +120 -0
- package/src/components/MenuBubble.vue +124 -134
- package/src/components/MenuFloating.vue +48 -45
- package/src/components/Tiptapify.upload.test.ts +10 -0
- package/src/components/Tiptapify.vue +85 -45
- package/src/components/Toolbar/Index.vue +10 -0
- package/src/components/UI/BubbleRow.vue +41 -0
- package/src/components/UI/TiptapifyDialog.test.ts +37 -3
- package/src/components/UI/TiptapifyDialog.vue +15 -4
- package/src/components/editorExtensions.test.ts +9 -0
- package/src/components/editorExtensions.ts +21 -1
- package/src/composables/fakeUploadEndpoint.ts +65 -0
- package/src/composables/useTiptapifyConfig.ts +19 -0
- package/src/composables/useTiptapifyLinkPreview.test.ts +51 -0
- package/src/composables/useTiptapifyLinkPreview.ts +60 -0
- package/src/composables/useTiptapifyUpload.test.ts +96 -0
- package/src/composables/useTiptapifyUpload.ts +126 -0
- package/src/extensions/components/ai/Dialog.test.ts +335 -0
- package/src/extensions/components/ai/Dialog.vue +607 -310
- package/src/extensions/components/ai/backend.test.ts +1 -0
- package/src/extensions/components/ai/backend.ts +2 -1
- package/src/extensions/components/ai/context.test.ts +149 -0
- package/src/extensions/components/ai/context.ts +145 -0
- package/src/extensions/components/ai/diff.test.ts +65 -0
- package/src/extensions/components/ai/diff.ts +120 -0
- package/src/extensions/components/ai/errors.test.ts +45 -0
- package/src/extensions/components/ai/errors.ts +67 -0
- package/src/extensions/components/ai/result.test.ts +144 -0
- package/src/extensions/components/ai/result.ts +101 -0
- package/src/extensions/components/ai/thinking.test.ts +45 -0
- package/src/extensions/components/ai/thinking.ts +58 -0
- package/src/extensions/components/formatExtra/sub/Button.vue +1 -1
- package/src/extensions/components/formatExtra/sup/Button.vue +1 -1
- package/src/extensions/components/media/iframe/Button.vue +6 -7
- package/src/extensions/components/media/iframe/IframeDialog.test.ts +177 -0
- package/src/extensions/components/media/iframe/IframeDialog.vue +477 -105
- package/src/extensions/components/media/iframe/embed.test.ts +123 -0
- package/src/extensions/components/media/iframe/embed.ts +236 -0
- package/src/extensions/components/media/iframe/index.test.ts +123 -0
- package/src/extensions/components/media/iframe/index.ts +92 -10
- package/src/extensions/components/media/image/ImageDialog.vue +430 -132
- package/src/extensions/components/media/image/caption.test.ts +66 -1
- package/src/extensions/components/media/image/caption.ts +27 -1
- package/src/extensions/components/media/image/imageDialog.test.ts +200 -0
- package/src/extensions/components/media/image/imageDialog.ts +114 -0
- package/src/extensions/components/media/image/index.ts +2 -1
- package/src/extensions/components/media/link/Button.test.ts +96 -0
- package/src/extensions/components/media/link/Button.vue +6 -2
- package/src/extensions/components/media/link/LinkDialog.display.test.ts +363 -0
- package/src/extensions/components/media/link/LinkDialog.vue +687 -61
- package/src/extensions/components/media/link/index.ts +111 -6
- package/src/extensions/components/media/link/link.marks.test.ts +89 -0
- package/src/extensions/components/media/link/linkDialog.test.ts +296 -0
- package/src/extensions/components/media/link/linkDialog.ts +154 -0
- package/src/extensions/components/media/linkCard/PasteCardMenu.test.ts +133 -0
- package/src/extensions/components/media/linkCard/PasteCardMenu.vue +168 -0
- package/src/extensions/components/media/linkCard/index.test.ts +146 -0
- package/src/extensions/components/media/linkCard/index.ts +177 -0
- package/src/extensions/components/media/linkCard/linkCardDialog.test.ts +87 -0
- package/src/extensions/components/media/linkCard/linkCardDialog.ts +62 -0
- package/src/extensions/components/media/linkCard/linkPreview.test.ts +102 -0
- package/src/extensions/components/media/linkCard/linkPreview.ts +84 -0
- package/src/extensions/components/media/linkCard/makeLinkCard.test.ts +59 -0
- package/src/extensions/components/media/linkCard/makeLinkCard.ts +58 -0
- package/src/extensions/components/media/linkCard/pasteCard.test.ts +190 -0
- package/src/extensions/components/media/linkCard/pasteCard.ts +153 -0
- package/src/extensions/components/media/video/Button.vue +2 -1
- package/src/extensions/components/media/video/VideoDialog.test.ts +233 -0
- package/src/extensions/components/media/video/VideoDialog.vue +748 -59
- package/src/extensions/components/media/video/index.test.ts +154 -0
- package/src/extensions/components/media/video/index.ts +70 -5
- package/src/extensions/components/media/video/providers.test.ts +148 -0
- package/src/extensions/components/media/video/providers.ts +241 -0
- package/src/extensions/components/media/video/videoFile.test.ts +98 -0
- package/src/extensions/components/media/video/videoFile.ts +142 -0
- package/src/extensions/components/misc/fullscreen/Button.test.ts +55 -0
- package/src/extensions/components/misc/fullscreen/Button.vue +2 -2
- package/src/extensions/components/style/StyleColor.test.ts +255 -0
- package/src/extensions/components/style/StyleColor.vue +358 -208
- package/src/extensions/components/style/color/Button.vue +1 -3
- package/src/extensions/components/style/color/colors.test.ts +191 -0
- package/src/extensions/components/style/color/colors.ts +168 -0
- package/src/extensions/components/style/highlight/Button.vue +1 -3
- package/src/i18n/locales/ar.json +226 -23
- package/src/i18n/locales/bg.json +225 -22
- package/src/i18n/locales/ch.json +226 -23
- package/src/i18n/locales/cz.json +226 -23
- package/src/i18n/locales/de.json +226 -23
- package/src/i18n/locales/el.json +225 -22
- package/src/i18n/locales/en.json +225 -22
- package/src/i18n/locales/es.json +226 -23
- package/src/i18n/locales/et.json +225 -22
- package/src/i18n/locales/fi.json +226 -23
- package/src/i18n/locales/fr.json +226 -23
- package/src/i18n/locales/he.json +225 -22
- package/src/i18n/locales/hi.json +225 -22
- package/src/i18n/locales/hr.json +225 -22
- package/src/i18n/locales/hu.json +226 -23
- package/src/i18n/locales/id.json +225 -22
- package/src/i18n/locales/it.json +226 -23
- package/src/i18n/locales/ja.json +226 -23
- package/src/i18n/locales/ko.json +226 -23
- package/src/i18n/locales/la.json +226 -23
- package/src/i18n/locales/lt.json +226 -23
- package/src/i18n/locales/nl.json +226 -23
- package/src/i18n/locales/no.json +227 -24
- package/src/i18n/locales/pl.json +226 -23
- package/src/i18n/locales/pt-BR.json +225 -22
- package/src/i18n/locales/pt.json +226 -23
- package/src/i18n/locales/ro.json +225 -22
- package/src/i18n/locales/ru.json +227 -24
- package/src/i18n/locales/se.json +226 -23
- package/src/i18n/locales/sk.json +225 -22
- package/src/i18n/locales/sl.json +225 -22
- package/src/i18n/locales/sr.json +225 -22
- package/src/i18n/locales/th.json +226 -23
- package/src/i18n/locales/tr.json +226 -23
- package/src/i18n/locales/uk.json +227 -24
- package/src/i18n/locales/vi.json +226 -23
- package/src/index.ts +8 -0
- package/src/skins/registry.test.ts +1 -1
- package/src/skins/tokens.reachability.test.ts +1 -0
- package/src/skins/tokens.ts +3 -3
- package/src/types/editor.ts +40 -1
- package/src/utils/contentStyles.test.ts +116 -0
- package/src/utils/contentStyles.ts +175 -0
- package/src/utils/mediaSize.test.ts +22 -0
- package/src/utils/mediaSize.ts +27 -0
- package/src/utils/validateHref.test.ts +47 -2
- package/src/utils/validateHref.ts +47 -7
|
@@ -1,135 +1,663 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
|
|
3
|
+
import * as mdi from '@mdi/js'
|
|
3
4
|
import { Editor } from '@tiptap/vue-3'
|
|
5
|
+
import Tooltip from '@tiptapify/components/UI/Tooltip.vue'
|
|
4
6
|
import { useTiptapifyConfig } from '@tiptapify/composables/useTiptapifyConfig'
|
|
7
|
+
import { useTiptapifyLinkPreview } from '@tiptapify/composables/useTiptapifyLinkPreview'
|
|
8
|
+
import { useTiptapifyUpload } from '@tiptapify/composables/useTiptapifyUpload'
|
|
9
|
+
import { applyLinkCard, LinkCardAttrs, LinkCardDraft, LinkCardLayout } from '@tiptapify/extensions/components/media/linkCard'
|
|
10
|
+
import {
|
|
11
|
+
isLinkCardAddress,
|
|
12
|
+
LINK_CARD_LAYOUTS,
|
|
13
|
+
linkCardAddressProblem,
|
|
14
|
+
linkCardToLink,
|
|
15
|
+
} from '@tiptapify/extensions/components/media/linkCard/linkCardDialog'
|
|
16
|
+
import { makeLinkCard } from '@tiptapify/extensions/components/media/linkCard/makeLinkCard'
|
|
17
|
+
import {
|
|
18
|
+
applyLink,
|
|
19
|
+
formRel,
|
|
20
|
+
hasAdvancedValues,
|
|
21
|
+
LinkContext,
|
|
22
|
+
LINK_PROTOCOLS,
|
|
23
|
+
linkToCard,
|
|
24
|
+
missingProtocol,
|
|
25
|
+
parseRel,
|
|
26
|
+
REL_SUGGESTIONS,
|
|
27
|
+
withProtocol,
|
|
28
|
+
removeLink,
|
|
29
|
+
} from '@tiptapify/extensions/components/media/link/linkDialog'
|
|
5
30
|
import { isValidHref } from '@tiptapify/utils/validateHref'
|
|
6
31
|
|
|
7
|
-
import { computed, inject, onMounted, onUnmounted, Ref, ref, useTemplateRef, watch } from 'vue'
|
|
32
|
+
import { computed, inject, onMounted, onUnmounted, reactive, Ref, ref, useTemplateRef, watch } from 'vue'
|
|
8
33
|
|
|
9
34
|
import TiptapifyDialog from '@tiptapify/components/UI/TiptapifyDialog.vue'
|
|
10
35
|
import { ComposerTranslation } from 'vue-i18n'
|
|
11
36
|
|
|
12
|
-
|
|
37
|
+
// The page data of a card is asked once the author stops typing the address
|
|
38
|
+
const FETCH_DELAY = 400
|
|
39
|
+
|
|
40
|
+
const { variantBtn } = useTiptapifyConfig()
|
|
41
|
+
const { available, fetchPreview, recheck } = useTiptapifyLinkPreview()
|
|
42
|
+
const { upload, uploadFile } = useTiptapifyUpload()
|
|
13
43
|
|
|
14
44
|
const editor = inject('tiptapifyEditor') as Ref<Editor>
|
|
15
45
|
const { t } = inject('tiptapifyI18n') as { t: ComposerTranslation }
|
|
16
46
|
|
|
17
|
-
const
|
|
47
|
+
const relDelimiters = [' ', ',']
|
|
48
|
+
|
|
49
|
+
const generateForm = () => ({
|
|
18
50
|
href: '',
|
|
19
|
-
|
|
51
|
+
text: '',
|
|
52
|
+
newTab: false,
|
|
53
|
+
rel: [] as string[],
|
|
20
54
|
cssClass: '',
|
|
21
|
-
|
|
55
|
+
title: '',
|
|
56
|
+
description: '',
|
|
57
|
+
siteName: '',
|
|
58
|
+
image: '',
|
|
59
|
+
layout: 'compact' as LinkCardLayout,
|
|
22
60
|
})
|
|
23
61
|
|
|
24
|
-
const
|
|
62
|
+
const generateContext = (): LinkContext => ({ active: false, attrs: {}, text: '' })
|
|
25
63
|
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
64
|
+
const form = ref(generateForm())
|
|
65
|
+
const context = ref<LinkContext>(generateContext())
|
|
66
|
+
// A selected card is being edited (its texts may be the author's own: no fetch on open)
|
|
67
|
+
const cardEdit = ref(false)
|
|
68
|
+
const advancedOpen = ref(false)
|
|
69
|
+
const hrefError = ref('')
|
|
70
|
+
const fetchError = ref('')
|
|
71
|
+
const display = ref<'link' | 'card'>('link')
|
|
72
|
+
const loading = ref(false)
|
|
73
|
+
const image = reactive({ uploading: false, error: '' })
|
|
30
74
|
|
|
31
|
-
|
|
32
|
-
|
|
75
|
+
// The address of the page data asked last: the same address is not asked again
|
|
76
|
+
let fetchedHref = ''
|
|
77
|
+
let fetchTimer: ReturnType<typeof setTimeout> | null = null
|
|
78
|
+
let fetchRequest: AbortController | null = null
|
|
79
|
+
let imageUpload: AbortController | null = null
|
|
33
80
|
|
|
34
81
|
const dialog = useTemplateRef('dialog')
|
|
82
|
+
const imageInput = useTemplateRef<HTMLInputElement>('imageInput')
|
|
83
|
+
|
|
84
|
+
const isEdit = computed(() => context.value.active || cardEdit.value)
|
|
85
|
+
const isDisabled = computed(() => !form.value.href.trim() || loading.value || image.uploading)
|
|
86
|
+
// A new card only with the link preview endpoint up; a card already in the document stays editable by hand
|
|
87
|
+
const canShowCard = computed(() => available.value || cardEdit.value)
|
|
88
|
+
const isCard = computed(() => canShowCard.value && display.value === 'card')
|
|
89
|
+
const canRefresh = computed(() => available.value && !loading.value && isLinkCardAddress(form.value.href))
|
|
35
90
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
91
|
+
const title = computed(() => {
|
|
92
|
+
if (isCard.value) {
|
|
93
|
+
return t(isEdit.value ? 'dialog.linkCard.title_edit' : 'dialog.linkCard.title_insert')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return t(isEdit.value ? 'dialog.link.title_edit' : 'dialog.link.title_insert')
|
|
39
97
|
})
|
|
40
98
|
|
|
99
|
+
const layouts = computed(() => LINK_CARD_LAYOUTS.map(value => ({ value, label: t(`dialog.linkCard.layout_${value}`) })))
|
|
100
|
+
|
|
101
|
+
const relHint = computed(() => {
|
|
102
|
+
const hint = t('dialog.link.rel_hint')
|
|
103
|
+
|
|
104
|
+
return form.value.newTab ? `${hint} ${t('dialog.link.rel_new_tab_hint')}` : hint
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
function updateRel(value: string[]) {
|
|
108
|
+
form.value.rel = parseRel(value)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// An address without a protocol: the author picks one of the offered protocols, the dialog never picks it
|
|
112
|
+
const hrefMissingProtocol = computed(() => missingProtocol(form.value.href))
|
|
113
|
+
|
|
114
|
+
const hrefField = useTemplateRef<{ focus: () => void }>('hrefField')
|
|
115
|
+
|
|
116
|
+
// Back to the address, so Enter inserts the link right away
|
|
117
|
+
function addProtocol(protocol: string) {
|
|
118
|
+
form.value.href = withProtocol(form.value.href, protocol)
|
|
119
|
+
hrefField.value?.focus()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function errorMessage(error: unknown): string {
|
|
123
|
+
return error instanceof Error ? error.message : String(error)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function cancelFetch() {
|
|
127
|
+
if (fetchTimer) {
|
|
128
|
+
clearTimeout(fetchTimer)
|
|
129
|
+
fetchTimer = null
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// An address whose request was canceled is asked again when the dialog shows a card of it again
|
|
133
|
+
if (fetchRequest) {
|
|
134
|
+
fetchRequest.abort()
|
|
135
|
+
fetchRequest = null
|
|
136
|
+
fetchedHref = ''
|
|
137
|
+
}
|
|
138
|
+
loading.value = false
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The page data into the card fields; false when it failed or another request replaced it
|
|
142
|
+
async function fetchCard(): Promise<boolean> {
|
|
143
|
+
cancelFetch()
|
|
144
|
+
|
|
145
|
+
const href = form.value.href.trim()
|
|
146
|
+
|
|
147
|
+
if (!available.value || !isLinkCardAddress(href)) {
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const controller = new AbortController()
|
|
152
|
+
fetchRequest = controller
|
|
153
|
+
fetchedHref = href
|
|
154
|
+
loading.value = true
|
|
155
|
+
fetchError.value = ''
|
|
156
|
+
|
|
157
|
+
// A canceled or replaced request (another controller) leaves the form alone when it settles
|
|
158
|
+
try {
|
|
159
|
+
const card = await makeLinkCard(href, { fetchPreview, upload: upload.value, signal: controller.signal })
|
|
160
|
+
|
|
161
|
+
if (fetchRequest !== controller) {
|
|
162
|
+
return false
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
Object.assign(form.value, {
|
|
166
|
+
title: card.title ?? '',
|
|
167
|
+
description: card.description ?? '',
|
|
168
|
+
siteName: card.siteName ?? '',
|
|
169
|
+
image: card.image ?? '',
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
return true
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (fetchRequest === controller) {
|
|
175
|
+
fetchError.value = t('dialog.linkCard.fetch_failed', { error: errorMessage(error) })
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return false
|
|
179
|
+
} finally {
|
|
180
|
+
if (fetchRequest === controller) {
|
|
181
|
+
fetchRequest = null
|
|
182
|
+
loading.value = false
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// A new card asks the data of its address; an edited card's data is asked again only by Refresh
|
|
188
|
+
function wantsFetch(): boolean {
|
|
189
|
+
return isCard.value && !cardEdit.value && available.value && isLinkCardAddress(form.value.href)
|
|
190
|
+
&& form.value.href.trim() !== fetchedHref
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function scheduleFetch() {
|
|
194
|
+
cancelFetch()
|
|
195
|
+
|
|
196
|
+
if (wantsFetch()) {
|
|
197
|
+
fetchTimer = setTimeout(() => {
|
|
198
|
+
fetchTimer = null
|
|
199
|
+
void fetchCard()
|
|
200
|
+
}, FETCH_DELAY)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function cancelImageUpload() {
|
|
205
|
+
imageUpload?.abort()
|
|
206
|
+
imageUpload = null
|
|
207
|
+
image.uploading = false
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function replaceImage(event: Event) {
|
|
211
|
+
const input = event.target as HTMLInputElement
|
|
212
|
+
const picked = input.files?.[0] ?? null
|
|
213
|
+
|
|
214
|
+
// Picking the same file again must fire `change` again
|
|
215
|
+
input.value = ''
|
|
216
|
+
|
|
217
|
+
if (!picked) {
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
cancelImageUpload()
|
|
222
|
+
|
|
223
|
+
const controller = new AbortController()
|
|
224
|
+
imageUpload = controller
|
|
225
|
+
image.uploading = true
|
|
226
|
+
image.error = ''
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const { url } = await uploadFile(picked, { kind: 'image', onProgress: () => {}, signal: controller.signal })
|
|
230
|
+
|
|
231
|
+
if (imageUpload === controller) {
|
|
232
|
+
form.value.image = url
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
if (imageUpload === controller) {
|
|
236
|
+
image.error = t('dialog.linkCard.image_failed')
|
|
237
|
+
}
|
|
238
|
+
} finally {
|
|
239
|
+
if (imageUpload === controller) {
|
|
240
|
+
imageUpload = null
|
|
241
|
+
image.uploading = false
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function removeImage() {
|
|
247
|
+
cancelImageUpload()
|
|
248
|
+
form.value.image = ''
|
|
249
|
+
image.error = ''
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function cardAttrs(href: string): LinkCardAttrs {
|
|
253
|
+
const { title: cardTitle, description, image: src, siteName, layout, newTab } = form.value
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
href,
|
|
257
|
+
title: cardTitle.trim() || null,
|
|
258
|
+
description: description.trim() || null,
|
|
259
|
+
image: src.trim() || null,
|
|
260
|
+
siteName: siteName.trim() || null,
|
|
261
|
+
layout,
|
|
262
|
+
newTab,
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function addressError(href: string): string {
|
|
267
|
+
if (hrefMissingProtocol.value) {
|
|
268
|
+
return t('dialog.link.protocol_missing')
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!isValidHref(href)) {
|
|
272
|
+
return t('dialog.link.href_invalid')
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// A card is made of a page: only an http(s) address
|
|
276
|
+
const problem = isCard.value ? linkCardAddressProblem(href) : null
|
|
277
|
+
|
|
278
|
+
if (!problem) {
|
|
279
|
+
return ''
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return problem === 'protocol' ? t('dialog.linkCard.protocol_missing') : t('dialog.linkCard.href_invalid')
|
|
283
|
+
}
|
|
284
|
+
|
|
41
285
|
function apply() {
|
|
42
|
-
|
|
43
|
-
|
|
286
|
+
if (isDisabled.value) {
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const href = form.value.href.trim()
|
|
291
|
+
|
|
292
|
+
hrefError.value = addressError(href)
|
|
293
|
+
if (hrefError.value) {
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (isCard.value) {
|
|
298
|
+
void applyCard(href)
|
|
299
|
+
|
|
300
|
+
return
|
|
301
|
+
}
|
|
44
302
|
|
|
45
|
-
|
|
46
|
-
|
|
303
|
+
// A card displayed as a link again: the link text, else the card's title, else the address
|
|
304
|
+
const applied = cardEdit.value
|
|
305
|
+
? linkCardToLink(editor.value, {
|
|
306
|
+
href,
|
|
307
|
+
title: form.value.text.trim() || form.value.title,
|
|
308
|
+
newTab: form.value.newTab,
|
|
309
|
+
rel: form.value.rel,
|
|
310
|
+
cssClass: form.value.cssClass,
|
|
311
|
+
})
|
|
312
|
+
: applyLink(editor.value, { ...form.value, href }, context.value)
|
|
313
|
+
|
|
314
|
+
if (!applied) {
|
|
315
|
+
hrefError.value = t('dialog.link.apply_error')
|
|
316
|
+
|
|
317
|
+
return
|
|
47
318
|
}
|
|
48
319
|
|
|
49
320
|
close()
|
|
50
321
|
}
|
|
51
322
|
|
|
52
|
-
|
|
53
|
-
|
|
323
|
+
// An address typed and submitted at once is asked first; a failed preview keeps the dialog open with the
|
|
324
|
+
// error, and the next submit inserts the fields filled by hand
|
|
325
|
+
async function applyCard(href: string) {
|
|
326
|
+
if (wantsFetch() && !await fetchCard()) {
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const attrs = cardAttrs(href)
|
|
331
|
+
const applied = cardEdit.value
|
|
332
|
+
? applyLinkCard(editor.value, attrs, true)
|
|
333
|
+
: linkToCard(editor.value, attrs, context.value)
|
|
334
|
+
|
|
335
|
+
if (!applied) {
|
|
336
|
+
hrefError.value = t('dialog.linkCard.apply_failed')
|
|
337
|
+
|
|
338
|
+
return
|
|
339
|
+
}
|
|
54
340
|
|
|
55
341
|
close()
|
|
56
342
|
}
|
|
57
343
|
|
|
58
|
-
function
|
|
59
|
-
|
|
344
|
+
function remove() {
|
|
345
|
+
if (cardEdit.value) {
|
|
346
|
+
editor.value.chain().focus().deleteSelection().run()
|
|
347
|
+
} else {
|
|
348
|
+
removeLink(editor.value)
|
|
349
|
+
}
|
|
60
350
|
|
|
351
|
+
close()
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function close() {
|
|
61
355
|
dialog.value?.close()
|
|
62
356
|
}
|
|
63
357
|
|
|
64
|
-
|
|
358
|
+
function reset() {
|
|
359
|
+
cancelFetch()
|
|
360
|
+
cancelImageUpload()
|
|
361
|
+
|
|
362
|
+
fetchedHref = ''
|
|
363
|
+
form.value = generateForm()
|
|
364
|
+
context.value = generateContext()
|
|
365
|
+
cardEdit.value = false
|
|
366
|
+
advancedOpen.value = false
|
|
367
|
+
hrefError.value = ''
|
|
368
|
+
fetchError.value = ''
|
|
369
|
+
image.error = ''
|
|
370
|
+
display.value = 'link'
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
type ShowLinkDetail = {
|
|
374
|
+
link?: Record<string, string | null>,
|
|
375
|
+
active?: boolean,
|
|
376
|
+
text?: string,
|
|
377
|
+
card?: LinkCardAttrs | null,
|
|
378
|
+
draft?: LinkCardDraft | null,
|
|
379
|
+
editorId: string,
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function openCard(card: LinkCardAttrs) {
|
|
383
|
+
cardEdit.value = true
|
|
384
|
+
display.value = 'card'
|
|
385
|
+
Object.assign(form.value, {
|
|
386
|
+
href: card.href ?? '',
|
|
387
|
+
newTab: card.newTab !== false,
|
|
388
|
+
title: card.title ?? '',
|
|
389
|
+
description: card.description ?? '',
|
|
390
|
+
siteName: card.siteName ?? '',
|
|
391
|
+
image: card.image ?? '',
|
|
392
|
+
layout: LINK_CARD_LAYOUTS.includes(card.layout) ? card.layout : 'compact',
|
|
393
|
+
})
|
|
394
|
+
// The texts of a saved card stay as they are: they may be the author's own
|
|
395
|
+
fetchedHref = form.value.href.trim()
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const showLink = (event: CustomEvent<ShowLinkDetail>) => {
|
|
65
399
|
if (event.detail.editorId !== editor.value.instanceId) {
|
|
66
400
|
return
|
|
67
401
|
}
|
|
68
402
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
403
|
+
reset()
|
|
404
|
+
|
|
405
|
+
const { card, draft } = event.detail
|
|
406
|
+
const link = event.detail.link ?? {}
|
|
407
|
+
const newTab = link.target === '_blank'
|
|
408
|
+
|
|
409
|
+
context.value = { active: !card && !!event.detail.active, attrs: card ? {} : link, text: card ? '' : event.detail.text ?? '' }
|
|
410
|
+
Object.assign(form.value, {
|
|
411
|
+
href: link.href ?? '',
|
|
412
|
+
text: context.value.text,
|
|
413
|
+
newTab,
|
|
414
|
+
rel: formRel(link.rel, newTab),
|
|
415
|
+
cssClass: link.class ?? '',
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
if (card) {
|
|
419
|
+
openCard(card)
|
|
420
|
+
} else if (draft) {
|
|
421
|
+
form.value.href = draft.href ?? ''
|
|
422
|
+
form.value.newTab = draft.newTab ?? false
|
|
423
|
+
display.value = 'card'
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
advancedOpen.value = hasAdvancedValues(form.value.rel, form.value.cssClass)
|
|
427
|
+
// An endpoint that failed may be up by now
|
|
428
|
+
void recheck()
|
|
73
429
|
|
|
74
430
|
dialog.value?.open()
|
|
75
431
|
}
|
|
76
432
|
|
|
433
|
+
watch(() => form.value.href, (href) => {
|
|
434
|
+
hrefError.value = ''
|
|
435
|
+
|
|
436
|
+
if (href.trim() === fetchedHref) {
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
fetchedHref = ''
|
|
441
|
+
fetchError.value = ''
|
|
442
|
+
scheduleFetch()
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
// Switched to a card (or the endpoint came up): asked at once, without the typing delay
|
|
446
|
+
watch(isCard, (card) => {
|
|
447
|
+
hrefError.value = ''
|
|
448
|
+
|
|
449
|
+
if (card && wantsFetch()) {
|
|
450
|
+
void fetchCard()
|
|
451
|
+
} else if (!card) {
|
|
452
|
+
cancelFetch()
|
|
453
|
+
}
|
|
454
|
+
})
|
|
455
|
+
|
|
77
456
|
onMounted(() => {
|
|
78
457
|
window.addEventListener('tiptapify-show-link', showLink as EventListener)
|
|
79
458
|
})
|
|
80
459
|
|
|
81
460
|
onUnmounted(() => {
|
|
82
461
|
window.removeEventListener('tiptapify-show-link', showLink as EventListener)
|
|
83
|
-
})
|
|
84
462
|
|
|
85
|
-
|
|
86
|
-
|
|
463
|
+
cancelFetch()
|
|
464
|
+
cancelImageUpload()
|
|
87
465
|
})
|
|
88
466
|
</script>
|
|
89
467
|
|
|
90
468
|
<template>
|
|
91
|
-
<TiptapifyDialog
|
|
469
|
+
<TiptapifyDialog
|
|
470
|
+
ref="dialog"
|
|
471
|
+
module="link"
|
|
472
|
+
:title="title"
|
|
473
|
+
:max-width="isCard ? 560 : 480"
|
|
474
|
+
@submit="apply"
|
|
475
|
+
@closed="reset"
|
|
476
|
+
>
|
|
92
477
|
<template #content>
|
|
93
478
|
<VCardText>
|
|
94
479
|
<div class="tiptapify-dialog-grid">
|
|
480
|
+
<div v-if="canShowCard" class="tiptapify-dialog-col tiptapify-link-display">
|
|
481
|
+
<span class="text-body-2 text-medium-emphasis">{{ t('dialog.linkCard.display') }}</span>
|
|
482
|
+
<VBtnToggle
|
|
483
|
+
v-model="display"
|
|
484
|
+
mandatory
|
|
485
|
+
density="compact"
|
|
486
|
+
variant="outlined"
|
|
487
|
+
divided
|
|
488
|
+
>
|
|
489
|
+
<VBtn value="link" :prepend-icon="`mdiSvg:${mdi.mdiLinkVariant}`">
|
|
490
|
+
{{ t('dialog.linkCard.display_link') }}
|
|
491
|
+
</VBtn>
|
|
492
|
+
<VBtn value="card" :prepend-icon="`mdiSvg:${mdi.mdiCardTextOutline}`">
|
|
493
|
+
{{ t('dialog.linkCard.display_card') }}
|
|
494
|
+
</VBtn>
|
|
495
|
+
</VBtnToggle>
|
|
496
|
+
</div>
|
|
497
|
+
|
|
95
498
|
<div class="tiptapify-dialog-col">
|
|
96
499
|
<VTextField
|
|
97
|
-
|
|
500
|
+
ref="hrefField"
|
|
501
|
+
v-model="form.href"
|
|
98
502
|
density="compact"
|
|
99
503
|
variant="outlined"
|
|
100
504
|
:label="t('dialog.link.href')"
|
|
101
|
-
:error-messages="
|
|
505
|
+
:error-messages="hrefError || fetchError"
|
|
506
|
+
:hint="loading ? t('dialog.linkCard.loading') : ''"
|
|
507
|
+
:loading="loading"
|
|
508
|
+
persistent-hint
|
|
509
|
+
hide-details="auto"
|
|
102
510
|
autofocus
|
|
103
|
-
|
|
511
|
+
>
|
|
512
|
+
<template v-if="isCard && available" #append-inner>
|
|
513
|
+
<VBtn
|
|
514
|
+
variant="text"
|
|
515
|
+
size="28"
|
|
516
|
+
icon
|
|
517
|
+
:disabled="!canRefresh"
|
|
518
|
+
:aria-label="t('dialog.linkCard.refresh')"
|
|
519
|
+
@click="fetchCard"
|
|
520
|
+
>
|
|
521
|
+
<VIcon size="18" :icon="`mdiSvg:${mdi.mdiRefresh}`" />
|
|
522
|
+
<Tooltip :label="t('dialog.linkCard.refresh')" />
|
|
523
|
+
</VBtn>
|
|
524
|
+
</template>
|
|
525
|
+
</VTextField>
|
|
526
|
+
<div v-if="hrefMissingProtocol" class="tiptapify-link-protocols">
|
|
527
|
+
<span v-if="!hrefError" class="text-caption text-medium-emphasis">{{ t('dialog.link.protocol_missing') }}</span>
|
|
528
|
+
<VChip
|
|
529
|
+
v-for="protocol in LINK_PROTOCOLS"
|
|
530
|
+
:key="protocol"
|
|
531
|
+
size="small"
|
|
532
|
+
label
|
|
533
|
+
variant="outlined"
|
|
534
|
+
@click="addProtocol(protocol)"
|
|
535
|
+
>
|
|
536
|
+
{{ protocol }}://
|
|
537
|
+
</VChip>
|
|
538
|
+
</div>
|
|
104
539
|
</div>
|
|
105
540
|
|
|
106
|
-
<div class="tiptapify-dialog-col
|
|
107
|
-
<
|
|
108
|
-
v-model="
|
|
109
|
-
:items="targetAttrs"
|
|
110
|
-
:label="t('dialog.link.target')"
|
|
111
|
-
variant="outlined"
|
|
112
|
-
return-object
|
|
541
|
+
<div v-if="!isCard" class="tiptapify-dialog-col">
|
|
542
|
+
<VTextField
|
|
543
|
+
v-model="form.text"
|
|
113
544
|
density="compact"
|
|
545
|
+
variant="outlined"
|
|
546
|
+
:label="t('dialog.link.text')"
|
|
547
|
+
:hint="context.text ? '' : t('dialog.link.text_hint')"
|
|
548
|
+
persistent-hint
|
|
549
|
+
hide-details="auto"
|
|
114
550
|
/>
|
|
115
551
|
</div>
|
|
116
552
|
|
|
117
|
-
<div class="tiptapify-dialog-col tiptapify-
|
|
118
|
-
<
|
|
553
|
+
<div v-if="isCard" class="tiptapify-dialog-col tiptapify-link-card-fields">
|
|
554
|
+
<div class="tiptapify-link-card-fields__media" role="group" :aria-label="t('dialog.linkCard.image')">
|
|
555
|
+
<input ref="imageInput" type="file" accept="image/*" hidden @change="replaceImage">
|
|
556
|
+
<div class="tiptapify-link-card-fields__image">
|
|
557
|
+
<VProgressCircular v-if="image.uploading || loading" indeterminate size="20" width="2" />
|
|
558
|
+
<img v-else-if="form.image" :src="form.image" alt="">
|
|
559
|
+
<VIcon v-else size="28" :icon="`mdiSvg:${mdi.mdiImageOutline}`" />
|
|
560
|
+
</div>
|
|
561
|
+
<div class="tiptapify-link-card-fields__image-actions">
|
|
562
|
+
<VBtn size="small" variant="text" :disabled="image.uploading" @click="imageInput?.click()">
|
|
563
|
+
{{ t(form.image ? 'dialog.linkCard.image_replace' : 'dialog.linkCard.image_choose') }}
|
|
564
|
+
</VBtn>
|
|
565
|
+
<VBtn size="small" variant="text" :disabled="!form.image && !image.uploading" @click="removeImage">
|
|
566
|
+
{{ t('dialog.linkCard.image_remove') }}
|
|
567
|
+
</VBtn>
|
|
568
|
+
</div>
|
|
569
|
+
<div v-if="image.error" class="text-error text-caption" role="alert">
|
|
570
|
+
{{ image.error }}
|
|
571
|
+
</div>
|
|
572
|
+
</div>
|
|
573
|
+
|
|
574
|
+
<div class="tiptapify-link-card-fields__texts">
|
|
575
|
+
<VTextField
|
|
576
|
+
v-model="form.title"
|
|
577
|
+
density="compact"
|
|
578
|
+
variant="outlined"
|
|
579
|
+
:label="t('dialog.linkCard.page_title')"
|
|
580
|
+
hide-details="auto"
|
|
581
|
+
/>
|
|
582
|
+
<VTextarea
|
|
583
|
+
v-model="form.description"
|
|
584
|
+
density="compact"
|
|
585
|
+
variant="outlined"
|
|
586
|
+
:label="t('dialog.linkCard.description')"
|
|
587
|
+
rows="2"
|
|
588
|
+
max-rows="3"
|
|
589
|
+
auto-grow
|
|
590
|
+
hide-details="auto"
|
|
591
|
+
/>
|
|
592
|
+
<VTextField
|
|
593
|
+
v-model="form.siteName"
|
|
594
|
+
density="compact"
|
|
595
|
+
variant="outlined"
|
|
596
|
+
:label="t('dialog.linkCard.site_name')"
|
|
597
|
+
hide-details="auto"
|
|
598
|
+
/>
|
|
599
|
+
</div>
|
|
119
600
|
</div>
|
|
120
601
|
|
|
121
|
-
<div class="tiptapify-dialog-col">
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
multiple
|
|
128
|
-
chips
|
|
129
|
-
closable-chips
|
|
130
|
-
clearable
|
|
602
|
+
<div v-if="isCard" class="tiptapify-dialog-col tiptapify-link-display" role="group" :aria-label="t('dialog.linkCard.layout')">
|
|
603
|
+
<span class="text-body-2 text-medium-emphasis">{{ t('dialog.linkCard.layout') }}</span>
|
|
604
|
+
<VBtnToggle
|
|
605
|
+
v-model="form.layout"
|
|
606
|
+
class="tiptapify-link-card-fields__layout"
|
|
607
|
+
mandatory
|
|
131
608
|
density="compact"
|
|
132
|
-
|
|
609
|
+
variant="outlined"
|
|
610
|
+
divided
|
|
611
|
+
>
|
|
612
|
+
<VBtn v-for="layout in layouts" :key="layout.value" :value="layout.value">
|
|
613
|
+
{{ layout.label }}
|
|
614
|
+
</VBtn>
|
|
615
|
+
</VBtnToggle>
|
|
616
|
+
</div>
|
|
617
|
+
|
|
618
|
+
<div class="tiptapify-dialog-col">
|
|
619
|
+
<VCheckbox v-model="form.newTab" density="compact" :label="t('dialog.link.new_tab')" hide-details />
|
|
620
|
+
</div>
|
|
621
|
+
|
|
622
|
+
<div v-if="!isCard" class="tiptapify-dialog-col">
|
|
623
|
+
<VBtn
|
|
624
|
+
variant="text"
|
|
625
|
+
size="small"
|
|
626
|
+
class="tiptapify-link-advanced-toggle"
|
|
627
|
+
:aria-expanded="advancedOpen"
|
|
628
|
+
:append-icon="`mdiSvg:${advancedOpen ? mdi.mdiChevronUp : mdi.mdiChevronDown}`"
|
|
629
|
+
@click="advancedOpen = !advancedOpen"
|
|
630
|
+
>
|
|
631
|
+
{{ t('dialog.link.advanced') }}
|
|
632
|
+
</VBtn>
|
|
633
|
+
|
|
634
|
+
<VExpandTransition>
|
|
635
|
+
<div v-show="advancedOpen" class="tiptapify-link-advanced">
|
|
636
|
+
<VCombobox
|
|
637
|
+
:model-value="form.rel"
|
|
638
|
+
:items="REL_SUGGESTIONS"
|
|
639
|
+
:delimiters="relDelimiters"
|
|
640
|
+
:label="t('dialog.link.rel')"
|
|
641
|
+
:hint="relHint"
|
|
642
|
+
variant="outlined"
|
|
643
|
+
density="compact"
|
|
644
|
+
multiple
|
|
645
|
+
chips
|
|
646
|
+
closable-chips
|
|
647
|
+
persistent-hint
|
|
648
|
+
hide-details="auto"
|
|
649
|
+
@update:model-value="updateRel"
|
|
650
|
+
/>
|
|
651
|
+
|
|
652
|
+
<VTextField
|
|
653
|
+
v-model="form.cssClass"
|
|
654
|
+
density="compact"
|
|
655
|
+
variant="outlined"
|
|
656
|
+
:label="t('dialog.link.class')"
|
|
657
|
+
hide-details
|
|
658
|
+
/>
|
|
659
|
+
</div>
|
|
660
|
+
</VExpandTransition>
|
|
133
661
|
</div>
|
|
134
662
|
</div>
|
|
135
663
|
</VCardText>
|
|
@@ -139,20 +667,118 @@ watch(() => attrs.value.href, () => {
|
|
|
139
667
|
<VCardActions>
|
|
140
668
|
<div class="tiptapify-dialog-actions">
|
|
141
669
|
<div class="tiptapify-dialog-actions__start">
|
|
142
|
-
<VBtn v-if="
|
|
143
|
-
{{ t('dialog.
|
|
670
|
+
<VBtn v-if="isEdit" color="error" :variant="variantBtn" @click="remove">
|
|
671
|
+
{{ t(cardEdit ? 'dialog.remove' : 'dialog.link.remove') }}
|
|
144
672
|
</VBtn>
|
|
145
673
|
</div>
|
|
146
674
|
<div class="tiptapify-dialog-actions__end">
|
|
147
675
|
<VBtn :variant="variantBtn" @click="close">
|
|
148
|
-
{{ t('dialog.
|
|
676
|
+
{{ t('dialog.cancel') }}
|
|
149
677
|
</VBtn>
|
|
150
|
-
<VBtn color="primary" :variant="variantBtn" :disabled="isDisabled
|
|
151
|
-
{{ t('dialog.
|
|
678
|
+
<VBtn type="submit" color="primary" :variant="variantBtn" :disabled="isDisabled">
|
|
679
|
+
{{ isEdit ? t('dialog.save') : t('dialog.insert') }}
|
|
152
680
|
</VBtn>
|
|
153
681
|
</div>
|
|
154
682
|
</div>
|
|
155
683
|
</VCardActions>
|
|
156
684
|
</template>
|
|
157
685
|
</TiptapifyDialog>
|
|
158
|
-
</template>
|
|
686
|
+
</template>
|
|
687
|
+
|
|
688
|
+
<style lang="scss" scoped>
|
|
689
|
+
.tiptapify-link-advanced-toggle {
|
|
690
|
+
margin-inline-start: -8px;
|
|
691
|
+
letter-spacing: normal;
|
|
692
|
+
text-transform: none;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.tiptapify-link-display {
|
|
696
|
+
display: flex;
|
|
697
|
+
flex-wrap: wrap;
|
|
698
|
+
align-items: center;
|
|
699
|
+
gap: 12px;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
.tiptapify-link-protocols {
|
|
703
|
+
display: flex;
|
|
704
|
+
flex-wrap: wrap;
|
|
705
|
+
align-items: center;
|
|
706
|
+
gap: 6px;
|
|
707
|
+
padding: 6px 16px 0;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
.tiptapify-link-advanced {
|
|
711
|
+
display: flex;
|
|
712
|
+
flex-direction: column;
|
|
713
|
+
gap: 16px;
|
|
714
|
+
padding-top: 12px;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.tiptapify-link-card-fields {
|
|
718
|
+
display: flex;
|
|
719
|
+
align-items: flex-start;
|
|
720
|
+
gap: 16px;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
.tiptapify-link-card-fields__media,
|
|
724
|
+
.tiptapify-link-card-fields__texts {
|
|
725
|
+
display: flex;
|
|
726
|
+
flex-direction: column;
|
|
727
|
+
min-width: 0;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.tiptapify-link-card-fields__media {
|
|
731
|
+
flex: none;
|
|
732
|
+
gap: 8px;
|
|
733
|
+
width: 180px;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.tiptapify-link-card-fields__texts {
|
|
737
|
+
flex: 1;
|
|
738
|
+
gap: 12px;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
.tiptapify-link-card-fields__image {
|
|
742
|
+
display: flex;
|
|
743
|
+
align-items: center;
|
|
744
|
+
justify-content: center;
|
|
745
|
+
overflow: hidden;
|
|
746
|
+
aspect-ratio: 1.91 / 1;
|
|
747
|
+
border-radius: 8px;
|
|
748
|
+
background: rgba(var(--v-theme-on-surface), 0.04);
|
|
749
|
+
color: rgba(var(--v-theme-on-surface), var(--v-medium-emphasis-opacity));
|
|
750
|
+
|
|
751
|
+
img {
|
|
752
|
+
display: block;
|
|
753
|
+
width: 100%;
|
|
754
|
+
height: 100%;
|
|
755
|
+
object-fit: cover;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
.tiptapify-link-card-fields__image-actions {
|
|
760
|
+
display: flex;
|
|
761
|
+
flex-wrap: wrap;
|
|
762
|
+
gap: 4px;
|
|
763
|
+
|
|
764
|
+
.v-btn {
|
|
765
|
+
letter-spacing: normal;
|
|
766
|
+
text-transform: none;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
.tiptapify-link-card-fields__layout {
|
|
771
|
+
max-width: 100%;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
@media (max-width: 520px) {
|
|
775
|
+
.tiptapify-link-card-fields {
|
|
776
|
+
flex-direction: column;
|
|
777
|
+
align-items: stretch;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.tiptapify-link-card-fields__media {
|
|
781
|
+
width: 100%;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
</style>
|