@bagelink/vue 1.19.1 → 1.20.1
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 +73 -48
- 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 +18604 -17549
- package/dist/style.css +2 -2
- package/package.json +1 -1
- package/src/components/Dropdown.vue +48 -7
- package/src/components/FilterQuery.vue +1 -1
- package/src/components/Icon/Icon.vue +8 -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 +354 -463
- 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
|
@@ -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,
|
|
@@ -67,11 +72,17 @@ const props = defineProps<{
|
|
|
67
72
|
textColor?: string
|
|
68
73
|
// Font size
|
|
69
74
|
fontSize?: number | string
|
|
75
|
+
// Optional document metrics and limits
|
|
76
|
+
showCounter?: boolean
|
|
77
|
+
maxLength?: number
|
|
78
|
+
maxWords?: number
|
|
70
79
|
// Field-specific CSS injected into the editor iframe.
|
|
71
80
|
contentStyles?: string
|
|
81
|
+
// Optional image uploader. Must resolve to a persistent path key or URL.
|
|
82
|
+
uploadImage?: (file: File) => Promise<string>
|
|
72
83
|
} & BagelInputShellProps>()
|
|
73
84
|
|
|
74
|
-
const emit = defineEmits(['update:modelValue'])
|
|
85
|
+
const emit = defineEmits(['update:modelValue', 'limit-exceeded'])
|
|
75
86
|
|
|
76
87
|
const { shellClass, shellStyle } = useBagelInputShell(props)
|
|
77
88
|
const extensionRegistry = inject(RICH_TEXT_EXTENSION_REGISTRY_KEY, new Map())
|
|
@@ -107,6 +118,35 @@ const effectiveContentStyles = computed(() => [
|
|
|
107
118
|
...resolvedExtensions.value.map(extension => extension.contentStyles),
|
|
108
119
|
].filter(Boolean).join('\n'))
|
|
109
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
|
+
|
|
110
150
|
function updateCustomToolbarStyles() {
|
|
111
151
|
const { doc } = editor.state
|
|
112
152
|
if (!doc) return
|
|
@@ -146,6 +186,25 @@ const editor = useEditor()
|
|
|
146
186
|
const isInitializing = ref(false)
|
|
147
187
|
const hasInitialized = ref(false)
|
|
148
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
|
+
})
|
|
149
208
|
|
|
150
209
|
function setBlock(tagName: string, className?: string, removeClasses: string[] = []) {
|
|
151
210
|
const { doc } = editor.state
|
|
@@ -355,17 +414,8 @@ let pendingLinkData: {
|
|
|
355
414
|
|
|
356
415
|
// Image modal state
|
|
357
416
|
const showImageModal = ref(false)
|
|
358
|
-
const imageForm = ref(
|
|
359
|
-
|
|
360
|
-
alt: '',
|
|
361
|
-
width: '',
|
|
362
|
-
height: '',
|
|
363
|
-
credit: '',
|
|
364
|
-
figcaption: false,
|
|
365
|
-
align: 'full', // full, start, end
|
|
366
|
-
link: '',
|
|
367
|
-
linkNewTab: false
|
|
368
|
-
})
|
|
417
|
+
const imageForm = ref(createDefaultImageData())
|
|
418
|
+
const imageUploadError = ref('')
|
|
369
419
|
let pendingImageData: {
|
|
370
420
|
selection: Selection
|
|
371
421
|
range: Range
|
|
@@ -393,19 +443,7 @@ const inlineToolbarSelection = ref<Selection | null>(null)
|
|
|
393
443
|
|
|
394
444
|
// Video modal state
|
|
395
445
|
const showVideoModal = ref(false)
|
|
396
|
-
const videoForm = ref(
|
|
397
|
-
src: '',
|
|
398
|
-
width: '',
|
|
399
|
-
autoplay: false,
|
|
400
|
-
mute: false,
|
|
401
|
-
controls: true,
|
|
402
|
-
loop: false,
|
|
403
|
-
aspectRatio: '16:9',
|
|
404
|
-
customWidth: '',
|
|
405
|
-
customHeight: '',
|
|
406
|
-
caption: '',
|
|
407
|
-
showCaption: false
|
|
408
|
-
})
|
|
446
|
+
const videoForm = ref(createDefaultVideoData())
|
|
409
447
|
let pendingVideoData: {
|
|
410
448
|
selection: Selection
|
|
411
449
|
range: Range
|
|
@@ -804,6 +842,7 @@ function isValidUrl(url: string): boolean {
|
|
|
804
842
|
function openImageModal(existingImage: HTMLElement | null = null) {
|
|
805
843
|
const { doc } = editor.state
|
|
806
844
|
if (!doc) { return }
|
|
845
|
+
imageUploadError.value = ''
|
|
807
846
|
|
|
808
847
|
// Get current selection for new images
|
|
809
848
|
if (!existingImage) {
|
|
@@ -821,51 +860,18 @@ function openImageModal(existingImage: HTMLElement | null = null) {
|
|
|
821
860
|
}
|
|
822
861
|
|
|
823
862
|
if (existingImage) {
|
|
824
|
-
|
|
825
|
-
const img = existingImage.tagName === 'IMG'
|
|
826
|
-
? existingImage as HTMLImageElement
|
|
827
|
-
: existingImage.querySelector('img')
|
|
828
|
-
const figcaption = existingImage.querySelector('figcaption')
|
|
829
|
-
const credit = existingImage.getAttribute('data-credit') || img?.getAttribute('data-credit') || ''
|
|
830
|
-
|
|
831
|
-
// Read alignment from the figure (or bare img) data-align attribute
|
|
832
|
-
const alignSource = existingImage.tagName === 'IMG' ? img : existingImage
|
|
833
|
-
const align = alignSource?.getAttribute('data-align') || 'full'
|
|
834
|
-
|
|
835
|
-
// Read link from the anchor wrapping the image (if any)
|
|
836
|
-
const anchor = img?.closest('a')
|
|
837
|
-
const link = anchor?.getAttribute('href') || ''
|
|
838
|
-
const linkNewTab = anchor?.getAttribute('target') === '_blank'
|
|
839
|
-
|
|
840
|
-
imageForm.value = {
|
|
841
|
-
src: img?.src || '',
|
|
842
|
-
alt: img?.alt || '',
|
|
843
|
-
width: img?.getAttribute('data-width') || img?.style.width || '',
|
|
844
|
-
height: img?.getAttribute('data-height') || img?.style.height || '',
|
|
845
|
-
credit,
|
|
846
|
-
figcaption: !!figcaption,
|
|
847
|
-
align,
|
|
848
|
-
link,
|
|
849
|
-
linkNewTab
|
|
850
|
-
}
|
|
863
|
+
imageForm.value = readImageData(existingImage)
|
|
851
864
|
} else {
|
|
852
|
-
|
|
853
|
-
imageForm.value = {
|
|
854
|
-
src: '',
|
|
855
|
-
alt: '',
|
|
856
|
-
width: '',
|
|
857
|
-
height: '',
|
|
858
|
-
credit: '',
|
|
859
|
-
figcaption: false,
|
|
860
|
-
align: 'full',
|
|
861
|
-
link: '',
|
|
862
|
-
linkNewTab: false
|
|
863
|
-
}
|
|
865
|
+
imageForm.value = createDefaultImageData()
|
|
864
866
|
}
|
|
865
867
|
|
|
866
868
|
showImageModal.value = true
|
|
867
869
|
}
|
|
868
870
|
|
|
871
|
+
function handleImageUploadError() {
|
|
872
|
+
imageUploadError.value = 'Image upload failed. Please try again or paste an image URL.'
|
|
873
|
+
}
|
|
874
|
+
|
|
869
875
|
// Function to submit image
|
|
870
876
|
function submitImage() {
|
|
871
877
|
if (!imageForm.value.src || !pendingImageData) { return }
|
|
@@ -874,124 +880,9 @@ function submitImage() {
|
|
|
874
880
|
if (!doc) { return }
|
|
875
881
|
|
|
876
882
|
const { existingImage } = pendingImageData
|
|
883
|
+
const elementToUpdate = buildImageFigure(doc, imageForm.value, existingImage)
|
|
877
884
|
|
|
878
|
-
|
|
879
|
-
const img = doc.createElement('img')
|
|
880
|
-
img.src = imageForm.value.src
|
|
881
|
-
img.alt = imageForm.value.alt || ''
|
|
882
|
-
|
|
883
|
-
// Store original width/height values in data attributes
|
|
884
|
-
if (imageForm.value.width) {
|
|
885
|
-
img.setAttribute('data-width', imageForm.value.width)
|
|
886
|
-
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')) {
|
|
887
|
-
img.style.width = imageForm.value.width
|
|
888
|
-
} else {
|
|
889
|
-
img.style.width = `${imageForm.value.width}px`
|
|
890
|
-
}
|
|
891
|
-
} else {
|
|
892
|
-
// Floated images default to a partial width so text can wrap alongside;
|
|
893
|
-
// full-width images fill the container.
|
|
894
|
-
img.style.width = (imageForm.value.align === 'start' || imageForm.value.align === 'end') ? '45%' : '100%'
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
if (imageForm.value.height) {
|
|
898
|
-
img.setAttribute('data-height', imageForm.value.height)
|
|
899
|
-
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') {
|
|
900
|
-
img.style.height = imageForm.value.height
|
|
901
|
-
} else {
|
|
902
|
-
img.style.height = `${imageForm.value.height}px`
|
|
903
|
-
}
|
|
904
|
-
} else {
|
|
905
|
-
img.style.height = 'auto'
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// Helper function to create figcaption with alt text and credit (using safe DOM nodes)
|
|
909
|
-
function createFigcaption() {
|
|
910
|
-
const figcaption = doc!.createElement('figcaption')
|
|
911
|
-
|
|
912
|
-
if (imageForm.value.figcaption && imageForm.value.alt) {
|
|
913
|
-
const altStrong = doc!.createElement('strong')
|
|
914
|
-
altStrong.className = 'alt-text'
|
|
915
|
-
altStrong.textContent = imageForm.value.alt
|
|
916
|
-
figcaption.appendChild(altStrong)
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
if (imageForm.value.credit) {
|
|
920
|
-
if (figcaption.childNodes.length > 0) {
|
|
921
|
-
figcaption.appendChild(doc!.createTextNode(' • '))
|
|
922
|
-
}
|
|
923
|
-
const creditSpan = doc!.createElement('span')
|
|
924
|
-
creditSpan.className = 'photo-credit'
|
|
925
|
-
creditSpan.textContent = imageForm.value.credit
|
|
926
|
-
figcaption.appendChild(creditSpan)
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
return figcaption
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
// Wrap the image in an anchor when a link is provided, otherwise return the bare image
|
|
933
|
-
function createImageContent(): HTMLElement {
|
|
934
|
-
const link = imageForm.value.link?.trim()
|
|
935
|
-
if (!link) { return img }
|
|
936
|
-
|
|
937
|
-
const anchor = doc!.createElement('a')
|
|
938
|
-
anchor.setAttribute('href', link)
|
|
939
|
-
if (imageForm.value.linkNewTab) {
|
|
940
|
-
anchor.setAttribute('target', '_blank')
|
|
941
|
-
anchor.setAttribute('rel', 'noopener noreferrer')
|
|
942
|
-
}
|
|
943
|
-
anchor.appendChild(img)
|
|
944
|
-
return anchor
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
// Create or update figure/image - always wrap in figure
|
|
948
|
-
let elementToUpdate: HTMLElement
|
|
949
|
-
if (existingImage) {
|
|
950
|
-
if (existingImage.tagName.toLowerCase() === 'img') {
|
|
951
|
-
// Converting standalone image to figure (always)
|
|
952
|
-
const figure = doc.createElement('figure')
|
|
953
|
-
figure.className = 'image-figure'
|
|
954
|
-
if (imageForm.value.credit) {
|
|
955
|
-
figure.setAttribute('data-credit', imageForm.value.credit)
|
|
956
|
-
}
|
|
957
|
-
figure.appendChild(createImageContent())
|
|
958
|
-
|
|
959
|
-
// Add caption if needed (alt with checkbox checked OR credit exists)
|
|
960
|
-
if ((imageForm.value.figcaption && imageForm.value.alt) || imageForm.value.credit) {
|
|
961
|
-
figure.appendChild(createFigcaption())
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
existingImage.parentNode?.replaceChild(figure, existingImage)
|
|
965
|
-
elementToUpdate = figure
|
|
966
|
-
} else {
|
|
967
|
-
// Updating an existing figure
|
|
968
|
-
existingImage.innerHTML = '' // Clear existing content
|
|
969
|
-
if (imageForm.value.credit) {
|
|
970
|
-
existingImage.setAttribute('data-credit', imageForm.value.credit)
|
|
971
|
-
}
|
|
972
|
-
existingImage.appendChild(createImageContent())
|
|
973
|
-
|
|
974
|
-
// Add caption if needed (alt with checkbox checked OR credit exists)
|
|
975
|
-
if ((imageForm.value.figcaption && imageForm.value.alt) || imageForm.value.credit) {
|
|
976
|
-
existingImage.appendChild(createFigcaption())
|
|
977
|
-
}
|
|
978
|
-
elementToUpdate = existingImage
|
|
979
|
-
}
|
|
980
|
-
} else {
|
|
981
|
-
// Creating new image - always wrap in figure
|
|
982
|
-
const figure = doc.createElement('figure')
|
|
983
|
-
figure.className = 'image-figure'
|
|
984
|
-
if (imageForm.value.credit) {
|
|
985
|
-
figure.setAttribute('data-credit', imageForm.value.credit)
|
|
986
|
-
}
|
|
987
|
-
figure.appendChild(createImageContent())
|
|
988
|
-
|
|
989
|
-
// Add caption if needed (alt with checkbox checked OR credit exists)
|
|
990
|
-
if ((imageForm.value.figcaption && imageForm.value.alt) || imageForm.value.credit) {
|
|
991
|
-
figure.appendChild(createFigcaption())
|
|
992
|
-
}
|
|
993
|
-
elementToUpdate = figure
|
|
994
|
-
|
|
885
|
+
if (!existingImage) {
|
|
995
886
|
// Insert new element
|
|
996
887
|
const { selection, range } = pendingImageData
|
|
997
888
|
range.collapse(false)
|
|
@@ -1004,12 +895,6 @@ function submitImage() {
|
|
|
1004
895
|
selection.addRange(range)
|
|
1005
896
|
}
|
|
1006
897
|
|
|
1007
|
-
// Apply alignment to the figure wrapper (full = full width, start/end = float)
|
|
1008
|
-
const align = imageForm.value.align || 'full'
|
|
1009
|
-
elementToUpdate.classList.remove('align-full', 'align-start', 'align-end')
|
|
1010
|
-
elementToUpdate.classList.add(`align-${align}`)
|
|
1011
|
-
elementToUpdate.setAttribute('data-align', align)
|
|
1012
|
-
|
|
1013
898
|
updateContentWithHistory(doc)
|
|
1014
899
|
showImageModal.value = false
|
|
1015
900
|
pendingImageData = null
|
|
@@ -1107,6 +992,7 @@ function submitEmbed() {
|
|
|
1107
992
|
iframe.frameBorder = '0'
|
|
1108
993
|
iframe.setAttribute('allowfullscreen', '')
|
|
1109
994
|
iframe.setAttribute('data-media-type', 'embed')
|
|
995
|
+
iframe.title = embedForm.value.alt || 'Embedded content'
|
|
1110
996
|
iframe.className = 'embed-iframe'
|
|
1111
997
|
|
|
1112
998
|
// Set width and height - always set them
|
|
@@ -1179,43 +1065,9 @@ function openVideoModal(existingVideo: HTMLElement | null = null) {
|
|
|
1179
1065
|
}
|
|
1180
1066
|
|
|
1181
1067
|
if (existingVideo) {
|
|
1182
|
-
|
|
1183
|
-
const container = existingVideo.querySelector('.video-container')
|
|
1184
|
-
const bareVideo = existingVideo.tagName === 'VIDEO' ? existingVideo as HTMLVideoElement : null
|
|
1185
|
-
const bareIframe = existingVideo.tagName === 'IFRAME' ? existingVideo as HTMLIFrameElement : null
|
|
1186
|
-
const isCustom = container?.getAttribute('data-custom-aspect-ratio') === 'true'
|
|
1187
|
-
|
|
1188
|
-
videoForm.value = {
|
|
1189
|
-
src: container?.getAttribute('data-video-src')
|
|
1190
|
-
|| bareVideo?.src
|
|
1191
|
-
|| bareIframe?.src
|
|
1192
|
-
|| '',
|
|
1193
|
-
width: container?.getAttribute('data-width') || (bareVideo?.style.width ?? '') || '',
|
|
1194
|
-
autoplay: container ? container.getAttribute('data-autoplay') === 'true' : (bareVideo?.autoplay ?? false),
|
|
1195
|
-
mute: container ? container.getAttribute('data-mute') === 'true' : (bareVideo?.muted ?? false),
|
|
1196
|
-
controls: container ? container.getAttribute('data-controls') === 'true' : (bareVideo?.controls ?? true),
|
|
1197
|
-
loop: container ? container.getAttribute('data-loop') === 'true' : (bareVideo?.loop ?? false),
|
|
1198
|
-
aspectRatio: isCustom ? 'custom' : (container?.getAttribute('data-aspect-ratio') || '16:9'),
|
|
1199
|
-
customWidth: container?.getAttribute('data-custom-width') || '',
|
|
1200
|
-
customHeight: container?.getAttribute('data-custom-height') || '',
|
|
1201
|
-
caption: existingVideo.querySelector?.('figcaption')?.textContent || '',
|
|
1202
|
-
showCaption: !!existingVideo.querySelector?.('figcaption')
|
|
1203
|
-
}
|
|
1068
|
+
videoForm.value = readVideoData(existingVideo)
|
|
1204
1069
|
} else {
|
|
1205
|
-
|
|
1206
|
-
videoForm.value = {
|
|
1207
|
-
src: '',
|
|
1208
|
-
width: '',
|
|
1209
|
-
autoplay: false,
|
|
1210
|
-
mute: false,
|
|
1211
|
-
controls: true,
|
|
1212
|
-
loop: false,
|
|
1213
|
-
aspectRatio: '16:9',
|
|
1214
|
-
customWidth: '',
|
|
1215
|
-
customHeight: '',
|
|
1216
|
-
caption: '',
|
|
1217
|
-
showCaption: false
|
|
1218
|
-
}
|
|
1070
|
+
videoForm.value = createDefaultVideoData()
|
|
1219
1071
|
}
|
|
1220
1072
|
|
|
1221
1073
|
showVideoModal.value = true
|
|
@@ -1264,133 +1116,7 @@ function submitVideo() {
|
|
|
1264
1116
|
if (!doc) { return }
|
|
1265
1117
|
|
|
1266
1118
|
const { existingVideo } = pendingVideoData
|
|
1267
|
-
|
|
1268
|
-
// Calculate aspect ratio
|
|
1269
|
-
let { aspectRatio } = videoForm.value
|
|
1270
|
-
const isCustom = videoForm.value.aspectRatio === 'custom'
|
|
1271
|
-
if (isCustom && videoForm.value.customWidth && videoForm.value.customHeight) {
|
|
1272
|
-
aspectRatio = `${videoForm.value.customWidth}:${videoForm.value.customHeight}`
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
// Auto-adjust aspect ratio for YouTube Shorts
|
|
1276
|
-
const isYoutubeShort = videoForm.value.src.includes('youtube.com/shorts/')
|
|
1277
|
-
if (isYoutubeShort && aspectRatio === '16:9') {
|
|
1278
|
-
aspectRatio = '9:16'
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
// Create video container
|
|
1282
|
-
const videoContainer = doc.createElement('div')
|
|
1283
|
-
videoContainer.className = 'video-container'
|
|
1284
|
-
videoContainer.setAttribute('data-video-src', videoForm.value.src)
|
|
1285
|
-
videoContainer.setAttribute('data-component', 'BglVideo')
|
|
1286
|
-
videoContainer.setAttribute('data-media-type', 'video')
|
|
1287
|
-
videoContainer.setAttribute('data-width', videoForm.value.width)
|
|
1288
|
-
videoContainer.setAttribute('data-autoplay', videoForm.value.autoplay.toString())
|
|
1289
|
-
videoContainer.setAttribute('data-mute', videoForm.value.mute.toString())
|
|
1290
|
-
videoContainer.setAttribute('data-controls', videoForm.value.controls.toString())
|
|
1291
|
-
videoContainer.setAttribute('data-loop', videoForm.value.loop.toString())
|
|
1292
|
-
videoContainer.setAttribute('data-aspect-ratio', aspectRatio)
|
|
1293
|
-
videoContainer.setAttribute('data-custom-aspect-ratio', isCustom.toString())
|
|
1294
|
-
videoContainer.setAttribute('data-custom-width', videoForm.value.customWidth)
|
|
1295
|
-
videoContainer.setAttribute('data-custom-height', videoForm.value.customHeight)
|
|
1296
|
-
|
|
1297
|
-
// Set width
|
|
1298
|
-
if (videoForm.value.width) {
|
|
1299
|
-
videoContainer.style.width = videoForm.value.width.includes('%') || videoForm.value.width.includes('px')
|
|
1300
|
-
? videoForm.value.width
|
|
1301
|
-
: `${videoForm.value.width}px`
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
// Create the video placeholder/thumbnail instead of actual video
|
|
1305
|
-
const videoWrapper = doc.createElement('div')
|
|
1306
|
-
videoWrapper.className = 'bgl_vid'
|
|
1307
|
-
|
|
1308
|
-
// Create video placeholder/thumbnail
|
|
1309
|
-
const placeholder = doc.createElement('div')
|
|
1310
|
-
placeholder.className = 'video-placeholder'
|
|
1311
|
-
placeholder.style.aspectRatio = aspectRatio.replace(':', '/')
|
|
1312
|
-
|
|
1313
|
-
// Detect video type for display
|
|
1314
|
-
const isYoutube = videoForm.value.src.includes('youtube.com') || videoForm.value.src.includes('youtu.be')
|
|
1315
|
-
const isVimeo = videoForm.value.src.includes('vimeo.com')
|
|
1316
|
-
|
|
1317
|
-
// Create play icon
|
|
1318
|
-
const playIcon = doc.createElement('div')
|
|
1319
|
-
playIcon.className = 'video-placeholder-icon'
|
|
1320
|
-
|
|
1321
|
-
// Create text description
|
|
1322
|
-
const description = doc.createElement('div')
|
|
1323
|
-
description.className = 'video-placeholder-description'
|
|
1324
|
-
let platformText = 'video'
|
|
1325
|
-
if (isYoutube) { platformText = 'YouTube video' }
|
|
1326
|
-
else if (isVimeo) { platformText = 'Vimeo video' }
|
|
1327
|
-
description.textContent = platformText
|
|
1328
|
-
|
|
1329
|
-
// Try to get YouTube thumbnail if possible
|
|
1330
|
-
if (isYoutube) {
|
|
1331
|
-
let videoId = ''
|
|
1332
|
-
if (videoForm.value.src.includes('youtube.com/watch?v=')) {
|
|
1333
|
-
videoId = videoForm.value.src.split('watch?v=')[1]?.split('&')[0]
|
|
1334
|
-
} else if (videoForm.value.src.includes('youtu.be/')) {
|
|
1335
|
-
videoId = videoForm.value.src.split('youtu.be/')[1]?.split('?')[0]
|
|
1336
|
-
} else if (videoForm.value.src.includes('youtube.com/shorts/')) {
|
|
1337
|
-
videoId = videoForm.value.src.split('shorts/')[1]?.split('?')[0]
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
if (videoId) {
|
|
1341
|
-
// Add thumbnail class and create thumbnail image
|
|
1342
|
-
placeholder.classList.add('has-thumbnail')
|
|
1343
|
-
|
|
1344
|
-
const thumbnail = doc.createElement('img')
|
|
1345
|
-
thumbnail.className = 'video-thumbnail'
|
|
1346
|
-
thumbnail.src = `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`
|
|
1347
|
-
placeholder.appendChild(thumbnail)
|
|
1348
|
-
|
|
1349
|
-
// Add overlay with play button
|
|
1350
|
-
const overlay = doc.createElement('div')
|
|
1351
|
-
overlay.className = 'video-overlay'
|
|
1352
|
-
|
|
1353
|
-
const overlayPlayIcon = doc.createElement('div')
|
|
1354
|
-
overlayPlayIcon.className = 'video-overlay-icon'
|
|
1355
|
-
|
|
1356
|
-
overlay.appendChild(overlayPlayIcon)
|
|
1357
|
-
placeholder.appendChild(overlay)
|
|
1358
|
-
} else {
|
|
1359
|
-
placeholder.appendChild(playIcon)
|
|
1360
|
-
placeholder.appendChild(description)
|
|
1361
|
-
}
|
|
1362
|
-
} else {
|
|
1363
|
-
placeholder.appendChild(playIcon)
|
|
1364
|
-
placeholder.appendChild(description)
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
// Add special class for YouTube Shorts
|
|
1368
|
-
if (isYoutube && videoForm.value.src.includes('youtube.com/shorts/')) {
|
|
1369
|
-
videoWrapper.classList.add('vid_short')
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
videoWrapper.appendChild(placeholder)
|
|
1373
|
-
|
|
1374
|
-
videoContainer.appendChild(videoWrapper)
|
|
1375
|
-
|
|
1376
|
-
// Create or update figure
|
|
1377
|
-
let figure: HTMLElement
|
|
1378
|
-
if (existingVideo) {
|
|
1379
|
-
figure = existingVideo
|
|
1380
|
-
figure.innerHTML = '' // Clear existing content
|
|
1381
|
-
} else {
|
|
1382
|
-
figure = doc.createElement('figure')
|
|
1383
|
-
figure.className = 'video-figure'
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
figure.appendChild(videoContainer)
|
|
1387
|
-
|
|
1388
|
-
// Add caption if enabled
|
|
1389
|
-
if (videoForm.value.showCaption && videoForm.value.caption) {
|
|
1390
|
-
const figcaption = doc.createElement('figcaption')
|
|
1391
|
-
figcaption.textContent = videoForm.value.caption
|
|
1392
|
-
figure.appendChild(figcaption)
|
|
1393
|
-
}
|
|
1119
|
+
const figure = buildVideoFigure(doc, videoForm.value, existingVideo)
|
|
1394
1120
|
|
|
1395
1121
|
// Insert new video or update existing
|
|
1396
1122
|
if (!existingVideo) {
|
|
@@ -1825,6 +1551,7 @@ function deleteTable() {
|
|
|
1825
1551
|
if (!pendingTableData?.existingTable) { return }
|
|
1826
1552
|
|
|
1827
1553
|
removeTableElement(pendingTableData.existingTable)
|
|
1554
|
+
updateContentWithHistory(editor.state.doc!)
|
|
1828
1555
|
showTableEditor.value = false
|
|
1829
1556
|
pendingTableData = null
|
|
1830
1557
|
}
|
|
@@ -1883,6 +1610,11 @@ function getCleanHTML(doc: Document | null): string {
|
|
|
1883
1610
|
clone.querySelectorAll('.media-selected').forEach((el) => { el.classList.remove('media-selected') })
|
|
1884
1611
|
clone.querySelectorAll('.table-cell-selected').forEach((el) => { el.classList.remove('table-cell-selected') })
|
|
1885
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
|
+
})
|
|
1886
1618
|
|
|
1887
1619
|
// Unwrap .table-wrapper divs: restore original table margin-based alignment
|
|
1888
1620
|
clone.querySelectorAll('.table-wrapper').forEach((wrapper) => {
|
|
@@ -1915,79 +1647,7 @@ function getCleanHTML(doc: Document | null): string {
|
|
|
1915
1647
|
table.removeAttribute('data-edit-button-added')
|
|
1916
1648
|
})
|
|
1917
1649
|
|
|
1918
|
-
|
|
1919
|
-
clone.querySelectorAll('.video-container[data-component="BglVideo"]').forEach((container) => {
|
|
1920
|
-
const el = container as HTMLElement
|
|
1921
|
-
const src = el.getAttribute('data-video-src') || ''
|
|
1922
|
-
if (!src) { return }
|
|
1923
|
-
|
|
1924
|
-
const autoplay = el.getAttribute('data-autoplay') === 'true'
|
|
1925
|
-
const muted = el.getAttribute('data-mute') === 'true'
|
|
1926
|
-
const controls = el.getAttribute('data-controls') !== 'false'
|
|
1927
|
-
const loop = el.getAttribute('data-loop') === 'true'
|
|
1928
|
-
const aspectRatio = el.getAttribute('data-aspect-ratio') || '16:9'
|
|
1929
|
-
const width = el.getAttribute('data-width') || ''
|
|
1930
|
-
|
|
1931
|
-
const widthStyle = width
|
|
1932
|
-
? `width: ${/[%pxvwemr]/.test(width) ? width : `${width}px`};`
|
|
1933
|
-
: 'width: 100%;'
|
|
1934
|
-
|
|
1935
|
-
const [ratioW, ratioH] = aspectRatio.split(':').map(Number)
|
|
1936
|
-
const paddingBottom = ratioW && ratioH ? `${(ratioH / ratioW) * 100}%` : '56.25%'
|
|
1937
|
-
|
|
1938
|
-
const makeResponsiveIframe = (iframeSrc: string): HTMLElement => {
|
|
1939
|
-
const wrapper = document.createElement('div')
|
|
1940
|
-
wrapper.style.cssText = `${widthStyle} position: relative; padding-bottom: ${paddingBottom}; height: 0; overflow: hidden;`
|
|
1941
|
-
const iframe = document.createElement('iframe')
|
|
1942
|
-
iframe.src = iframeSrc
|
|
1943
|
-
iframe.frameBorder = '0'
|
|
1944
|
-
iframe.setAttribute('allowfullscreen', '')
|
|
1945
|
-
iframe.style.cssText = 'position: absolute; top: 0; left: 0; width: 100%; height: 100%;'
|
|
1946
|
-
wrapper.appendChild(iframe)
|
|
1947
|
-
return wrapper
|
|
1948
|
-
}
|
|
1949
|
-
|
|
1950
|
-
let newEl: HTMLElement | null = null
|
|
1951
|
-
|
|
1952
|
-
if (src.includes('youtube.com') || src.includes('youtu.be')) {
|
|
1953
|
-
let embedUrl = src
|
|
1954
|
-
if (src.includes('youtube.com/watch?v=')) {
|
|
1955
|
-
const videoId = src.split('watch?v=')[1]?.split('&')[0]
|
|
1956
|
-
const params = []
|
|
1957
|
-
if (autoplay) { params.push('autoplay=1') }
|
|
1958
|
-
if (muted) { params.push('mute=1') }
|
|
1959
|
-
if (loop) { params.push(`loop=1&playlist=${videoId}`) }
|
|
1960
|
-
if (!controls) { params.push('controls=0') }
|
|
1961
|
-
embedUrl = `https://www.youtube.com/embed/${videoId}${params.length ? `?${params.join('&')}` : ''}`
|
|
1962
|
-
} else if (src.includes('youtu.be/')) {
|
|
1963
|
-
const videoId = src.split('youtu.be/')[1]?.split('?')[0]
|
|
1964
|
-
embedUrl = `https://www.youtube.com/embed/${videoId}`
|
|
1965
|
-
} else if (src.includes('youtube.com/shorts/')) {
|
|
1966
|
-
const videoId = src.split('shorts/')[1]?.split('?')[0]
|
|
1967
|
-
embedUrl = `https://www.youtube.com/embed/${videoId}`
|
|
1968
|
-
}
|
|
1969
|
-
newEl = makeResponsiveIframe(embedUrl)
|
|
1970
|
-
} else if (src.includes('vimeo.com')) {
|
|
1971
|
-
const match = src.match(/vimeo\.com\/(\d+)/)
|
|
1972
|
-
const embedUrl = match ? `https://player.vimeo.com/video/${match[1]}` : src
|
|
1973
|
-
newEl = makeResponsiveIframe(embedUrl)
|
|
1974
|
-
} else if (src.includes('<iframe')) {
|
|
1975
|
-
const srcMatch = src.match(/src=["']([^"']+)["']/)
|
|
1976
|
-
if (srcMatch) { newEl = makeResponsiveIframe(srcMatch[1]) }
|
|
1977
|
-
} else {
|
|
1978
|
-
// Direct video file
|
|
1979
|
-
const video = document.createElement('video')
|
|
1980
|
-
video.src = src
|
|
1981
|
-
if (controls) { video.setAttribute('controls', '') }
|
|
1982
|
-
if (autoplay) { video.setAttribute('autoplay', '') }
|
|
1983
|
-
if (muted) { video.setAttribute('muted', '') }
|
|
1984
|
-
if (loop) { video.setAttribute('loop', '') }
|
|
1985
|
-
video.style.cssText = `${widthStyle} height: auto; display: block;`
|
|
1986
|
-
newEl = video
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
|
-
if (newEl) { el.parentNode?.replaceChild(newEl, el) }
|
|
1990
|
-
})
|
|
1650
|
+
serializeVideoFigures(clone)
|
|
1991
1651
|
|
|
1992
1652
|
// Strip internal placeholder content from video figures (the bgl_vid wrapper)
|
|
1993
1653
|
// that remains after video-container conversion
|
|
@@ -2200,11 +1860,12 @@ function setupTableColumnResizing(doc: Document) {
|
|
|
2200
1860
|
table: HTMLTableElement
|
|
2201
1861
|
cell: HTMLTableCellElement
|
|
2202
1862
|
insertPosition: 'before' | 'after'
|
|
1863
|
+
rowInsertPosition: 'before' | 'after'
|
|
2203
1864
|
} | null = null
|
|
2204
1865
|
|
|
2205
1866
|
const hideAllInlineControls = () => {
|
|
2206
1867
|
doc.querySelectorAll<HTMLElement>('.table-inline-controls').forEach((ctrl) => {
|
|
2207
|
-
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')
|
|
2208
1869
|
})
|
|
2209
1870
|
}
|
|
2210
1871
|
|
|
@@ -2269,7 +1930,8 @@ function setupTableColumnResizing(doc: Document) {
|
|
|
2269
1930
|
const cell = target.closest('td, th') as HTMLTableCellElement | null
|
|
2270
1931
|
const table = cell?.closest('table') as HTMLTableElement | null
|
|
2271
1932
|
const wrapper = table?.closest('.table-wrapper') as HTMLElement | null
|
|
2272
|
-
|
|
1933
|
+
const row = cell?.closest('tr') as HTMLTableRowElement | null
|
|
1934
|
+
if (!cell || !table || !wrapper || !row) {
|
|
2273
1935
|
hideAllInlineControls()
|
|
2274
1936
|
return
|
|
2275
1937
|
}
|
|
@@ -2277,12 +1939,14 @@ function setupTableColumnResizing(doc: Document) {
|
|
|
2277
1939
|
const controls = wrapper.querySelector<HTMLElement>('.table-inline-controls')
|
|
2278
1940
|
const addBtn = controls?.querySelector<HTMLElement>('.table-col-add-btn')
|
|
2279
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')
|
|
2280
1944
|
const thumbBtn = controls?.querySelector<HTMLElement>('.table-col-thumb')
|
|
2281
|
-
if (!controls || !addBtn || !addLine || !thumbBtn) { return }
|
|
1945
|
+
if (!controls || !addBtn || !addLine || !rowAddBtn || !rowAddLine || !thumbBtn) { return }
|
|
2282
1946
|
|
|
2283
1947
|
doc.querySelectorAll<HTMLElement>('.table-inline-controls').forEach((other) => {
|
|
2284
1948
|
if (other !== controls) {
|
|
2285
|
-
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')
|
|
2286
1950
|
}
|
|
2287
1951
|
})
|
|
2288
1952
|
|
|
@@ -2292,6 +1956,8 @@ function setupTableColumnResizing(doc: Document) {
|
|
|
2292
1956
|
|
|
2293
1957
|
const tableTop = tableRect.top - wrapperRect.top
|
|
2294
1958
|
const tableHeight = tableRect.height
|
|
1959
|
+
const tableLeft = tableRect.left - wrapperRect.left
|
|
1960
|
+
const tableWidth = tableRect.width
|
|
2295
1961
|
const cellLeft = cellRect.left - wrapperRect.left
|
|
2296
1962
|
const cellRight = cellRect.right - wrapperRect.left
|
|
2297
1963
|
const cellCenter = cellLeft + cellRect.width / 2
|
|
@@ -2302,18 +1968,29 @@ function setupTableColumnResizing(doc: Document) {
|
|
|
2302
1968
|
const distRight = Math.abs(e.clientX - cellRect.right)
|
|
2303
1969
|
const nearLeft = distLeft <= distRight
|
|
2304
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)
|
|
2305
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'
|
|
2306
1980
|
|
|
2307
1981
|
// In LTR: left edge inserts 'before', right edge inserts 'after'. In RTL: reversed.
|
|
2308
1982
|
const insertPosition: 'before' | 'after' = nearLeft
|
|
2309
1983
|
? (isRTL ? 'after' : 'before')
|
|
2310
1984
|
: (isRTL ? 'before' : 'after')
|
|
1985
|
+
const rowInsertPosition: 'before' | 'after' = isHeaderRow ? 'after' : (nearTop ? 'before' : 'after')
|
|
2311
1986
|
const boundaryX = nearLeft ? cellLeft : cellRight
|
|
1987
|
+
const boundaryY = (isHeaderRow ? row.getBoundingClientRect().bottom : (nearTop ? cellRect.top : cellRect.bottom)) - wrapperRect.top
|
|
2312
1988
|
|
|
2313
1989
|
activeHoverState = {
|
|
2314
1990
|
table,
|
|
2315
1991
|
cell,
|
|
2316
1992
|
insertPosition,
|
|
1993
|
+
rowInsertPosition,
|
|
2317
1994
|
}
|
|
2318
1995
|
|
|
2319
1996
|
controls.classList.add('is-visible', 'show-col-thumb')
|
|
@@ -2331,6 +2008,18 @@ function setupTableColumnResizing(doc: Document) {
|
|
|
2331
2008
|
} else {
|
|
2332
2009
|
controls.classList.remove('show-col-add')
|
|
2333
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
|
+
}
|
|
2334
2023
|
}
|
|
2335
2024
|
|
|
2336
2025
|
const onHoverPointerMove = (e: PointerEvent) => {
|
|
@@ -2348,7 +2037,32 @@ function setupTableColumnResizing(doc: Document) {
|
|
|
2348
2037
|
const onPointerDown = (e: PointerEvent) => {
|
|
2349
2038
|
const target = e.target as HTMLElement | null
|
|
2350
2039
|
|
|
2351
|
-
// 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
|
|
2352
2066
|
const addColBtn = target?.closest('.table-col-add-btn') as HTMLElement | null
|
|
2353
2067
|
if (addColBtn) {
|
|
2354
2068
|
e.preventDefault()
|
|
@@ -2371,7 +2085,7 @@ function setupTableColumnResizing(doc: Document) {
|
|
|
2371
2085
|
return
|
|
2372
2086
|
}
|
|
2373
2087
|
|
|
2374
|
-
//
|
|
2088
|
+
// 3. Handle native inline '⋯' (Column Thumb: drag via usePointerSession to reorder, or click to open ContextMenu)
|
|
2375
2089
|
const thumbColBtn = target?.closest('.table-col-thumb') as HTMLElement | null
|
|
2376
2090
|
if (thumbColBtn) {
|
|
2377
2091
|
e.stopPropagation()
|
|
@@ -2469,7 +2183,7 @@ function setupTableColumnResizing(doc: Document) {
|
|
|
2469
2183
|
return
|
|
2470
2184
|
}
|
|
2471
2185
|
|
|
2472
|
-
//
|
|
2186
|
+
// 4. Handle column border drag-resizing via usePointerSession (with Escape rollback)
|
|
2473
2187
|
const hovered = getHoveredResizeCell(e)
|
|
2474
2188
|
if (!hovered || !hovered.cell.nextElementSibling) { return }
|
|
2475
2189
|
|
|
@@ -2578,6 +2292,18 @@ function setupTableEditButtons(doc: Document) {
|
|
|
2578
2292
|
addBtn.setAttribute('contenteditable', 'false')
|
|
2579
2293
|
addBtn.setAttribute('tabindex', '-1')
|
|
2580
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
|
+
|
|
2581
2307
|
const thumbBtn = doc.createElement('span')
|
|
2582
2308
|
thumbBtn.className = 'table-col-thumb'
|
|
2583
2309
|
thumbBtn.textContent = '⋯'
|
|
@@ -2588,6 +2314,8 @@ function setupTableEditButtons(doc: Document) {
|
|
|
2588
2314
|
|
|
2589
2315
|
controls.appendChild(addLine)
|
|
2590
2316
|
controls.appendChild(addBtn)
|
|
2317
|
+
controls.appendChild(rowAddLine)
|
|
2318
|
+
controls.appendChild(rowAddBtn)
|
|
2591
2319
|
controls.appendChild(thumbBtn)
|
|
2592
2320
|
return controls
|
|
2593
2321
|
}
|
|
@@ -2799,6 +2527,24 @@ function setupAutoWrapping(doc: Document) {
|
|
|
2799
2527
|
}
|
|
2800
2528
|
}
|
|
2801
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
|
+
|
|
2802
2548
|
// Handle input events
|
|
2803
2549
|
doc.addEventListener('input', (e) => {
|
|
2804
2550
|
const inputEvent = e as InputEvent
|
|
@@ -2975,9 +2721,7 @@ function setupAutoWrapping(doc: Document) {
|
|
|
2975
2721
|
subtree: true
|
|
2976
2722
|
})
|
|
2977
2723
|
|
|
2978
|
-
|
|
2979
|
-
doc.addEventListener('click', (e) => {
|
|
2980
|
-
const target = e.target as HTMLElement
|
|
2724
|
+
function openMediaFromTarget(target: HTMLElement, event?: Event): boolean {
|
|
2981
2725
|
const link = target.closest('a')
|
|
2982
2726
|
const table = target.closest('table')
|
|
2983
2727
|
|
|
@@ -3015,27 +2759,30 @@ function setupAutoWrapping(doc: Document) {
|
|
|
3015
2759
|
if (table) {
|
|
3016
2760
|
// Table clicks handled only by edit button
|
|
3017
2761
|
} else if (videoFigure) {
|
|
3018
|
-
|
|
3019
|
-
|
|
2762
|
+
event?.preventDefault()
|
|
2763
|
+
event?.stopPropagation()
|
|
3020
2764
|
videoFigure.classList.add('media-selected')
|
|
3021
2765
|
openVideoModal(videoFigure)
|
|
2766
|
+
return true
|
|
3022
2767
|
} else if (imageFigure) {
|
|
3023
|
-
|
|
3024
|
-
|
|
2768
|
+
event?.preventDefault()
|
|
2769
|
+
event?.stopPropagation()
|
|
3025
2770
|
imageFigure.classList.add('media-selected')
|
|
3026
2771
|
openImageModal(imageFigure)
|
|
2772
|
+
return true
|
|
3027
2773
|
} else if (embedFigure) {
|
|
3028
|
-
|
|
3029
|
-
|
|
2774
|
+
event?.preventDefault()
|
|
2775
|
+
event?.stopPropagation()
|
|
3030
2776
|
embedFigure.classList.add('media-selected')
|
|
3031
2777
|
openEmbedModal(embedFigure)
|
|
2778
|
+
return true
|
|
3032
2779
|
} else if (link && link.href) {
|
|
3033
|
-
|
|
3034
|
-
|
|
2780
|
+
event?.preventDefault()
|
|
2781
|
+
event?.stopPropagation()
|
|
3035
2782
|
|
|
3036
2783
|
// Get current selection
|
|
3037
2784
|
const selection = doc.getSelection()
|
|
3038
|
-
if (!selection) { return }
|
|
2785
|
+
if (!selection) { return false }
|
|
3039
2786
|
|
|
3040
2787
|
// Create a range that selects the entire link
|
|
3041
2788
|
const range = doc.createRange()
|
|
@@ -3056,7 +2803,21 @@ function setupAutoWrapping(doc: Document) {
|
|
|
3056
2803
|
}
|
|
3057
2804
|
|
|
3058
2805
|
showLinkModal.value = true
|
|
2806
|
+
return true
|
|
3059
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)
|
|
3060
2821
|
})
|
|
3061
2822
|
|
|
3062
2823
|
// Handle focus to remove placeholder when user starts interacting
|
|
@@ -3166,7 +2927,7 @@ async function initEditor() {
|
|
|
3166
2927
|
|
|
3167
2928
|
/* Ensure all text elements inherit the theme color */
|
|
3168
2929
|
p, h1, h2, h3, h4, h5, h6, div, span, li {
|
|
3169
|
-
color: inherit
|
|
2930
|
+
color: inherit;
|
|
3170
2931
|
}
|
|
3171
2932
|
table {
|
|
3172
2933
|
border-collapse: collapse;
|
|
@@ -3188,12 +2949,12 @@ async function initEditor() {
|
|
|
3188
2949
|
/* Table edit button styles */
|
|
3189
2950
|
.table-edit-btn {
|
|
3190
2951
|
position: absolute;
|
|
3191
|
-
|
|
2952
|
+
bottom: -8px;
|
|
3192
2953
|
right: -8px;
|
|
3193
|
-
background:
|
|
2954
|
+
background: rgba(0, 0, 0, 0.72);
|
|
3194
2955
|
color: #fff !important;
|
|
3195
2956
|
border: 1px solid #fff;
|
|
3196
|
-
border-radius:
|
|
2957
|
+
border-radius: 6px;
|
|
3197
2958
|
padding: 4px 8px;
|
|
3198
2959
|
font-size: 12px;
|
|
3199
2960
|
cursor: pointer;
|
|
@@ -3248,6 +3009,7 @@ async function initEditor() {
|
|
|
3248
3009
|
}
|
|
3249
3010
|
|
|
3250
3011
|
.table-col-add-btn,
|
|
3012
|
+
.table-row-add-btn,
|
|
3251
3013
|
.table-col-thumb {
|
|
3252
3014
|
position: absolute;
|
|
3253
3015
|
display: flex;
|
|
@@ -3264,7 +3026,8 @@ async function initEditor() {
|
|
|
3264
3026
|
transition: opacity 0.14s ease, transform 0.14s ease, background-color 0.14s ease, border-color 0.14s ease, visibility 0.14s ease;
|
|
3265
3027
|
}
|
|
3266
3028
|
|
|
3267
|
-
.table-col-add-btn
|
|
3029
|
+
.table-col-add-btn,
|
|
3030
|
+
.table-row-add-btn {
|
|
3268
3031
|
width: 20px;
|
|
3269
3032
|
height: 20px;
|
|
3270
3033
|
border-radius: 999px;
|
|
@@ -3275,16 +3038,18 @@ async function initEditor() {
|
|
|
3275
3038
|
cursor: pointer;
|
|
3276
3039
|
box-shadow: 0 2px 6px rgba(0, 123, 255, 0.35);
|
|
3277
3040
|
z-index: 98;
|
|
3041
|
+
padding-bottom: 2px;
|
|
3278
3042
|
}
|
|
3279
3043
|
|
|
3280
|
-
.table-col-add-btn:hover
|
|
3044
|
+
.table-col-add-btn:hover,
|
|
3045
|
+
.table-row-add-btn:hover {
|
|
3281
3046
|
background: #0056b3;
|
|
3282
3047
|
transform: translate(-50%, -50%) scale(1.08) !important;
|
|
3283
3048
|
}
|
|
3284
3049
|
|
|
3285
|
-
.table-col-add-line
|
|
3050
|
+
.table-col-add-line,
|
|
3051
|
+
.table-row-add-line {
|
|
3286
3052
|
position: absolute;
|
|
3287
|
-
width: 2px;
|
|
3288
3053
|
background: #007bff;
|
|
3289
3054
|
pointer-events: none;
|
|
3290
3055
|
opacity: 0;
|
|
@@ -3294,6 +3059,15 @@ async function initEditor() {
|
|
|
3294
3059
|
z-index: 94;
|
|
3295
3060
|
}
|
|
3296
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
|
+
|
|
3297
3071
|
.table-col-thumb {
|
|
3298
3072
|
width: 28px;
|
|
3299
3073
|
height: 14px;
|
|
@@ -3323,19 +3097,22 @@ async function initEditor() {
|
|
|
3323
3097
|
border-color: #0056b3;
|
|
3324
3098
|
}
|
|
3325
3099
|
|
|
3326
|
-
.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 {
|
|
3327
3102
|
opacity: 1;
|
|
3328
3103
|
visibility: visible;
|
|
3329
3104
|
transform: translate(-50%, -50%) scale(1);
|
|
3330
3105
|
}
|
|
3331
3106
|
|
|
3332
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,
|
|
3333
3109
|
.table-inline-controls.is-dragging-col .table-col-add-line {
|
|
3334
3110
|
opacity: 0.45;
|
|
3335
3111
|
visibility: visible;
|
|
3336
3112
|
}
|
|
3337
3113
|
|
|
3338
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,
|
|
3339
3116
|
.table-inline-controls.is-dragging-col .table-col-add-line {
|
|
3340
3117
|
opacity: 0.9;
|
|
3341
3118
|
visibility: visible;
|
|
@@ -3354,6 +3131,55 @@ async function initEditor() {
|
|
|
3354
3131
|
}
|
|
3355
3132
|
|
|
3356
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
|
+
|
|
3357
3183
|
/* Figures wrapping media: ::after badge + hover ring.
|
|
3358
3184
|
No display override — <figure> is already block, <img>/<video> keep their natural display. */
|
|
3359
3185
|
figure:has(img),
|
|
@@ -3447,6 +3273,25 @@ async function initEditor() {
|
|
|
3447
3273
|
width: 100%;
|
|
3448
3274
|
}
|
|
3449
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
|
+
|
|
3450
3295
|
/* Linked images: the wrapping <a> should behave like the image block.
|
|
3451
3296
|
In the editor, disable navigation so clicks open the edit modal. */
|
|
3452
3297
|
figure.image-figure > a {
|
|
@@ -3503,6 +3348,8 @@ async function initEditor() {
|
|
|
3503
3348
|
|
|
3504
3349
|
if (normalizedModelValue.value) {
|
|
3505
3350
|
doc.body.innerHTML = normalizedModelValue.value
|
|
3351
|
+
hydrateVideoFigures(doc.body)
|
|
3352
|
+
hydrateImageFigures(doc.body)
|
|
3506
3353
|
} else if (props.placeholder) {
|
|
3507
3354
|
const placeholderEl = doc.createElement('p')
|
|
3508
3355
|
placeholderEl.className = 'placeholder'
|
|
@@ -3764,10 +3611,12 @@ function updateIframeColors() {
|
|
|
3764
3611
|
font-family: ${currentFontFamily.value} !important;
|
|
3765
3612
|
}
|
|
3766
3613
|
|
|
3767
|
-
/*
|
|
3614
|
+
/* Body copy inherits the configured base size; headings keep semantic sizes. */
|
|
3615
|
+
p, div, li {
|
|
3616
|
+
font-size: inherit;
|
|
3617
|
+
}
|
|
3768
3618
|
p, h1, h2, h3, h4, h5, h6, div, span, li {
|
|
3769
|
-
color: inherit
|
|
3770
|
-
font-size: inherit !important;
|
|
3619
|
+
color: inherit;
|
|
3771
3620
|
}
|
|
3772
3621
|
${effectiveContentStyles.value}
|
|
3773
3622
|
`
|
|
@@ -3808,6 +3657,8 @@ watch(normalizedModelValue, (newValue) => {
|
|
|
3808
3657
|
}
|
|
3809
3658
|
} else {
|
|
3810
3659
|
doc.body.innerHTML = newValue
|
|
3660
|
+
hydrateVideoFigures(doc.body)
|
|
3661
|
+
hydrateImageFigures(doc.body)
|
|
3811
3662
|
}
|
|
3812
3663
|
|
|
3813
3664
|
detectAndSetDirection()
|
|
@@ -3839,6 +3690,16 @@ watch(() => editor.state.content, () => {
|
|
|
3839
3690
|
emit('update:modelValue', getCleanHTML(editor.state.doc ?? null))
|
|
3840
3691
|
})
|
|
3841
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
|
+
|
|
3842
3703
|
// Watch table alignment changes and update the table immediately
|
|
3843
3704
|
watch(() => tableForm.value.alignment, (newAlignment) => {
|
|
3844
3705
|
if (pendingTableData?.existingTable) {
|
|
@@ -3904,6 +3765,7 @@ const imgTransform = computed({
|
|
|
3904
3765
|
:selectedStyles="editor.state.selectedStyles" :hide-images="hideImages" :hide-videos="hideVideos"
|
|
3905
3766
|
:hide-embeds="hideEmbed" :hide-tables="hideTables" :hide-alignment="hideAlignment"
|
|
3906
3767
|
:hide-directions="hideDirections" :hide-h5-h6="hideH5H6" :hide="effectiveHideArray"
|
|
3768
|
+
:document-colors="documentColors"
|
|
3907
3769
|
@action="commands.execute"
|
|
3908
3770
|
>
|
|
3909
3771
|
<template #end>
|
|
@@ -3926,6 +3788,17 @@ const imgTransform = computed({
|
|
|
3926
3788
|
</slot>
|
|
3927
3789
|
</template>
|
|
3928
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>
|
|
3929
3802
|
<div class="editor-container" :class="{ 'split-view': editor.state.isSplitView }">
|
|
3930
3803
|
<div
|
|
3931
3804
|
class="content-area radius-05"
|
|
@@ -4037,7 +3910,15 @@ const imgTransform = computed({
|
|
|
4037
3910
|
width="l" @update:open="showImageModal = $event"
|
|
4038
3911
|
>
|
|
4039
3912
|
<div class="image-dialog-layout">
|
|
4040
|
-
<
|
|
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>
|
|
4041
3922
|
<div class="flex flex-column gap-05 min-w-0">
|
|
4042
3923
|
<TextInput
|
|
4043
3924
|
v-model="imageForm.src" label="Image URL" type="url" placeholder="https://example.com/image.jpg"
|
|
@@ -4143,6 +4024,16 @@ const imgTransform = computed({
|
|
|
4143
4024
|
help="Examples: 100%, 500px, 50vw, 300"
|
|
4144
4025
|
/>
|
|
4145
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
|
+
|
|
4146
4037
|
<SelectInput
|
|
4147
4038
|
v-model="videoForm.aspectRatio" label="Aspect Ratio" :options="[
|
|
4148
4039
|
{ value: '16:9', label: '16:9 (Standard)' },
|
|
@@ -4154,12 +4045,12 @@ const imgTransform = computed({
|
|
|
4154
4045
|
]"
|
|
4155
4046
|
/>
|
|
4156
4047
|
|
|
4157
|
-
<div v-if="videoForm.aspectRatio === 'custom'" class="grid grid-wrap-2 gap-05">
|
|
4158
|
-
<TextInput v-model="videoForm.customWidth" label="Width ratio" placeholder="16" type="number" />
|
|
4159
|
-
<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" />
|
|
4160
4051
|
</div>
|
|
4161
4052
|
|
|
4162
|
-
<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">
|
|
4163
4054
|
<CheckInput v-model="videoForm.controls" label="Show controls" />
|
|
4164
4055
|
<CheckInput v-model="videoForm.autoplay" label="Autoplay" />
|
|
4165
4056
|
<CheckInput v-model="videoForm.mute" label="Mute" />
|
|
@@ -4187,14 +4078,14 @@ const imgTransform = computed({
|
|
|
4187
4078
|
</Card>
|
|
4188
4079
|
</div>
|
|
4189
4080
|
<template #footer>
|
|
4190
|
-
<div class="flex gap-05 w-100 ">
|
|
4191
|
-
<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" />
|
|
4192
4083
|
<Btn
|
|
4193
4084
|
v-if="pendingVideoData?.existingVideo" value="Delete Video" color="red" flat thin icon="delete"
|
|
4194
|
-
@click="deleteVideo"
|
|
4085
|
+
full-width-mobile @click="deleteVideo"
|
|
4195
4086
|
/>
|
|
4196
4087
|
<Btn
|
|
4197
|
-
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)"
|
|
4198
4089
|
@click="submitVideo"
|
|
4199
4090
|
/>
|
|
4200
4091
|
</div>
|