@bagelink/vue 1.18.7 → 1.19.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/components/Dropdown.vue.d.ts.map +1 -1
- package/dist/components/Icon/Icon.vue.d.ts.map +1 -1
- package/dist/components/dataTable/DataTable.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts +1 -0
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/config.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/index.vue.d.ts +6 -0
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/richTextTypes.d.ts +1 -0
- package/dist/components/form/inputs/RichText/richTextTypes.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/contentMetrics.d.ts +7 -0
- package/dist/components/form/inputs/RichText/utils/contentMetrics.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/image.d.ts +17 -0
- package/dist/components/form/inputs/RichText/utils/image.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/sanitizePaste.d.ts +15 -0
- package/dist/components/form/inputs/RichText/utils/sanitizePaste.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/selection.d.ts +1 -1
- package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/video.d.ts +23 -0
- package/dist/components/form/inputs/RichText/utils/video.d.ts.map +1 -0
- package/dist/components/form/inputs/SelectBtn.vue.d.ts +28 -1
- package/dist/components/form/inputs/SelectBtn.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts +2 -0
- package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/Upload/useFileUpload.d.ts +1 -0
- package/dist/components/form/inputs/Upload/useFileUpload.d.ts.map +1 -1
- package/dist/core.css +2 -2
- package/dist/index.cjs +218 -130
- package/dist/index.css +1 -1
- package/dist/index.mjs +18662 -17635
- package/dist/style.css +2 -2
- package/package.json +1 -1
- package/src/components/Dropdown.vue +30 -0
- package/src/components/FilterQuery.vue +1 -1
- package/src/components/Icon/Icon.vue +4 -0
- package/src/components/dataTable/DataTable.vue +6 -3
- package/src/components/form/inputs/RichText/components/EditorToolbar.vue +238 -8
- package/src/components/form/inputs/RichText/composables/useCommands.ts +23 -5
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +28 -0
- package/src/components/form/inputs/RichText/config.ts +17 -14
- package/src/components/form/inputs/RichText/index.vue +381 -478
- package/src/components/form/inputs/RichText/richTextTypes.ts +1 -0
- package/src/components/form/inputs/RichText/utils/commands.ts +437 -24
- package/src/components/form/inputs/RichText/utils/contentMetrics.ts +24 -0
- package/src/components/form/inputs/RichText/utils/image.ts +136 -0
- package/src/components/form/inputs/RichText/utils/sanitizePaste.ts +110 -0
- package/src/components/form/inputs/RichText/utils/selection.ts +32 -0
- package/src/components/form/inputs/RichText/utils/table.ts +77 -8
- package/src/components/form/inputs/RichText/utils/video.ts +270 -0
- package/src/components/form/inputs/SelectBtn.vue +126 -15
- package/src/components/form/inputs/TelInput.vue +1 -1
- package/src/components/form/inputs/Upload/UploadInput.vue +3 -2
- package/src/components/form/inputs/Upload/useFileUpload.ts +2 -0
- package/src/styles/colors.css +5 -2
- package/src/styles/core.css +1 -0
- package/src/styles/rich-content.css +109 -0
- package/dist/components/form/inputs/RichText/utils/linkPolicy.d.ts +0 -11
- package/dist/components/form/inputs/RichText/utils/linkPolicy.d.ts.map +0 -1
- package/src/components/form/inputs/RichText/utils/linkPolicy.ts +0 -48
|
@@ -4,6 +4,10 @@ import type { CustomToolbarButton, RichTextExtension, RichTextToolbarApi, Toolba
|
|
|
4
4
|
import type { DropdownItem } from '../../../../index'
|
|
5
5
|
import { CodeEditor, copyText, Btn, ContextMenu, Dialog, Divider, BglVideo, Icon, Card, ColorInput, pathKeyToURL, usePointerSession } from '../../../../index'
|
|
6
6
|
import { watch, onUnmounted, onBeforeUnmount, onMounted, ref, computed, useAttrs, inject } from 'vue'
|
|
7
|
+
import { getRichTextMetrics } from './utils/contentMetrics'
|
|
8
|
+
import { insertPastedContent } from './utils/sanitizePaste'
|
|
9
|
+
import { buildImageFigure, createDefaultImageData, hydrateImageFigures, readImageData } from './utils/image'
|
|
10
|
+
import { buildVideoFigure, createDefaultVideoData, hydrateVideoFigures, readVideoData, serializeVideoFigures } from './utils/video'
|
|
7
11
|
import CheckInput from '../CheckInput.vue'
|
|
8
12
|
import NumberInput from '../NumberInput.vue'
|
|
9
13
|
import SelectBtn from '../SelectBtn.vue'
|
|
@@ -20,6 +24,7 @@ import {
|
|
|
20
24
|
deleteColumn as tableDeleteColumn,
|
|
21
25
|
deleteRow as tableDeleteRow,
|
|
22
26
|
findCellCoords,
|
|
27
|
+
focusTableCell,
|
|
23
28
|
insertColumn as tableInsertColumn,
|
|
24
29
|
mergeCells as tableMergeCells,
|
|
25
30
|
moveColumn as tableMoveColumn,
|
|
@@ -30,7 +35,6 @@ import {
|
|
|
30
35
|
import type { BagelInputShellProps } from '../bagelInputShell'
|
|
31
36
|
import { useBagelInputShell } from '../bagelInputShell'
|
|
32
37
|
import { RICH_TEXT_EXTENSION_REGISTRY_KEY } from './richTextExtensions'
|
|
33
|
-
import { normalizeRichTextLink, sanitizeRichTextLinks } from './utils/linkPolicy'
|
|
34
38
|
|
|
35
39
|
// Disable automatic inheritance of non-prop attributes
|
|
36
40
|
defineOptions({
|
|
@@ -68,11 +72,17 @@ const props = defineProps<{
|
|
|
68
72
|
textColor?: string
|
|
69
73
|
// Font size
|
|
70
74
|
fontSize?: number | string
|
|
75
|
+
// Optional document metrics and limits
|
|
76
|
+
showCounter?: boolean
|
|
77
|
+
maxLength?: number
|
|
78
|
+
maxWords?: number
|
|
71
79
|
// Field-specific CSS injected into the editor iframe.
|
|
72
80
|
contentStyles?: string
|
|
81
|
+
// Optional image uploader. Must resolve to a persistent path key or URL.
|
|
82
|
+
uploadImage?: (file: File) => Promise<string>
|
|
73
83
|
} & BagelInputShellProps>()
|
|
74
84
|
|
|
75
|
-
const emit = defineEmits(['update:modelValue'])
|
|
85
|
+
const emit = defineEmits(['update:modelValue', 'limit-exceeded'])
|
|
76
86
|
|
|
77
87
|
const { shellClass, shellStyle } = useBagelInputShell(props)
|
|
78
88
|
const extensionRegistry = inject(RICH_TEXT_EXTENSION_REGISTRY_KEY, new Map())
|
|
@@ -108,6 +118,35 @@ const effectiveContentStyles = computed(() => [
|
|
|
108
118
|
...resolvedExtensions.value.map(extension => extension.contentStyles),
|
|
109
119
|
].filter(Boolean).join('\n'))
|
|
110
120
|
|
|
121
|
+
function collectDocumentColors(property: 'color' | 'background-color'): string[] {
|
|
122
|
+
if (!normalizedModelValue.value || typeof document === 'undefined') { return [] }
|
|
123
|
+
const parsed = new DOMParser().parseFromString(normalizedModelValue.value, 'text/html')
|
|
124
|
+
const colors = new Set<string>()
|
|
125
|
+
|
|
126
|
+
parsed.body.querySelectorAll<HTMLElement>('[style]').forEach((element) => {
|
|
127
|
+
const value = element.style.getPropertyValue(property).trim()
|
|
128
|
+
if (value && value !== 'transparent' && value !== 'inherit') {
|
|
129
|
+
colors.add(value)
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
if (property === 'background-color') {
|
|
134
|
+
parsed.body.querySelectorAll<HTMLElement>('[bgcolor]').forEach((element) => {
|
|
135
|
+
const value = element.getAttribute('bgcolor')?.trim()
|
|
136
|
+
if (value) {
|
|
137
|
+
colors.add(value)
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return Array.from(colors).slice(0, 12)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const documentColors = computed(() => Array.from(new Set([
|
|
146
|
+
...collectDocumentColors('color'),
|
|
147
|
+
...collectDocumentColors('background-color'),
|
|
148
|
+
])).slice(0, 12))
|
|
149
|
+
|
|
111
150
|
function updateCustomToolbarStyles() {
|
|
112
151
|
const { doc } = editor.state
|
|
113
152
|
if (!doc) return
|
|
@@ -147,6 +186,25 @@ const editor = useEditor()
|
|
|
147
186
|
const isInitializing = ref(false)
|
|
148
187
|
const hasInitialized = ref(false)
|
|
149
188
|
const commands = useCommands(editor.state, editor.state.debug)
|
|
189
|
+
const documentMetrics = computed(() => {
|
|
190
|
+
void editor.state.content
|
|
191
|
+
return getRichTextMetrics(normalizedModelValue.value, editor.state.doc)
|
|
192
|
+
})
|
|
193
|
+
const characterCount = computed(() => documentMetrics.value.characters)
|
|
194
|
+
const wordCount = computed(() => documentMetrics.value.words)
|
|
195
|
+
const characterLimitExceeded = computed(() => Boolean(props.maxLength && characterCount.value > props.maxLength))
|
|
196
|
+
const wordLimitExceeded = computed(() => Boolean(props.maxWords && wordCount.value > props.maxWords))
|
|
197
|
+
const hasExceededLimit = computed(() => characterLimitExceeded.value || wordLimitExceeded.value)
|
|
198
|
+
const showDocumentCounter = computed(() => props.showCounter || Boolean(props.maxLength) || Boolean(props.maxWords))
|
|
199
|
+
const counterLabel = computed(() => {
|
|
200
|
+
const characters = props.maxLength
|
|
201
|
+
? `${characterCount.value} / ${props.maxLength} characters`
|
|
202
|
+
: `${characterCount.value} characters`
|
|
203
|
+
const words = props.maxWords
|
|
204
|
+
? `${wordCount.value} / ${props.maxWords} words`
|
|
205
|
+
: `${wordCount.value} words`
|
|
206
|
+
return `${characters} · ${words}`
|
|
207
|
+
})
|
|
150
208
|
|
|
151
209
|
function setBlock(tagName: string, className?: string, removeClasses: string[] = []) {
|
|
152
210
|
const { doc } = editor.state
|
|
@@ -356,17 +414,8 @@ let pendingLinkData: {
|
|
|
356
414
|
|
|
357
415
|
// Image modal state
|
|
358
416
|
const showImageModal = ref(false)
|
|
359
|
-
const imageForm = ref(
|
|
360
|
-
|
|
361
|
-
alt: '',
|
|
362
|
-
width: '',
|
|
363
|
-
height: '',
|
|
364
|
-
credit: '',
|
|
365
|
-
figcaption: false,
|
|
366
|
-
align: 'full', // full, start, end
|
|
367
|
-
link: '',
|
|
368
|
-
linkNewTab: false
|
|
369
|
-
})
|
|
417
|
+
const imageForm = ref(createDefaultImageData())
|
|
418
|
+
const imageUploadError = ref('')
|
|
370
419
|
let pendingImageData: {
|
|
371
420
|
selection: Selection
|
|
372
421
|
range: Range
|
|
@@ -394,19 +443,7 @@ const inlineToolbarSelection = ref<Selection | null>(null)
|
|
|
394
443
|
|
|
395
444
|
// Video modal state
|
|
396
445
|
const showVideoModal = ref(false)
|
|
397
|
-
const videoForm = ref(
|
|
398
|
-
src: '',
|
|
399
|
-
width: '',
|
|
400
|
-
autoplay: false,
|
|
401
|
-
mute: false,
|
|
402
|
-
controls: true,
|
|
403
|
-
loop: false,
|
|
404
|
-
aspectRatio: '16:9',
|
|
405
|
-
customWidth: '',
|
|
406
|
-
customHeight: '',
|
|
407
|
-
caption: '',
|
|
408
|
-
showCaption: false
|
|
409
|
-
})
|
|
446
|
+
const videoForm = ref(createDefaultVideoData())
|
|
410
447
|
let pendingVideoData: {
|
|
411
448
|
selection: Selection
|
|
412
449
|
range: Range
|
|
@@ -697,7 +734,7 @@ function showTooltipMessage(message: string, x?: number, y?: number) {
|
|
|
697
734
|
function openLinkModal(selection: Selection, range: Range, existingLink: HTMLAnchorElement | null) {
|
|
698
735
|
pendingLinkData = { selection, range, existingLink }
|
|
699
736
|
linkForm.value = {
|
|
700
|
-
url: existingLink?.
|
|
737
|
+
url: existingLink?.href || '',
|
|
701
738
|
title: existingLink?.getAttribute('title') || '',
|
|
702
739
|
openInNewTab: existingLink?.target !== '_self'
|
|
703
740
|
}
|
|
@@ -706,19 +743,17 @@ function openLinkModal(selection: Selection, range: Range, existingLink: HTMLAnc
|
|
|
706
743
|
|
|
707
744
|
// Function to submit link
|
|
708
745
|
function submitLink() {
|
|
709
|
-
|
|
710
|
-
if (!pendingLinkData || !normalizedUrl) { return }
|
|
746
|
+
if (!pendingLinkData || !linkForm.value.url) { return }
|
|
711
747
|
|
|
712
748
|
const { selection, range, existingLink } = pendingLinkData
|
|
713
|
-
const { title, openInNewTab } = linkForm.value
|
|
749
|
+
const { url, title, openInNewTab } = linkForm.value
|
|
714
750
|
const { doc } = editor.state
|
|
715
751
|
|
|
716
752
|
if (!doc) { return }
|
|
717
|
-
linkForm.value.url = normalizedUrl
|
|
718
753
|
|
|
719
754
|
if (existingLink) {
|
|
720
755
|
// Update existing link
|
|
721
|
-
existingLink.
|
|
756
|
+
existingLink.href = url
|
|
722
757
|
if (title.trim()) {
|
|
723
758
|
existingLink.title = title.trim()
|
|
724
759
|
} else {
|
|
@@ -734,7 +769,7 @@ function submitLink() {
|
|
|
734
769
|
// Create new link
|
|
735
770
|
try {
|
|
736
771
|
const anchor = doc.createElement('a')
|
|
737
|
-
anchor.
|
|
772
|
+
anchor.href = url
|
|
738
773
|
if (title.trim()) {
|
|
739
774
|
anchor.title = title.trim()
|
|
740
775
|
}
|
|
@@ -769,8 +804,14 @@ function submitLink() {
|
|
|
769
804
|
|
|
770
805
|
// Function to visit link
|
|
771
806
|
function visitLink() {
|
|
772
|
-
|
|
773
|
-
|
|
807
|
+
if (!linkForm.value.url || !isValidUrl(linkForm.value.url)) { return }
|
|
808
|
+
|
|
809
|
+
let url = linkForm.value.url.trim()
|
|
810
|
+
|
|
811
|
+
// Add https:// if no protocol is specified
|
|
812
|
+
if (!url.match(/^https?:\/\//)) {
|
|
813
|
+
url = `https://${url}`
|
|
814
|
+
}
|
|
774
815
|
|
|
775
816
|
if (linkForm.value.openInNewTab) {
|
|
776
817
|
window.open(url, '_blank', 'noopener,noreferrer')
|
|
@@ -781,13 +822,27 @@ function visitLink() {
|
|
|
781
822
|
|
|
782
823
|
// Function to validate URL
|
|
783
824
|
function isValidUrl(url: string): boolean {
|
|
784
|
-
|
|
825
|
+
if (!url || url.trim() === '') { return false }
|
|
826
|
+
|
|
827
|
+
try {
|
|
828
|
+
URL.canParse(url)
|
|
829
|
+
return true
|
|
830
|
+
} catch {
|
|
831
|
+
// Try with https:// prefix if it's missing
|
|
832
|
+
try {
|
|
833
|
+
URL.canParse(`https://${url}`)
|
|
834
|
+
return true
|
|
835
|
+
} catch {
|
|
836
|
+
return false
|
|
837
|
+
}
|
|
838
|
+
}
|
|
785
839
|
}
|
|
786
840
|
|
|
787
841
|
// Function to open image modal
|
|
788
842
|
function openImageModal(existingImage: HTMLElement | null = null) {
|
|
789
843
|
const { doc } = editor.state
|
|
790
844
|
if (!doc) { return }
|
|
845
|
+
imageUploadError.value = ''
|
|
791
846
|
|
|
792
847
|
// Get current selection for new images
|
|
793
848
|
if (!existingImage) {
|
|
@@ -805,51 +860,18 @@ function openImageModal(existingImage: HTMLElement | null = null) {
|
|
|
805
860
|
}
|
|
806
861
|
|
|
807
862
|
if (existingImage) {
|
|
808
|
-
|
|
809
|
-
const img = existingImage.tagName === 'IMG'
|
|
810
|
-
? existingImage as HTMLImageElement
|
|
811
|
-
: existingImage.querySelector('img')
|
|
812
|
-
const figcaption = existingImage.querySelector('figcaption')
|
|
813
|
-
const credit = existingImage.getAttribute('data-credit') || img?.getAttribute('data-credit') || ''
|
|
814
|
-
|
|
815
|
-
// Read alignment from the figure (or bare img) data-align attribute
|
|
816
|
-
const alignSource = existingImage.tagName === 'IMG' ? img : existingImage
|
|
817
|
-
const align = alignSource?.getAttribute('data-align') || 'full'
|
|
818
|
-
|
|
819
|
-
// Read link from the anchor wrapping the image (if any)
|
|
820
|
-
const anchor = img?.closest('a')
|
|
821
|
-
const link = anchor?.getAttribute('href') || ''
|
|
822
|
-
const linkNewTab = anchor?.getAttribute('target') === '_blank'
|
|
823
|
-
|
|
824
|
-
imageForm.value = {
|
|
825
|
-
src: img?.src || '',
|
|
826
|
-
alt: img?.alt || '',
|
|
827
|
-
width: img?.getAttribute('data-width') || img?.style.width || '',
|
|
828
|
-
height: img?.getAttribute('data-height') || img?.style.height || '',
|
|
829
|
-
credit,
|
|
830
|
-
figcaption: !!figcaption,
|
|
831
|
-
align,
|
|
832
|
-
link,
|
|
833
|
-
linkNewTab
|
|
834
|
-
}
|
|
863
|
+
imageForm.value = readImageData(existingImage)
|
|
835
864
|
} else {
|
|
836
|
-
|
|
837
|
-
imageForm.value = {
|
|
838
|
-
src: '',
|
|
839
|
-
alt: '',
|
|
840
|
-
width: '',
|
|
841
|
-
height: '',
|
|
842
|
-
credit: '',
|
|
843
|
-
figcaption: false,
|
|
844
|
-
align: 'full',
|
|
845
|
-
link: '',
|
|
846
|
-
linkNewTab: false
|
|
847
|
-
}
|
|
865
|
+
imageForm.value = createDefaultImageData()
|
|
848
866
|
}
|
|
849
867
|
|
|
850
868
|
showImageModal.value = true
|
|
851
869
|
}
|
|
852
870
|
|
|
871
|
+
function handleImageUploadError() {
|
|
872
|
+
imageUploadError.value = 'Image upload failed. Please try again or paste an image URL.'
|
|
873
|
+
}
|
|
874
|
+
|
|
853
875
|
// Function to submit image
|
|
854
876
|
function submitImage() {
|
|
855
877
|
if (!imageForm.value.src || !pendingImageData) { return }
|
|
@@ -858,124 +880,9 @@ function submitImage() {
|
|
|
858
880
|
if (!doc) { return }
|
|
859
881
|
|
|
860
882
|
const { existingImage } = pendingImageData
|
|
883
|
+
const elementToUpdate = buildImageFigure(doc, imageForm.value, existingImage)
|
|
861
884
|
|
|
862
|
-
|
|
863
|
-
const img = doc.createElement('img')
|
|
864
|
-
img.src = imageForm.value.src
|
|
865
|
-
img.alt = imageForm.value.alt || ''
|
|
866
|
-
|
|
867
|
-
// Store original width/height values in data attributes
|
|
868
|
-
if (imageForm.value.width) {
|
|
869
|
-
img.setAttribute('data-width', imageForm.value.width)
|
|
870
|
-
if (imageForm.value.width.includes('%') || imageForm.value.width.includes('px') || imageForm.value.width.includes('vw') || imageForm.value.width.includes('rem') || imageForm.value.width.includes('em')) {
|
|
871
|
-
img.style.width = imageForm.value.width
|
|
872
|
-
} else {
|
|
873
|
-
img.style.width = `${imageForm.value.width}px`
|
|
874
|
-
}
|
|
875
|
-
} else {
|
|
876
|
-
// Floated images default to a partial width so text can wrap alongside;
|
|
877
|
-
// full-width images fill the container.
|
|
878
|
-
img.style.width = (imageForm.value.align === 'start' || imageForm.value.align === 'end') ? '45%' : '100%'
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
if (imageForm.value.height) {
|
|
882
|
-
img.setAttribute('data-height', imageForm.value.height)
|
|
883
|
-
if (imageForm.value.height.includes('%') || imageForm.value.height.includes('px') || imageForm.value.height.includes('vh') || imageForm.value.height.includes('rem') || imageForm.value.height.includes('em') || imageForm.value.height === 'auto') {
|
|
884
|
-
img.style.height = imageForm.value.height
|
|
885
|
-
} else {
|
|
886
|
-
img.style.height = `${imageForm.value.height}px`
|
|
887
|
-
}
|
|
888
|
-
} else {
|
|
889
|
-
img.style.height = 'auto'
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
// Helper function to create figcaption with alt text and credit (using safe DOM nodes)
|
|
893
|
-
function createFigcaption() {
|
|
894
|
-
const figcaption = doc!.createElement('figcaption')
|
|
895
|
-
|
|
896
|
-
if (imageForm.value.figcaption && imageForm.value.alt) {
|
|
897
|
-
const altStrong = doc!.createElement('strong')
|
|
898
|
-
altStrong.className = 'alt-text'
|
|
899
|
-
altStrong.textContent = imageForm.value.alt
|
|
900
|
-
figcaption.appendChild(altStrong)
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
if (imageForm.value.credit) {
|
|
904
|
-
if (figcaption.childNodes.length > 0) {
|
|
905
|
-
figcaption.appendChild(doc!.createTextNode(' • '))
|
|
906
|
-
}
|
|
907
|
-
const creditSpan = doc!.createElement('span')
|
|
908
|
-
creditSpan.className = 'photo-credit'
|
|
909
|
-
creditSpan.textContent = imageForm.value.credit
|
|
910
|
-
figcaption.appendChild(creditSpan)
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
return figcaption
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
// Wrap the image in an anchor when a link is provided, otherwise return the bare image
|
|
917
|
-
function createImageContent(): HTMLElement {
|
|
918
|
-
const link = normalizeRichTextLink(imageForm.value.link)
|
|
919
|
-
if (!link) { return img }
|
|
920
|
-
|
|
921
|
-
const anchor = doc!.createElement('a')
|
|
922
|
-
anchor.setAttribute('href', link)
|
|
923
|
-
if (imageForm.value.linkNewTab) {
|
|
924
|
-
anchor.setAttribute('target', '_blank')
|
|
925
|
-
anchor.setAttribute('rel', 'noopener noreferrer')
|
|
926
|
-
}
|
|
927
|
-
anchor.appendChild(img)
|
|
928
|
-
return anchor
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
// Create or update figure/image - always wrap in figure
|
|
932
|
-
let elementToUpdate: HTMLElement
|
|
933
|
-
if (existingImage) {
|
|
934
|
-
if (existingImage.tagName.toLowerCase() === 'img') {
|
|
935
|
-
// Converting standalone image to figure (always)
|
|
936
|
-
const figure = doc.createElement('figure')
|
|
937
|
-
figure.className = 'image-figure'
|
|
938
|
-
if (imageForm.value.credit) {
|
|
939
|
-
figure.setAttribute('data-credit', imageForm.value.credit)
|
|
940
|
-
}
|
|
941
|
-
figure.appendChild(createImageContent())
|
|
942
|
-
|
|
943
|
-
// Add caption if needed (alt with checkbox checked OR credit exists)
|
|
944
|
-
if ((imageForm.value.figcaption && imageForm.value.alt) || imageForm.value.credit) {
|
|
945
|
-
figure.appendChild(createFigcaption())
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
existingImage.parentNode?.replaceChild(figure, existingImage)
|
|
949
|
-
elementToUpdate = figure
|
|
950
|
-
} else {
|
|
951
|
-
// Updating an existing figure
|
|
952
|
-
existingImage.innerHTML = '' // Clear existing content
|
|
953
|
-
if (imageForm.value.credit) {
|
|
954
|
-
existingImage.setAttribute('data-credit', imageForm.value.credit)
|
|
955
|
-
}
|
|
956
|
-
existingImage.appendChild(createImageContent())
|
|
957
|
-
|
|
958
|
-
// Add caption if needed (alt with checkbox checked OR credit exists)
|
|
959
|
-
if ((imageForm.value.figcaption && imageForm.value.alt) || imageForm.value.credit) {
|
|
960
|
-
existingImage.appendChild(createFigcaption())
|
|
961
|
-
}
|
|
962
|
-
elementToUpdate = existingImage
|
|
963
|
-
}
|
|
964
|
-
} else {
|
|
965
|
-
// Creating new image - always wrap in figure
|
|
966
|
-
const figure = doc.createElement('figure')
|
|
967
|
-
figure.className = 'image-figure'
|
|
968
|
-
if (imageForm.value.credit) {
|
|
969
|
-
figure.setAttribute('data-credit', imageForm.value.credit)
|
|
970
|
-
}
|
|
971
|
-
figure.appendChild(createImageContent())
|
|
972
|
-
|
|
973
|
-
// Add caption if needed (alt with checkbox checked OR credit exists)
|
|
974
|
-
if ((imageForm.value.figcaption && imageForm.value.alt) || imageForm.value.credit) {
|
|
975
|
-
figure.appendChild(createFigcaption())
|
|
976
|
-
}
|
|
977
|
-
elementToUpdate = figure
|
|
978
|
-
|
|
885
|
+
if (!existingImage) {
|
|
979
886
|
// Insert new element
|
|
980
887
|
const { selection, range } = pendingImageData
|
|
981
888
|
range.collapse(false)
|
|
@@ -988,12 +895,6 @@ function submitImage() {
|
|
|
988
895
|
selection.addRange(range)
|
|
989
896
|
}
|
|
990
897
|
|
|
991
|
-
// Apply alignment to the figure wrapper (full = full width, start/end = float)
|
|
992
|
-
const align = imageForm.value.align || 'full'
|
|
993
|
-
elementToUpdate.classList.remove('align-full', 'align-start', 'align-end')
|
|
994
|
-
elementToUpdate.classList.add(`align-${align}`)
|
|
995
|
-
elementToUpdate.setAttribute('data-align', align)
|
|
996
|
-
|
|
997
898
|
updateContentWithHistory(doc)
|
|
998
899
|
showImageModal.value = false
|
|
999
900
|
pendingImageData = null
|
|
@@ -1091,6 +992,7 @@ function submitEmbed() {
|
|
|
1091
992
|
iframe.frameBorder = '0'
|
|
1092
993
|
iframe.setAttribute('allowfullscreen', '')
|
|
1093
994
|
iframe.setAttribute('data-media-type', 'embed')
|
|
995
|
+
iframe.title = embedForm.value.alt || 'Embedded content'
|
|
1094
996
|
iframe.className = 'embed-iframe'
|
|
1095
997
|
|
|
1096
998
|
// Set width and height - always set them
|
|
@@ -1163,43 +1065,9 @@ function openVideoModal(existingVideo: HTMLElement | null = null) {
|
|
|
1163
1065
|
}
|
|
1164
1066
|
|
|
1165
1067
|
if (existingVideo) {
|
|
1166
|
-
|
|
1167
|
-
const container = existingVideo.querySelector('.video-container')
|
|
1168
|
-
const bareVideo = existingVideo.tagName === 'VIDEO' ? existingVideo as HTMLVideoElement : null
|
|
1169
|
-
const bareIframe = existingVideo.tagName === 'IFRAME' ? existingVideo as HTMLIFrameElement : null
|
|
1170
|
-
const isCustom = container?.getAttribute('data-custom-aspect-ratio') === 'true'
|
|
1171
|
-
|
|
1172
|
-
videoForm.value = {
|
|
1173
|
-
src: container?.getAttribute('data-video-src')
|
|
1174
|
-
|| bareVideo?.src
|
|
1175
|
-
|| bareIframe?.src
|
|
1176
|
-
|| '',
|
|
1177
|
-
width: container?.getAttribute('data-width') || (bareVideo?.style.width ?? '') || '',
|
|
1178
|
-
autoplay: container ? container.getAttribute('data-autoplay') === 'true' : (bareVideo?.autoplay ?? false),
|
|
1179
|
-
mute: container ? container.getAttribute('data-mute') === 'true' : (bareVideo?.muted ?? false),
|
|
1180
|
-
controls: container ? container.getAttribute('data-controls') === 'true' : (bareVideo?.controls ?? true),
|
|
1181
|
-
loop: container ? container.getAttribute('data-loop') === 'true' : (bareVideo?.loop ?? false),
|
|
1182
|
-
aspectRatio: isCustom ? 'custom' : (container?.getAttribute('data-aspect-ratio') || '16:9'),
|
|
1183
|
-
customWidth: container?.getAttribute('data-custom-width') || '',
|
|
1184
|
-
customHeight: container?.getAttribute('data-custom-height') || '',
|
|
1185
|
-
caption: existingVideo.querySelector?.('figcaption')?.textContent || '',
|
|
1186
|
-
showCaption: !!existingVideo.querySelector?.('figcaption')
|
|
1187
|
-
}
|
|
1068
|
+
videoForm.value = readVideoData(existingVideo)
|
|
1188
1069
|
} else {
|
|
1189
|
-
|
|
1190
|
-
videoForm.value = {
|
|
1191
|
-
src: '',
|
|
1192
|
-
width: '',
|
|
1193
|
-
autoplay: false,
|
|
1194
|
-
mute: false,
|
|
1195
|
-
controls: true,
|
|
1196
|
-
loop: false,
|
|
1197
|
-
aspectRatio: '16:9',
|
|
1198
|
-
customWidth: '',
|
|
1199
|
-
customHeight: '',
|
|
1200
|
-
caption: '',
|
|
1201
|
-
showCaption: false
|
|
1202
|
-
}
|
|
1070
|
+
videoForm.value = createDefaultVideoData()
|
|
1203
1071
|
}
|
|
1204
1072
|
|
|
1205
1073
|
showVideoModal.value = true
|
|
@@ -1248,133 +1116,7 @@ function submitVideo() {
|
|
|
1248
1116
|
if (!doc) { return }
|
|
1249
1117
|
|
|
1250
1118
|
const { existingVideo } = pendingVideoData
|
|
1251
|
-
|
|
1252
|
-
// Calculate aspect ratio
|
|
1253
|
-
let { aspectRatio } = videoForm.value
|
|
1254
|
-
const isCustom = videoForm.value.aspectRatio === 'custom'
|
|
1255
|
-
if (isCustom && videoForm.value.customWidth && videoForm.value.customHeight) {
|
|
1256
|
-
aspectRatio = `${videoForm.value.customWidth}:${videoForm.value.customHeight}`
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
// Auto-adjust aspect ratio for YouTube Shorts
|
|
1260
|
-
const isYoutubeShort = videoForm.value.src.includes('youtube.com/shorts/')
|
|
1261
|
-
if (isYoutubeShort && aspectRatio === '16:9') {
|
|
1262
|
-
aspectRatio = '9:16'
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
// Create video container
|
|
1266
|
-
const videoContainer = doc.createElement('div')
|
|
1267
|
-
videoContainer.className = 'video-container'
|
|
1268
|
-
videoContainer.setAttribute('data-video-src', videoForm.value.src)
|
|
1269
|
-
videoContainer.setAttribute('data-component', 'BglVideo')
|
|
1270
|
-
videoContainer.setAttribute('data-media-type', 'video')
|
|
1271
|
-
videoContainer.setAttribute('data-width', videoForm.value.width)
|
|
1272
|
-
videoContainer.setAttribute('data-autoplay', videoForm.value.autoplay.toString())
|
|
1273
|
-
videoContainer.setAttribute('data-mute', videoForm.value.mute.toString())
|
|
1274
|
-
videoContainer.setAttribute('data-controls', videoForm.value.controls.toString())
|
|
1275
|
-
videoContainer.setAttribute('data-loop', videoForm.value.loop.toString())
|
|
1276
|
-
videoContainer.setAttribute('data-aspect-ratio', aspectRatio)
|
|
1277
|
-
videoContainer.setAttribute('data-custom-aspect-ratio', isCustom.toString())
|
|
1278
|
-
videoContainer.setAttribute('data-custom-width', videoForm.value.customWidth)
|
|
1279
|
-
videoContainer.setAttribute('data-custom-height', videoForm.value.customHeight)
|
|
1280
|
-
|
|
1281
|
-
// Set width
|
|
1282
|
-
if (videoForm.value.width) {
|
|
1283
|
-
videoContainer.style.width = videoForm.value.width.includes('%') || videoForm.value.width.includes('px')
|
|
1284
|
-
? videoForm.value.width
|
|
1285
|
-
: `${videoForm.value.width}px`
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
// Create the video placeholder/thumbnail instead of actual video
|
|
1289
|
-
const videoWrapper = doc.createElement('div')
|
|
1290
|
-
videoWrapper.className = 'bgl_vid'
|
|
1291
|
-
|
|
1292
|
-
// Create video placeholder/thumbnail
|
|
1293
|
-
const placeholder = doc.createElement('div')
|
|
1294
|
-
placeholder.className = 'video-placeholder'
|
|
1295
|
-
placeholder.style.aspectRatio = aspectRatio.replace(':', '/')
|
|
1296
|
-
|
|
1297
|
-
// Detect video type for display
|
|
1298
|
-
const isYoutube = videoForm.value.src.includes('youtube.com') || videoForm.value.src.includes('youtu.be')
|
|
1299
|
-
const isVimeo = videoForm.value.src.includes('vimeo.com')
|
|
1300
|
-
|
|
1301
|
-
// Create play icon
|
|
1302
|
-
const playIcon = doc.createElement('div')
|
|
1303
|
-
playIcon.className = 'video-placeholder-icon'
|
|
1304
|
-
|
|
1305
|
-
// Create text description
|
|
1306
|
-
const description = doc.createElement('div')
|
|
1307
|
-
description.className = 'video-placeholder-description'
|
|
1308
|
-
let platformText = 'video'
|
|
1309
|
-
if (isYoutube) { platformText = 'YouTube video' }
|
|
1310
|
-
else if (isVimeo) { platformText = 'Vimeo video' }
|
|
1311
|
-
description.textContent = platformText
|
|
1312
|
-
|
|
1313
|
-
// Try to get YouTube thumbnail if possible
|
|
1314
|
-
if (isYoutube) {
|
|
1315
|
-
let videoId = ''
|
|
1316
|
-
if (videoForm.value.src.includes('youtube.com/watch?v=')) {
|
|
1317
|
-
videoId = videoForm.value.src.split('watch?v=')[1]?.split('&')[0]
|
|
1318
|
-
} else if (videoForm.value.src.includes('youtu.be/')) {
|
|
1319
|
-
videoId = videoForm.value.src.split('youtu.be/')[1]?.split('?')[0]
|
|
1320
|
-
} else if (videoForm.value.src.includes('youtube.com/shorts/')) {
|
|
1321
|
-
videoId = videoForm.value.src.split('shorts/')[1]?.split('?')[0]
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
if (videoId) {
|
|
1325
|
-
// Add thumbnail class and create thumbnail image
|
|
1326
|
-
placeholder.classList.add('has-thumbnail')
|
|
1327
|
-
|
|
1328
|
-
const thumbnail = doc.createElement('img')
|
|
1329
|
-
thumbnail.className = 'video-thumbnail'
|
|
1330
|
-
thumbnail.src = `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`
|
|
1331
|
-
placeholder.appendChild(thumbnail)
|
|
1332
|
-
|
|
1333
|
-
// Add overlay with play button
|
|
1334
|
-
const overlay = doc.createElement('div')
|
|
1335
|
-
overlay.className = 'video-overlay'
|
|
1336
|
-
|
|
1337
|
-
const overlayPlayIcon = doc.createElement('div')
|
|
1338
|
-
overlayPlayIcon.className = 'video-overlay-icon'
|
|
1339
|
-
|
|
1340
|
-
overlay.appendChild(overlayPlayIcon)
|
|
1341
|
-
placeholder.appendChild(overlay)
|
|
1342
|
-
} else {
|
|
1343
|
-
placeholder.appendChild(playIcon)
|
|
1344
|
-
placeholder.appendChild(description)
|
|
1345
|
-
}
|
|
1346
|
-
} else {
|
|
1347
|
-
placeholder.appendChild(playIcon)
|
|
1348
|
-
placeholder.appendChild(description)
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
// Add special class for YouTube Shorts
|
|
1352
|
-
if (isYoutube && videoForm.value.src.includes('youtube.com/shorts/')) {
|
|
1353
|
-
videoWrapper.classList.add('vid_short')
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
videoWrapper.appendChild(placeholder)
|
|
1357
|
-
|
|
1358
|
-
videoContainer.appendChild(videoWrapper)
|
|
1359
|
-
|
|
1360
|
-
// Create or update figure
|
|
1361
|
-
let figure: HTMLElement
|
|
1362
|
-
if (existingVideo) {
|
|
1363
|
-
figure = existingVideo
|
|
1364
|
-
figure.innerHTML = '' // Clear existing content
|
|
1365
|
-
} else {
|
|
1366
|
-
figure = doc.createElement('figure')
|
|
1367
|
-
figure.className = 'video-figure'
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
figure.appendChild(videoContainer)
|
|
1371
|
-
|
|
1372
|
-
// Add caption if enabled
|
|
1373
|
-
if (videoForm.value.showCaption && videoForm.value.caption) {
|
|
1374
|
-
const figcaption = doc.createElement('figcaption')
|
|
1375
|
-
figcaption.textContent = videoForm.value.caption
|
|
1376
|
-
figure.appendChild(figcaption)
|
|
1377
|
-
}
|
|
1119
|
+
const figure = buildVideoFigure(doc, videoForm.value, existingVideo)
|
|
1378
1120
|
|
|
1379
1121
|
// Insert new video or update existing
|
|
1380
1122
|
if (!existingVideo) {
|
|
@@ -1809,6 +1551,7 @@ function deleteTable() {
|
|
|
1809
1551
|
if (!pendingTableData?.existingTable) { return }
|
|
1810
1552
|
|
|
1811
1553
|
removeTableElement(pendingTableData.existingTable)
|
|
1554
|
+
updateContentWithHistory(editor.state.doc!)
|
|
1812
1555
|
showTableEditor.value = false
|
|
1813
1556
|
pendingTableData = null
|
|
1814
1557
|
}
|
|
@@ -1867,6 +1610,11 @@ function getCleanHTML(doc: Document | null): string {
|
|
|
1867
1610
|
clone.querySelectorAll('.media-selected').forEach((el) => { el.classList.remove('media-selected') })
|
|
1868
1611
|
clone.querySelectorAll('.table-cell-selected').forEach((el) => { el.classList.remove('table-cell-selected') })
|
|
1869
1612
|
clone.querySelectorAll('[data-dir-explicit]').forEach((el) => { el.removeAttribute('data-dir-explicit') })
|
|
1613
|
+
clone.querySelectorAll('figure[data-media-type]').forEach((figure) => {
|
|
1614
|
+
figure.removeAttribute('tabindex')
|
|
1615
|
+
figure.removeAttribute('role')
|
|
1616
|
+
figure.removeAttribute('aria-label')
|
|
1617
|
+
})
|
|
1870
1618
|
|
|
1871
1619
|
// Unwrap .table-wrapper divs: restore original table margin-based alignment
|
|
1872
1620
|
clone.querySelectorAll('.table-wrapper').forEach((wrapper) => {
|
|
@@ -1899,79 +1647,7 @@ function getCleanHTML(doc: Document | null): string {
|
|
|
1899
1647
|
table.removeAttribute('data-edit-button-added')
|
|
1900
1648
|
})
|
|
1901
1649
|
|
|
1902
|
-
|
|
1903
|
-
clone.querySelectorAll('.video-container[data-component="BglVideo"]').forEach((container) => {
|
|
1904
|
-
const el = container as HTMLElement
|
|
1905
|
-
const src = el.getAttribute('data-video-src') || ''
|
|
1906
|
-
if (!src) { return }
|
|
1907
|
-
|
|
1908
|
-
const autoplay = el.getAttribute('data-autoplay') === 'true'
|
|
1909
|
-
const muted = el.getAttribute('data-mute') === 'true'
|
|
1910
|
-
const controls = el.getAttribute('data-controls') !== 'false'
|
|
1911
|
-
const loop = el.getAttribute('data-loop') === 'true'
|
|
1912
|
-
const aspectRatio = el.getAttribute('data-aspect-ratio') || '16:9'
|
|
1913
|
-
const width = el.getAttribute('data-width') || ''
|
|
1914
|
-
|
|
1915
|
-
const widthStyle = width
|
|
1916
|
-
? `width: ${/[%pxvwemr]/.test(width) ? width : `${width}px`};`
|
|
1917
|
-
: 'width: 100%;'
|
|
1918
|
-
|
|
1919
|
-
const [ratioW, ratioH] = aspectRatio.split(':').map(Number)
|
|
1920
|
-
const paddingBottom = ratioW && ratioH ? `${(ratioH / ratioW) * 100}%` : '56.25%'
|
|
1921
|
-
|
|
1922
|
-
const makeResponsiveIframe = (iframeSrc: string): HTMLElement => {
|
|
1923
|
-
const wrapper = document.createElement('div')
|
|
1924
|
-
wrapper.style.cssText = `${widthStyle} position: relative; padding-bottom: ${paddingBottom}; height: 0; overflow: hidden;`
|
|
1925
|
-
const iframe = document.createElement('iframe')
|
|
1926
|
-
iframe.src = iframeSrc
|
|
1927
|
-
iframe.frameBorder = '0'
|
|
1928
|
-
iframe.setAttribute('allowfullscreen', '')
|
|
1929
|
-
iframe.style.cssText = 'position: absolute; top: 0; left: 0; width: 100%; height: 100%;'
|
|
1930
|
-
wrapper.appendChild(iframe)
|
|
1931
|
-
return wrapper
|
|
1932
|
-
}
|
|
1933
|
-
|
|
1934
|
-
let newEl: HTMLElement | null = null
|
|
1935
|
-
|
|
1936
|
-
if (src.includes('youtube.com') || src.includes('youtu.be')) {
|
|
1937
|
-
let embedUrl = src
|
|
1938
|
-
if (src.includes('youtube.com/watch?v=')) {
|
|
1939
|
-
const videoId = src.split('watch?v=')[1]?.split('&')[0]
|
|
1940
|
-
const params = []
|
|
1941
|
-
if (autoplay) { params.push('autoplay=1') }
|
|
1942
|
-
if (muted) { params.push('mute=1') }
|
|
1943
|
-
if (loop) { params.push(`loop=1&playlist=${videoId}`) }
|
|
1944
|
-
if (!controls) { params.push('controls=0') }
|
|
1945
|
-
embedUrl = `https://www.youtube.com/embed/${videoId}${params.length ? `?${params.join('&')}` : ''}`
|
|
1946
|
-
} else if (src.includes('youtu.be/')) {
|
|
1947
|
-
const videoId = src.split('youtu.be/')[1]?.split('?')[0]
|
|
1948
|
-
embedUrl = `https://www.youtube.com/embed/${videoId}`
|
|
1949
|
-
} else if (src.includes('youtube.com/shorts/')) {
|
|
1950
|
-
const videoId = src.split('shorts/')[1]?.split('?')[0]
|
|
1951
|
-
embedUrl = `https://www.youtube.com/embed/${videoId}`
|
|
1952
|
-
}
|
|
1953
|
-
newEl = makeResponsiveIframe(embedUrl)
|
|
1954
|
-
} else if (src.includes('vimeo.com')) {
|
|
1955
|
-
const match = src.match(/vimeo\.com\/(\d+)/)
|
|
1956
|
-
const embedUrl = match ? `https://player.vimeo.com/video/${match[1]}` : src
|
|
1957
|
-
newEl = makeResponsiveIframe(embedUrl)
|
|
1958
|
-
} else if (src.includes('<iframe')) {
|
|
1959
|
-
const srcMatch = src.match(/src=["']([^"']+)["']/)
|
|
1960
|
-
if (srcMatch) { newEl = makeResponsiveIframe(srcMatch[1]) }
|
|
1961
|
-
} else {
|
|
1962
|
-
// Direct video file
|
|
1963
|
-
const video = document.createElement('video')
|
|
1964
|
-
video.src = src
|
|
1965
|
-
if (controls) { video.setAttribute('controls', '') }
|
|
1966
|
-
if (autoplay) { video.setAttribute('autoplay', '') }
|
|
1967
|
-
if (muted) { video.setAttribute('muted', '') }
|
|
1968
|
-
if (loop) { video.setAttribute('loop', '') }
|
|
1969
|
-
video.style.cssText = `${widthStyle} height: auto; display: block;`
|
|
1970
|
-
newEl = video
|
|
1971
|
-
}
|
|
1972
|
-
|
|
1973
|
-
if (newEl) { el.parentNode?.replaceChild(newEl, el) }
|
|
1974
|
-
})
|
|
1650
|
+
serializeVideoFigures(clone)
|
|
1975
1651
|
|
|
1976
1652
|
// Strip internal placeholder content from video figures (the bgl_vid wrapper)
|
|
1977
1653
|
// that remains after video-container conversion
|
|
@@ -1985,10 +1661,6 @@ function getCleanHTML(doc: Document | null): string {
|
|
|
1985
1661
|
if (fig.classList.length === 0) { fig.removeAttribute('class') }
|
|
1986
1662
|
})
|
|
1987
1663
|
|
|
1988
|
-
// Keep imported/pasted legacy links from leaking into saved output. Invalid
|
|
1989
|
-
// destinations lose their href, but their visible content is preserved.
|
|
1990
|
-
sanitizeRichTextLinks(clone)
|
|
1991
|
-
|
|
1992
1664
|
const html = clone.innerHTML
|
|
1993
1665
|
if (!clone.textContent?.trim() && !clone.querySelector('img, video, iframe, table, hr, figure')) {
|
|
1994
1666
|
return ''
|
|
@@ -2188,11 +1860,12 @@ function setupTableColumnResizing(doc: Document) {
|
|
|
2188
1860
|
table: HTMLTableElement
|
|
2189
1861
|
cell: HTMLTableCellElement
|
|
2190
1862
|
insertPosition: 'before' | 'after'
|
|
1863
|
+
rowInsertPosition: 'before' | 'after'
|
|
2191
1864
|
} | null = null
|
|
2192
1865
|
|
|
2193
1866
|
const hideAllInlineControls = () => {
|
|
2194
1867
|
doc.querySelectorAll<HTMLElement>('.table-inline-controls').forEach((ctrl) => {
|
|
2195
|
-
ctrl.classList.remove('is-visible', 'show-col-add', 'show-col-thumb', 'is-dragging-col')
|
|
1868
|
+
ctrl.classList.remove('is-visible', 'show-col-add', 'show-row-add', 'show-col-thumb', 'is-dragging-col')
|
|
2196
1869
|
})
|
|
2197
1870
|
}
|
|
2198
1871
|
|
|
@@ -2257,7 +1930,8 @@ function setupTableColumnResizing(doc: Document) {
|
|
|
2257
1930
|
const cell = target.closest('td, th') as HTMLTableCellElement | null
|
|
2258
1931
|
const table = cell?.closest('table') as HTMLTableElement | null
|
|
2259
1932
|
const wrapper = table?.closest('.table-wrapper') as HTMLElement | null
|
|
2260
|
-
|
|
1933
|
+
const row = cell?.closest('tr') as HTMLTableRowElement | null
|
|
1934
|
+
if (!cell || !table || !wrapper || !row) {
|
|
2261
1935
|
hideAllInlineControls()
|
|
2262
1936
|
return
|
|
2263
1937
|
}
|
|
@@ -2265,12 +1939,14 @@ function setupTableColumnResizing(doc: Document) {
|
|
|
2265
1939
|
const controls = wrapper.querySelector<HTMLElement>('.table-inline-controls')
|
|
2266
1940
|
const addBtn = controls?.querySelector<HTMLElement>('.table-col-add-btn')
|
|
2267
1941
|
const addLine = controls?.querySelector<HTMLElement>('.table-col-add-line')
|
|
1942
|
+
const rowAddBtn = controls?.querySelector<HTMLElement>('.table-row-add-btn')
|
|
1943
|
+
const rowAddLine = controls?.querySelector<HTMLElement>('.table-row-add-line')
|
|
2268
1944
|
const thumbBtn = controls?.querySelector<HTMLElement>('.table-col-thumb')
|
|
2269
|
-
if (!controls || !addBtn || !addLine || !thumbBtn) { return }
|
|
1945
|
+
if (!controls || !addBtn || !addLine || !rowAddBtn || !rowAddLine || !thumbBtn) { return }
|
|
2270
1946
|
|
|
2271
1947
|
doc.querySelectorAll<HTMLElement>('.table-inline-controls').forEach((other) => {
|
|
2272
1948
|
if (other !== controls) {
|
|
2273
|
-
other.classList.remove('is-visible', 'show-col-add', 'show-col-thumb', 'is-dragging-col')
|
|
1949
|
+
other.classList.remove('is-visible', 'show-col-add', 'show-row-add', 'show-col-thumb', 'is-dragging-col')
|
|
2274
1950
|
}
|
|
2275
1951
|
})
|
|
2276
1952
|
|
|
@@ -2280,6 +1956,8 @@ function setupTableColumnResizing(doc: Document) {
|
|
|
2280
1956
|
|
|
2281
1957
|
const tableTop = tableRect.top - wrapperRect.top
|
|
2282
1958
|
const tableHeight = tableRect.height
|
|
1959
|
+
const tableLeft = tableRect.left - wrapperRect.left
|
|
1960
|
+
const tableWidth = tableRect.width
|
|
2283
1961
|
const cellLeft = cellRect.left - wrapperRect.left
|
|
2284
1962
|
const cellRight = cellRect.right - wrapperRect.left
|
|
2285
1963
|
const cellCenter = cellLeft + cellRect.width / 2
|
|
@@ -2290,18 +1968,29 @@ function setupTableColumnResizing(doc: Document) {
|
|
|
2290
1968
|
const distRight = Math.abs(e.clientX - cellRect.right)
|
|
2291
1969
|
const nearLeft = distLeft <= distRight
|
|
2292
1970
|
const minEdgeDist = Math.min(distLeft, distRight)
|
|
1971
|
+
const distTop = Math.abs(e.clientY - cellRect.top)
|
|
1972
|
+
const distBottom = Math.abs(e.clientY - cellRect.bottom)
|
|
1973
|
+
const nearTop = distTop <= distBottom
|
|
1974
|
+
const minRowEdgeDist = Math.min(distTop, distBottom)
|
|
2293
1975
|
const nearTopBand = (e.clientY - tableRect.top) <= 36
|
|
1976
|
+
const nearInlineStartBand = isRTL
|
|
1977
|
+
? (tableRect.right - e.clientX) <= 36
|
|
1978
|
+
: (e.clientX - tableRect.left) <= 36
|
|
1979
|
+
const isHeaderRow = row.parentElement?.tagName === 'THEAD'
|
|
2294
1980
|
|
|
2295
1981
|
// In LTR: left edge inserts 'before', right edge inserts 'after'. In RTL: reversed.
|
|
2296
1982
|
const insertPosition: 'before' | 'after' = nearLeft
|
|
2297
1983
|
? (isRTL ? 'after' : 'before')
|
|
2298
1984
|
: (isRTL ? 'before' : 'after')
|
|
1985
|
+
const rowInsertPosition: 'before' | 'after' = isHeaderRow ? 'after' : (nearTop ? 'before' : 'after')
|
|
2299
1986
|
const boundaryX = nearLeft ? cellLeft : cellRight
|
|
1987
|
+
const boundaryY = (isHeaderRow ? row.getBoundingClientRect().bottom : (nearTop ? cellRect.top : cellRect.bottom)) - wrapperRect.top
|
|
2300
1988
|
|
|
2301
1989
|
activeHoverState = {
|
|
2302
1990
|
table,
|
|
2303
1991
|
cell,
|
|
2304
1992
|
insertPosition,
|
|
1993
|
+
rowInsertPosition,
|
|
2305
1994
|
}
|
|
2306
1995
|
|
|
2307
1996
|
controls.classList.add('is-visible', 'show-col-thumb')
|
|
@@ -2319,6 +2008,18 @@ function setupTableColumnResizing(doc: Document) {
|
|
|
2319
2008
|
} else {
|
|
2320
2009
|
controls.classList.remove('show-col-add')
|
|
2321
2010
|
}
|
|
2011
|
+
|
|
2012
|
+
// Mirror the column affordance on horizontal row boundaries.
|
|
2013
|
+
if (minRowEdgeDist <= ADD_TRIGGER_ZONE || nearInlineStartBand) {
|
|
2014
|
+
rowAddBtn.style.left = `${isRTL ? tableLeft + tableWidth : tableLeft}px`
|
|
2015
|
+
rowAddBtn.style.top = `${boundaryY}px`
|
|
2016
|
+
rowAddLine.style.left = `${tableLeft}px`
|
|
2017
|
+
rowAddLine.style.top = `${boundaryY}px`
|
|
2018
|
+
rowAddLine.style.width = `${tableWidth}px`
|
|
2019
|
+
controls.classList.add('show-row-add')
|
|
2020
|
+
} else {
|
|
2021
|
+
controls.classList.remove('show-row-add')
|
|
2022
|
+
}
|
|
2322
2023
|
}
|
|
2323
2024
|
|
|
2324
2025
|
const onHoverPointerMove = (e: PointerEvent) => {
|
|
@@ -2336,7 +2037,32 @@ function setupTableColumnResizing(doc: Document) {
|
|
|
2336
2037
|
const onPointerDown = (e: PointerEvent) => {
|
|
2337
2038
|
const target = e.target as HTMLElement | null
|
|
2338
2039
|
|
|
2339
|
-
// 1. Handle native inline '+' (Add
|
|
2040
|
+
// 1. Handle native inline '+' (Add Row) click
|
|
2041
|
+
const addRowBtn = target?.closest('.table-row-add-btn') as HTMLElement | null
|
|
2042
|
+
if (addRowBtn) {
|
|
2043
|
+
e.preventDefault()
|
|
2044
|
+
e.stopPropagation()
|
|
2045
|
+
e.stopImmediatePropagation()
|
|
2046
|
+
const wrapper = addRowBtn.closest('.table-wrapper')
|
|
2047
|
+
const table = wrapper?.querySelector('table') as HTMLTableElement | null
|
|
2048
|
+
const fallbackCell = table?.querySelector('tbody td, td, th') as HTMLTableCellElement | null
|
|
2049
|
+
const targetCell = (activeHoverState?.table === table && activeHoverState?.cell.isConnected)
|
|
2050
|
+
? activeHoverState.cell
|
|
2051
|
+
: (contextMenuCell.value?.closest('table') === table ? contextMenuCell.value : fallbackCell)
|
|
2052
|
+
const pos = activeHoverState?.table === table
|
|
2053
|
+
? activeHoverState.rowInsertPosition
|
|
2054
|
+
: ((addRowBtn.getAttribute('data-insert-pos') as 'before' | 'after') || 'after')
|
|
2055
|
+
|
|
2056
|
+
if (targetCell) {
|
|
2057
|
+
const newRow = tableAddRow(pos, editor.state.range, doc, targetCell)
|
|
2058
|
+
const firstNewCell = newRow?.querySelector('td, th') as HTMLTableCellElement | null
|
|
2059
|
+
if (firstNewCell) { focusTableCell(firstNewCell, true) }
|
|
2060
|
+
updateContentWithHistory(doc)
|
|
2061
|
+
}
|
|
2062
|
+
return
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// 2. Handle native inline '+' (Add Column) click
|
|
2340
2066
|
const addColBtn = target?.closest('.table-col-add-btn') as HTMLElement | null
|
|
2341
2067
|
if (addColBtn) {
|
|
2342
2068
|
e.preventDefault()
|
|
@@ -2359,7 +2085,7 @@ function setupTableColumnResizing(doc: Document) {
|
|
|
2359
2085
|
return
|
|
2360
2086
|
}
|
|
2361
2087
|
|
|
2362
|
-
//
|
|
2088
|
+
// 3. Handle native inline '⋯' (Column Thumb: drag via usePointerSession to reorder, or click to open ContextMenu)
|
|
2363
2089
|
const thumbColBtn = target?.closest('.table-col-thumb') as HTMLElement | null
|
|
2364
2090
|
if (thumbColBtn) {
|
|
2365
2091
|
e.stopPropagation()
|
|
@@ -2457,7 +2183,7 @@ function setupTableColumnResizing(doc: Document) {
|
|
|
2457
2183
|
return
|
|
2458
2184
|
}
|
|
2459
2185
|
|
|
2460
|
-
//
|
|
2186
|
+
// 4. Handle column border drag-resizing via usePointerSession (with Escape rollback)
|
|
2461
2187
|
const hovered = getHoveredResizeCell(e)
|
|
2462
2188
|
if (!hovered || !hovered.cell.nextElementSibling) { return }
|
|
2463
2189
|
|
|
@@ -2566,6 +2292,18 @@ function setupTableEditButtons(doc: Document) {
|
|
|
2566
2292
|
addBtn.setAttribute('contenteditable', 'false')
|
|
2567
2293
|
addBtn.setAttribute('tabindex', '-1')
|
|
2568
2294
|
|
|
2295
|
+
const rowAddLine = doc.createElement('span')
|
|
2296
|
+
rowAddLine.className = 'table-row-add-line'
|
|
2297
|
+
rowAddLine.setAttribute('contenteditable', 'false')
|
|
2298
|
+
|
|
2299
|
+
const rowAddBtn = doc.createElement('span')
|
|
2300
|
+
rowAddBtn.className = 'table-row-add-btn'
|
|
2301
|
+
rowAddBtn.textContent = '+'
|
|
2302
|
+
rowAddBtn.title = 'Insert row here'
|
|
2303
|
+
rowAddBtn.setAttribute('aria-label', 'Insert row here')
|
|
2304
|
+
rowAddBtn.setAttribute('contenteditable', 'false')
|
|
2305
|
+
rowAddBtn.setAttribute('tabindex', '-1')
|
|
2306
|
+
|
|
2569
2307
|
const thumbBtn = doc.createElement('span')
|
|
2570
2308
|
thumbBtn.className = 'table-col-thumb'
|
|
2571
2309
|
thumbBtn.textContent = '⋯'
|
|
@@ -2576,6 +2314,8 @@ function setupTableEditButtons(doc: Document) {
|
|
|
2576
2314
|
|
|
2577
2315
|
controls.appendChild(addLine)
|
|
2578
2316
|
controls.appendChild(addBtn)
|
|
2317
|
+
controls.appendChild(rowAddLine)
|
|
2318
|
+
controls.appendChild(rowAddBtn)
|
|
2579
2319
|
controls.appendChild(thumbBtn)
|
|
2580
2320
|
return controls
|
|
2581
2321
|
}
|
|
@@ -2787,6 +2527,24 @@ function setupAutoWrapping(doc: Document) {
|
|
|
2787
2527
|
}
|
|
2788
2528
|
}
|
|
2789
2529
|
|
|
2530
|
+
doc.addEventListener('paste', (event) => {
|
|
2531
|
+
const clipboard = event.clipboardData
|
|
2532
|
+
if (!clipboard) { return }
|
|
2533
|
+
|
|
2534
|
+
const selection = doc.getSelection()
|
|
2535
|
+
if (!selection || !selection.rangeCount) { return }
|
|
2536
|
+
|
|
2537
|
+
const pastedHtml = clipboard.getData('text/html')
|
|
2538
|
+
const pastedText = clipboard.getData('text/plain')
|
|
2539
|
+
if (!pastedHtml && !pastedText) { return }
|
|
2540
|
+
|
|
2541
|
+
event.preventDefault()
|
|
2542
|
+
if (!insertPastedContent(doc, selection, { html: pastedHtml, text: pastedText })) { return }
|
|
2543
|
+
|
|
2544
|
+
normalizeContent()
|
|
2545
|
+
updateContentWithHistory(doc)
|
|
2546
|
+
})
|
|
2547
|
+
|
|
2790
2548
|
// Handle input events
|
|
2791
2549
|
doc.addEventListener('input', (e) => {
|
|
2792
2550
|
const inputEvent = e as InputEvent
|
|
@@ -2963,9 +2721,7 @@ function setupAutoWrapping(doc: Document) {
|
|
|
2963
2721
|
subtree: true
|
|
2964
2722
|
})
|
|
2965
2723
|
|
|
2966
|
-
|
|
2967
|
-
doc.addEventListener('click', (e) => {
|
|
2968
|
-
const target = e.target as HTMLElement
|
|
2724
|
+
function openMediaFromTarget(target: HTMLElement, event?: Event): boolean {
|
|
2969
2725
|
const link = target.closest('a')
|
|
2970
2726
|
const table = target.closest('table')
|
|
2971
2727
|
|
|
@@ -3003,27 +2759,30 @@ function setupAutoWrapping(doc: Document) {
|
|
|
3003
2759
|
if (table) {
|
|
3004
2760
|
// Table clicks handled only by edit button
|
|
3005
2761
|
} else if (videoFigure) {
|
|
3006
|
-
|
|
3007
|
-
|
|
2762
|
+
event?.preventDefault()
|
|
2763
|
+
event?.stopPropagation()
|
|
3008
2764
|
videoFigure.classList.add('media-selected')
|
|
3009
2765
|
openVideoModal(videoFigure)
|
|
2766
|
+
return true
|
|
3010
2767
|
} else if (imageFigure) {
|
|
3011
|
-
|
|
3012
|
-
|
|
2768
|
+
event?.preventDefault()
|
|
2769
|
+
event?.stopPropagation()
|
|
3013
2770
|
imageFigure.classList.add('media-selected')
|
|
3014
2771
|
openImageModal(imageFigure)
|
|
2772
|
+
return true
|
|
3015
2773
|
} else if (embedFigure) {
|
|
3016
|
-
|
|
3017
|
-
|
|
2774
|
+
event?.preventDefault()
|
|
2775
|
+
event?.stopPropagation()
|
|
3018
2776
|
embedFigure.classList.add('media-selected')
|
|
3019
2777
|
openEmbedModal(embedFigure)
|
|
2778
|
+
return true
|
|
3020
2779
|
} else if (link && link.href) {
|
|
3021
|
-
|
|
3022
|
-
|
|
2780
|
+
event?.preventDefault()
|
|
2781
|
+
event?.stopPropagation()
|
|
3023
2782
|
|
|
3024
2783
|
// Get current selection
|
|
3025
2784
|
const selection = doc.getSelection()
|
|
3026
|
-
if (!selection) { return }
|
|
2785
|
+
if (!selection) { return false }
|
|
3027
2786
|
|
|
3028
2787
|
// Create a range that selects the entire link
|
|
3029
2788
|
const range = doc.createRange()
|
|
@@ -3044,7 +2803,21 @@ function setupAutoWrapping(doc: Document) {
|
|
|
3044
2803
|
}
|
|
3045
2804
|
|
|
3046
2805
|
showLinkModal.value = true
|
|
2806
|
+
return true
|
|
3047
2807
|
}
|
|
2808
|
+
return false
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
// Handle clicks on links and media to open their edit modals
|
|
2812
|
+
doc.addEventListener('click', (e) => {
|
|
2813
|
+
openMediaFromTarget(e.target as HTMLElement, e)
|
|
2814
|
+
})
|
|
2815
|
+
|
|
2816
|
+
doc.addEventListener('keydown', (e) => {
|
|
2817
|
+
if (e.key !== 'Enter' && e.key !== ' ') { return }
|
|
2818
|
+
const target = e.target as HTMLElement
|
|
2819
|
+
if (!target.matches('figure[data-media-type="image"], figure[data-media-type="video"]')) { return }
|
|
2820
|
+
openMediaFromTarget(target, e)
|
|
3048
2821
|
})
|
|
3049
2822
|
|
|
3050
2823
|
// Handle focus to remove placeholder when user starts interacting
|
|
@@ -3154,7 +2927,7 @@ async function initEditor() {
|
|
|
3154
2927
|
|
|
3155
2928
|
/* Ensure all text elements inherit the theme color */
|
|
3156
2929
|
p, h1, h2, h3, h4, h5, h6, div, span, li {
|
|
3157
|
-
color: inherit
|
|
2930
|
+
color: inherit;
|
|
3158
2931
|
}
|
|
3159
2932
|
table {
|
|
3160
2933
|
border-collapse: collapse;
|
|
@@ -3176,12 +2949,12 @@ async function initEditor() {
|
|
|
3176
2949
|
/* Table edit button styles */
|
|
3177
2950
|
.table-edit-btn {
|
|
3178
2951
|
position: absolute;
|
|
3179
|
-
|
|
2952
|
+
bottom: -8px;
|
|
3180
2953
|
right: -8px;
|
|
3181
|
-
background:
|
|
2954
|
+
background: rgba(0, 0, 0, 0.72);
|
|
3182
2955
|
color: #fff !important;
|
|
3183
2956
|
border: 1px solid #fff;
|
|
3184
|
-
border-radius:
|
|
2957
|
+
border-radius: 6px;
|
|
3185
2958
|
padding: 4px 8px;
|
|
3186
2959
|
font-size: 12px;
|
|
3187
2960
|
cursor: pointer;
|
|
@@ -3236,6 +3009,7 @@ async function initEditor() {
|
|
|
3236
3009
|
}
|
|
3237
3010
|
|
|
3238
3011
|
.table-col-add-btn,
|
|
3012
|
+
.table-row-add-btn,
|
|
3239
3013
|
.table-col-thumb {
|
|
3240
3014
|
position: absolute;
|
|
3241
3015
|
display: flex;
|
|
@@ -3252,7 +3026,8 @@ async function initEditor() {
|
|
|
3252
3026
|
transition: opacity 0.14s ease, transform 0.14s ease, background-color 0.14s ease, border-color 0.14s ease, visibility 0.14s ease;
|
|
3253
3027
|
}
|
|
3254
3028
|
|
|
3255
|
-
.table-col-add-btn
|
|
3029
|
+
.table-col-add-btn,
|
|
3030
|
+
.table-row-add-btn {
|
|
3256
3031
|
width: 20px;
|
|
3257
3032
|
height: 20px;
|
|
3258
3033
|
border-radius: 999px;
|
|
@@ -3263,16 +3038,18 @@ async function initEditor() {
|
|
|
3263
3038
|
cursor: pointer;
|
|
3264
3039
|
box-shadow: 0 2px 6px rgba(0, 123, 255, 0.35);
|
|
3265
3040
|
z-index: 98;
|
|
3041
|
+
padding-bottom: 2px;
|
|
3266
3042
|
}
|
|
3267
3043
|
|
|
3268
|
-
.table-col-add-btn:hover
|
|
3044
|
+
.table-col-add-btn:hover,
|
|
3045
|
+
.table-row-add-btn:hover {
|
|
3269
3046
|
background: #0056b3;
|
|
3270
3047
|
transform: translate(-50%, -50%) scale(1.08) !important;
|
|
3271
3048
|
}
|
|
3272
3049
|
|
|
3273
|
-
.table-col-add-line
|
|
3050
|
+
.table-col-add-line,
|
|
3051
|
+
.table-row-add-line {
|
|
3274
3052
|
position: absolute;
|
|
3275
|
-
width: 2px;
|
|
3276
3053
|
background: #007bff;
|
|
3277
3054
|
pointer-events: none;
|
|
3278
3055
|
opacity: 0;
|
|
@@ -3282,6 +3059,15 @@ async function initEditor() {
|
|
|
3282
3059
|
z-index: 94;
|
|
3283
3060
|
}
|
|
3284
3061
|
|
|
3062
|
+
.table-col-add-line {
|
|
3063
|
+
width: 2px;
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
.table-row-add-line {
|
|
3067
|
+
height: 2px;
|
|
3068
|
+
transform: translateY(-50%);
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3285
3071
|
.table-col-thumb {
|
|
3286
3072
|
width: 28px;
|
|
3287
3073
|
height: 14px;
|
|
@@ -3311,19 +3097,22 @@ async function initEditor() {
|
|
|
3311
3097
|
border-color: #0056b3;
|
|
3312
3098
|
}
|
|
3313
3099
|
|
|
3314
|
-
.table-wrapper:hover .table-inline-controls.is-visible.show-col-add .table-col-add-btn
|
|
3100
|
+
.table-wrapper:hover .table-inline-controls.is-visible.show-col-add .table-col-add-btn,
|
|
3101
|
+
.table-wrapper:hover .table-inline-controls.is-visible.show-row-add .table-row-add-btn {
|
|
3315
3102
|
opacity: 1;
|
|
3316
3103
|
visibility: visible;
|
|
3317
3104
|
transform: translate(-50%, -50%) scale(1);
|
|
3318
3105
|
}
|
|
3319
3106
|
|
|
3320
3107
|
.table-wrapper:hover .table-inline-controls.is-visible.show-col-add .table-col-add-line,
|
|
3108
|
+
.table-wrapper:hover .table-inline-controls.is-visible.show-row-add .table-row-add-line,
|
|
3321
3109
|
.table-inline-controls.is-dragging-col .table-col-add-line {
|
|
3322
3110
|
opacity: 0.45;
|
|
3323
3111
|
visibility: visible;
|
|
3324
3112
|
}
|
|
3325
3113
|
|
|
3326
3114
|
.table-wrapper:hover .table-inline-controls.is-visible.show-col-add:has(.table-col-add-btn:hover) .table-col-add-line,
|
|
3115
|
+
.table-wrapper:hover .table-inline-controls.is-visible.show-row-add:has(.table-row-add-btn:hover) .table-row-add-line,
|
|
3327
3116
|
.table-inline-controls.is-dragging-col .table-col-add-line {
|
|
3328
3117
|
opacity: 0.9;
|
|
3329
3118
|
visibility: visible;
|
|
@@ -3342,6 +3131,55 @@ async function initEditor() {
|
|
|
3342
3131
|
}
|
|
3343
3132
|
|
|
3344
3133
|
/* ── Media editing affordances ─────────────────────────────── */
|
|
3134
|
+
figure.video-figure {
|
|
3135
|
+
box-sizing: border-box;
|
|
3136
|
+
width: 100%;
|
|
3137
|
+
max-width: 100%;
|
|
3138
|
+
margin: 2rem 0;
|
|
3139
|
+
text-align: center;
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
figure.video-figure .video-container,
|
|
3143
|
+
figure.video-figure .bgl_vid,
|
|
3144
|
+
figure.video-figure .video-placeholder {
|
|
3145
|
+
box-sizing: border-box;
|
|
3146
|
+
width: 100%;
|
|
3147
|
+
max-width: 100%;
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
figure.video-figure .video-placeholder {
|
|
3151
|
+
position: relative;
|
|
3152
|
+
display: flex;
|
|
3153
|
+
align-items: center;
|
|
3154
|
+
justify-content: center;
|
|
3155
|
+
flex-direction: column;
|
|
3156
|
+
overflow: hidden;
|
|
3157
|
+
min-height: 50px;
|
|
3158
|
+
background: #f4f4f4;
|
|
3159
|
+
border-radius: 8px;
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
figure.video-figure .video-thumbnail {
|
|
3163
|
+
position: absolute;
|
|
3164
|
+
inset: 0;
|
|
3165
|
+
display: block;
|
|
3166
|
+
width: 100%;
|
|
3167
|
+
height: 100%;
|
|
3168
|
+
max-width: 100%;
|
|
3169
|
+
object-fit: cover;
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
figure.video-figure .video-overlay {
|
|
3173
|
+
position: absolute;
|
|
3174
|
+
inset: 0;
|
|
3175
|
+
display: flex;
|
|
3176
|
+
align-items: center;
|
|
3177
|
+
justify-content: center;
|
|
3178
|
+
flex-direction: column;
|
|
3179
|
+
background: rgba(0, 0, 0, 0.25);
|
|
3180
|
+
color: #fff;
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3345
3183
|
/* Figures wrapping media: ::after badge + hover ring.
|
|
3346
3184
|
No display override — <figure> is already block, <img>/<video> keep their natural display. */
|
|
3347
3185
|
figure:has(img),
|
|
@@ -3435,6 +3273,25 @@ async function initEditor() {
|
|
|
3435
3273
|
width: 100%;
|
|
3436
3274
|
}
|
|
3437
3275
|
|
|
3276
|
+
figure.video-figure.align-start,
|
|
3277
|
+
figure.video-figure.align-end {
|
|
3278
|
+
max-width: 50%;
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
figure.video-figure .video-container {
|
|
3282
|
+
width: 100%;
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
@media (max-width: 600px) {
|
|
3286
|
+
figure.video-figure.align-start,
|
|
3287
|
+
figure.video-figure.align-end {
|
|
3288
|
+
float: none;
|
|
3289
|
+
width: 100% !important;
|
|
3290
|
+
max-width: 100%;
|
|
3291
|
+
margin-inline: 0;
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3438
3295
|
/* Linked images: the wrapping <a> should behave like the image block.
|
|
3439
3296
|
In the editor, disable navigation so clicks open the edit modal. */
|
|
3440
3297
|
figure.image-figure > a {
|
|
@@ -3491,6 +3348,8 @@ async function initEditor() {
|
|
|
3491
3348
|
|
|
3492
3349
|
if (normalizedModelValue.value) {
|
|
3493
3350
|
doc.body.innerHTML = normalizedModelValue.value
|
|
3351
|
+
hydrateVideoFigures(doc.body)
|
|
3352
|
+
hydrateImageFigures(doc.body)
|
|
3494
3353
|
} else if (props.placeholder) {
|
|
3495
3354
|
const placeholderEl = doc.createElement('p')
|
|
3496
3355
|
placeholderEl.className = 'placeholder'
|
|
@@ -3752,10 +3611,12 @@ function updateIframeColors() {
|
|
|
3752
3611
|
font-family: ${currentFontFamily.value} !important;
|
|
3753
3612
|
}
|
|
3754
3613
|
|
|
3755
|
-
/*
|
|
3614
|
+
/* Body copy inherits the configured base size; headings keep semantic sizes. */
|
|
3615
|
+
p, div, li {
|
|
3616
|
+
font-size: inherit;
|
|
3617
|
+
}
|
|
3756
3618
|
p, h1, h2, h3, h4, h5, h6, div, span, li {
|
|
3757
|
-
color: inherit
|
|
3758
|
-
font-size: inherit !important;
|
|
3619
|
+
color: inherit;
|
|
3759
3620
|
}
|
|
3760
3621
|
${effectiveContentStyles.value}
|
|
3761
3622
|
`
|
|
@@ -3796,6 +3657,8 @@ watch(normalizedModelValue, (newValue) => {
|
|
|
3796
3657
|
}
|
|
3797
3658
|
} else {
|
|
3798
3659
|
doc.body.innerHTML = newValue
|
|
3660
|
+
hydrateVideoFigures(doc.body)
|
|
3661
|
+
hydrateImageFigures(doc.body)
|
|
3799
3662
|
}
|
|
3800
3663
|
|
|
3801
3664
|
detectAndSetDirection()
|
|
@@ -3827,6 +3690,16 @@ watch(() => editor.state.content, () => {
|
|
|
3827
3690
|
emit('update:modelValue', getCleanHTML(editor.state.doc ?? null))
|
|
3828
3691
|
})
|
|
3829
3692
|
|
|
3693
|
+
watch(hasExceededLimit, (exceeded) => {
|
|
3694
|
+
if (!exceeded) { return }
|
|
3695
|
+
emit('limit-exceeded', {
|
|
3696
|
+
characters: characterCount.value,
|
|
3697
|
+
words: wordCount.value,
|
|
3698
|
+
maxLength: props.maxLength,
|
|
3699
|
+
maxWords: props.maxWords,
|
|
3700
|
+
})
|
|
3701
|
+
})
|
|
3702
|
+
|
|
3830
3703
|
// Watch table alignment changes and update the table immediately
|
|
3831
3704
|
watch(() => tableForm.value.alignment, (newAlignment) => {
|
|
3832
3705
|
if (pendingTableData?.existingTable) {
|
|
@@ -3892,6 +3765,7 @@ const imgTransform = computed({
|
|
|
3892
3765
|
:selectedStyles="editor.state.selectedStyles" :hide-images="hideImages" :hide-videos="hideVideos"
|
|
3893
3766
|
:hide-embeds="hideEmbed" :hide-tables="hideTables" :hide-alignment="hideAlignment"
|
|
3894
3767
|
:hide-directions="hideDirections" :hide-h5-h6="hideH5H6" :hide="effectiveHideArray"
|
|
3768
|
+
:document-colors="documentColors"
|
|
3895
3769
|
@action="commands.execute"
|
|
3896
3770
|
>
|
|
3897
3771
|
<template #end>
|
|
@@ -3914,6 +3788,17 @@ const imgTransform = computed({
|
|
|
3914
3788
|
</slot>
|
|
3915
3789
|
</template>
|
|
3916
3790
|
</EditorToolbar>
|
|
3791
|
+
<div
|
|
3792
|
+
v-if="showDocumentCounter"
|
|
3793
|
+
class="flex space-between txt10 pb-025"
|
|
3794
|
+
:class="hasExceededLimit ? 'color-red' : 'color-gray'"
|
|
3795
|
+
role="status"
|
|
3796
|
+
aria-live="polite"
|
|
3797
|
+
>
|
|
3798
|
+
<span>{{ characterCount }}<template v-if="maxLength"> / {{ maxLength }}</template> characters</span>
|
|
3799
|
+
<span>{{ wordCount }}<template v-if="maxWords"> / {{ maxWords }}</template> words</span>
|
|
3800
|
+
<span class="hide">{{ counterLabel }}</span>
|
|
3801
|
+
</div>
|
|
3917
3802
|
<div class="editor-container" :class="{ 'split-view': editor.state.isSplitView }">
|
|
3918
3803
|
<div
|
|
3919
3804
|
class="content-area radius-05"
|
|
@@ -4025,7 +3910,15 @@ const imgTransform = computed({
|
|
|
4025
3910
|
width="l" @update:open="showImageModal = $event"
|
|
4026
3911
|
>
|
|
4027
3912
|
<div class="image-dialog-layout">
|
|
4028
|
-
<
|
|
3913
|
+
<div>
|
|
3914
|
+
<UploadInput
|
|
3915
|
+
v-model="imgTransform" label="Image" type="image" accept="image/*" :upload="uploadImage"
|
|
3916
|
+
@add-file-start="imageUploadError = ''" @upload-error="handleImageUploadError" @keydown.enter="submitImage"
|
|
3917
|
+
/>
|
|
3918
|
+
<p v-if="imageUploadError" class="color-red txt12 m-0" role="alert">
|
|
3919
|
+
{{ imageUploadError }}
|
|
3920
|
+
</p>
|
|
3921
|
+
</div>
|
|
4029
3922
|
<div class="flex flex-column gap-05 min-w-0">
|
|
4030
3923
|
<TextInput
|
|
4031
3924
|
v-model="imageForm.src" label="Image URL" type="url" placeholder="https://example.com/image.jpg"
|
|
@@ -4131,6 +4024,16 @@ const imgTransform = computed({
|
|
|
4131
4024
|
help="Examples: 100%, 500px, 50vw, 300"
|
|
4132
4025
|
/>
|
|
4133
4026
|
|
|
4027
|
+
<SelectBtn
|
|
4028
|
+
v-model="videoForm.align"
|
|
4029
|
+
label="Video alignment"
|
|
4030
|
+
:options="[
|
|
4031
|
+
{ value: 'full', label: 'Full width', icon: 'fit_screen' },
|
|
4032
|
+
{ value: 'start', label: 'Align start', icon: 'align_horizontal_left' },
|
|
4033
|
+
{ value: 'end', label: 'Align end', icon: 'align_horizontal_right' },
|
|
4034
|
+
]"
|
|
4035
|
+
/>
|
|
4036
|
+
|
|
4134
4037
|
<SelectInput
|
|
4135
4038
|
v-model="videoForm.aspectRatio" label="Aspect Ratio" :options="[
|
|
4136
4039
|
{ value: '16:9', label: '16:9 (Standard)' },
|
|
@@ -4142,12 +4045,12 @@ const imgTransform = computed({
|
|
|
4142
4045
|
]"
|
|
4143
4046
|
/>
|
|
4144
4047
|
|
|
4145
|
-
<div v-if="videoForm.aspectRatio === 'custom'" class="grid grid-wrap-2 gap-05">
|
|
4146
|
-
<TextInput v-model="videoForm.customWidth" label="Width ratio" placeholder="16" type="number" />
|
|
4147
|
-
<TextInput v-model="videoForm.customHeight" label="Height ratio" placeholder="9" type="number" />
|
|
4048
|
+
<div v-if="videoForm.aspectRatio === 'custom'" class="grid grid-wrap-2 gap-05 m_gap-05">
|
|
4049
|
+
<TextInput v-model="videoForm.customWidth" class="m_grid-span-1" label="Width ratio" placeholder="16" type="number" />
|
|
4050
|
+
<TextInput v-model="videoForm.customHeight" class="m_grid-span-1" label="Height ratio" placeholder="9" type="number" />
|
|
4148
4051
|
</div>
|
|
4149
4052
|
|
|
4150
|
-
<div class="grid grid-wrap-2 gap-05 py-1">
|
|
4053
|
+
<div class="grid grid-wrap-2 gap-05 py-1 m_gap-05 m_py-05">
|
|
4151
4054
|
<CheckInput v-model="videoForm.controls" label="Show controls" />
|
|
4152
4055
|
<CheckInput v-model="videoForm.autoplay" label="Autoplay" />
|
|
4153
4056
|
<CheckInput v-model="videoForm.mute" label="Mute" />
|
|
@@ -4175,14 +4078,14 @@ const imgTransform = computed({
|
|
|
4175
4078
|
</Card>
|
|
4176
4079
|
</div>
|
|
4177
4080
|
<template #footer>
|
|
4178
|
-
<div class="flex gap-05 w-100 ">
|
|
4179
|
-
<Btn value="Cancel" flat thin @click="showVideoModal = false" />
|
|
4081
|
+
<div class="flex gap-05 w-100 m_column">
|
|
4082
|
+
<Btn value="Cancel" flat thin full-width-mobile @click="showVideoModal = false" />
|
|
4180
4083
|
<Btn
|
|
4181
4084
|
v-if="pendingVideoData?.existingVideo" value="Delete Video" color="red" flat thin icon="delete"
|
|
4182
|
-
@click="deleteVideo"
|
|
4085
|
+
full-width-mobile @click="deleteVideo"
|
|
4183
4086
|
/>
|
|
4184
4087
|
<Btn
|
|
4185
|
-
value="Insert Video" class="ms-auto" :disabled="!videoForm.src || !isValidVideoUrl(videoForm.src)"
|
|
4088
|
+
:value="pendingVideoData?.existingVideo ? 'Save Video' : 'Insert Video'" class="ms-auto m_ms-0" full-width-mobile :disabled="!videoForm.src || !isValidVideoUrl(videoForm.src)"
|
|
4186
4089
|
@click="submitVideo"
|
|
4187
4090
|
/>
|
|
4188
4091
|
</div>
|