@bagelink/vue 1.15.271 → 1.15.273
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/Image.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/CodeEditor/Index.vue.d.ts +11 -0
- package/dist/components/form/inputs/CodeEditor/Index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/index.cjs +40 -33
- package/dist/index.mjs +4252 -4205
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/Image.vue +35 -25
- package/src/components/form/inputs/CodeEditor/Index.vue +21 -1
- package/src/components/form/inputs/RichText/editor.css +5 -0
- package/src/components/form/inputs/RichText/index.vue +60 -8
package/package.json
CHANGED
package/src/components/Image.vue
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
defineOptions({ name: 'BglImage', inheritAttrs: false })
|
|
3
3
|
import { Skeleton, normalizeDimension, Icon, useImageSrc } from '@bagelink/vue'
|
|
4
|
-
import { Comment, computed, useSlots, type StyleValue, type VNode } from 'vue'
|
|
4
|
+
import { Comment, computed, ref, useSlots, watch, type StyleValue, type VNode } from 'vue'
|
|
5
5
|
|
|
6
6
|
interface ImageProps {
|
|
7
7
|
src?: string
|
|
@@ -26,6 +26,15 @@ interface ImageProps {
|
|
|
26
26
|
const props = defineProps<ImageProps>()
|
|
27
27
|
|
|
28
28
|
const { imageSrc, loadingError } = useImageSrc(() => props.src ?? props.pathKey ?? props.modelValue)
|
|
29
|
+
const imageLoaded = ref(false)
|
|
30
|
+
|
|
31
|
+
watch(imageSrc, () => {
|
|
32
|
+
imageLoaded.value = false
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
function onImageLoad() {
|
|
36
|
+
imageLoaded.value = true
|
|
37
|
+
}
|
|
29
38
|
|
|
30
39
|
// ── Framed mode (ratio / gradient / blend / rounded / overlay slot) ──────────
|
|
31
40
|
const slots = useSlots()
|
|
@@ -89,10 +98,11 @@ const imgFit = computed(() => props.fit ?? 'cover')
|
|
|
89
98
|
:src="imageSrc"
|
|
90
99
|
:alt="alt"
|
|
91
100
|
class="bgl-image-frame-img h-100p"
|
|
92
|
-
:class="frameHasHeight ? 'absolute-fill' : 'bgl-image-frame-img--flow'"
|
|
101
|
+
:class="[frameHasHeight ? 'absolute-fill' : 'bgl-image-frame-img--flow', { 'opacity-0': !imageLoaded }]"
|
|
93
102
|
:style="{ objectFit: imgFit, mixBlendMode: blend } as StyleValue"
|
|
103
|
+
@load="onImageLoad"
|
|
94
104
|
>
|
|
95
|
-
<Skeleton v-
|
|
105
|
+
<Skeleton v-if="!imageLoaded && !loadingError" class="absolute-fill" />
|
|
96
106
|
<div v-else-if="loadingError && !gradientCss" class="flex-center absolute-fill error-image">
|
|
97
107
|
<Icon name="broken_image" />
|
|
98
108
|
</div>
|
|
@@ -100,6 +110,7 @@ const imgFit = computed(() => props.fit ?? 'cover')
|
|
|
100
110
|
</div>
|
|
101
111
|
|
|
102
112
|
<figcaption v-else-if="caption">
|
|
113
|
+
<div class="relative">
|
|
103
114
|
<img
|
|
104
115
|
v-if="imageSrc"
|
|
105
116
|
:src="imageSrc"
|
|
@@ -107,39 +118,38 @@ const imgFit = computed(() => props.fit ?? 'cover')
|
|
|
107
118
|
:alt="alt"
|
|
108
119
|
:width="normalizeDimension(width)"
|
|
109
120
|
:height="normalizeDimension(height)"
|
|
121
|
+
:class="{ 'opacity-0': !imageLoaded }"
|
|
122
|
+
@load="onImageLoad"
|
|
110
123
|
>
|
|
111
124
|
<Skeleton
|
|
112
|
-
|
|
125
|
+
v-if="!imageLoaded && !loadingError"
|
|
113
126
|
class="img-web-kit"
|
|
114
127
|
:width="normalizeDimension(width)"
|
|
115
128
|
:height="normalizeDimension(height)"
|
|
116
129
|
/>
|
|
130
|
+
</div>
|
|
117
131
|
</figcaption>
|
|
118
132
|
|
|
119
|
-
<img
|
|
120
|
-
v-else-if="imageSrc"
|
|
121
|
-
:src="imageSrc"
|
|
122
|
-
v-bind="$attrs"
|
|
123
|
-
:alt="alt"
|
|
124
|
-
:width="normalizeDimension(width)"
|
|
125
|
-
:height="normalizeDimension(height)"
|
|
126
|
-
>
|
|
127
133
|
<div
|
|
128
|
-
v-else
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
height: normalizeDimension(height),
|
|
133
|
-
}"
|
|
134
|
+
v-else
|
|
135
|
+
v-bind="$attrs"
|
|
136
|
+
class="relative"
|
|
137
|
+
:style="{ width: normalizeDimension(width), height: normalizeDimension(height) }"
|
|
134
138
|
>
|
|
135
|
-
<
|
|
139
|
+
<img
|
|
140
|
+
v-if="imageSrc"
|
|
141
|
+
:src="imageSrc"
|
|
142
|
+
:alt="alt"
|
|
143
|
+
class="w-100p h-100p"
|
|
144
|
+
:class="{ 'opacity-0': !imageLoaded }"
|
|
145
|
+
:style="{ objectFit: imgFit }"
|
|
146
|
+
@load="onImageLoad"
|
|
147
|
+
>
|
|
148
|
+
<div v-if="loadingError" class="flex-center absolute-fill error-image">
|
|
149
|
+
<Icon name="broken_image" />
|
|
150
|
+
</div>
|
|
151
|
+
<Skeleton v-else-if="!imageLoaded" class="absolute-fill" />
|
|
136
152
|
</div>
|
|
137
|
-
<Skeleton
|
|
138
|
-
v-else
|
|
139
|
-
class="img-web-kit"
|
|
140
|
-
:width="normalizeDimension(width)"
|
|
141
|
-
:height="normalizeDimension(height)"
|
|
142
|
-
/>
|
|
143
153
|
</template>
|
|
144
154
|
|
|
145
155
|
<style scoped>
|
|
@@ -16,6 +16,8 @@ const props = defineProps({
|
|
|
16
16
|
placeholder: { type: String, default: '' },
|
|
17
17
|
height: { type: String, default: '240px' },
|
|
18
18
|
disabled: { type: Boolean, default: false },
|
|
19
|
+
/** Wrap long lines instead of scrolling horizontally */
|
|
20
|
+
wrap: { type: Boolean, default: false },
|
|
19
21
|
theme: { type: String as PropType<HighlightTheme>, default: 'dark' },
|
|
20
22
|
frame: { type: Boolean, default: undefined },
|
|
21
23
|
outline: { type: Boolean, default: undefined },
|
|
@@ -162,7 +164,7 @@ watch(() => props.modelValue, () => {
|
|
|
162
164
|
v-if="loaded"
|
|
163
165
|
ref="editorRef"
|
|
164
166
|
class="code-editor-grandpa hljs"
|
|
165
|
-
:class="isLightTheme ? 'theme-light' : 'theme-dark'"
|
|
167
|
+
:class="[isLightTheme ? 'theme-light' : 'theme-dark', { 'code-wrap': wrap }]"
|
|
166
168
|
>
|
|
167
169
|
<div class="editor-content-papa">
|
|
168
170
|
<pre class="code-display"><code v-html="formattedCode" /></pre>
|
|
@@ -284,6 +286,24 @@ watch(() => props.modelValue, () => {
|
|
|
284
286
|
color: transparent;
|
|
285
287
|
-webkit-text-fill-color: transparent;
|
|
286
288
|
}
|
|
289
|
+
|
|
290
|
+
/* Wrap mode: long lines break onto the next line instead of scrolling.
|
|
291
|
+
The content box must fill the width so wrapping has a boundary to break at. */
|
|
292
|
+
.code-editor-grandpa.code-wrap {
|
|
293
|
+
overflow-x: hidden;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.code-editor-grandpa.code-wrap .editor-content-papa {
|
|
297
|
+
width: 100%;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.code-editor-grandpa.code-wrap .code-display,
|
|
301
|
+
.code-editor-grandpa.code-wrap .code-input,
|
|
302
|
+
.code-editor-grandpa.code-wrap .code-placeholder {
|
|
303
|
+
white-space: pre-wrap;
|
|
304
|
+
overflow-wrap: anywhere;
|
|
305
|
+
word-break: break-word;
|
|
306
|
+
}
|
|
287
307
|
</style>
|
|
288
308
|
|
|
289
309
|
<style>
|
|
@@ -181,6 +181,11 @@ th {
|
|
|
181
181
|
pointer-events: none;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
/* Linked image: the wrapping <a> behaves like the image block so layout is unchanged */
|
|
185
|
+
.richtext-editor-content .image-figure > a {
|
|
186
|
+
display: block;
|
|
187
|
+
}
|
|
188
|
+
|
|
184
189
|
.richtext-editor-content .image-figure figcaption {
|
|
185
190
|
margin-top: 0.5rem;
|
|
186
191
|
font-size: 0.9em;
|
|
@@ -230,7 +230,9 @@ const imageForm = ref({
|
|
|
230
230
|
height: '',
|
|
231
231
|
credit: '',
|
|
232
232
|
figcaption: false,
|
|
233
|
-
align: 'full' // full, start, end
|
|
233
|
+
align: 'full', // full, start, end
|
|
234
|
+
link: '',
|
|
235
|
+
linkNewTab: false
|
|
234
236
|
})
|
|
235
237
|
let pendingImageData: {
|
|
236
238
|
selection: Selection
|
|
@@ -708,6 +710,11 @@ function openImageModal(existingImage: HTMLElement | null = null) {
|
|
|
708
710
|
const alignSource = existingImage.tagName === 'IMG' ? img : existingImage
|
|
709
711
|
const align = alignSource?.getAttribute('data-align') || 'full'
|
|
710
712
|
|
|
713
|
+
// Read link from the anchor wrapping the image (if any)
|
|
714
|
+
const anchor = img?.closest('a')
|
|
715
|
+
const link = anchor?.getAttribute('href') || ''
|
|
716
|
+
const linkNewTab = anchor?.getAttribute('target') === '_blank'
|
|
717
|
+
|
|
711
718
|
imageForm.value = {
|
|
712
719
|
src: img?.src || '',
|
|
713
720
|
alt: img?.alt || '',
|
|
@@ -715,7 +722,9 @@ function openImageModal(existingImage: HTMLElement | null = null) {
|
|
|
715
722
|
height: img?.getAttribute('data-height') || img?.style.height || '',
|
|
716
723
|
credit,
|
|
717
724
|
figcaption: !!figcaption,
|
|
718
|
-
align
|
|
725
|
+
align,
|
|
726
|
+
link,
|
|
727
|
+
linkNewTab
|
|
719
728
|
}
|
|
720
729
|
} else {
|
|
721
730
|
// Reset form for new image
|
|
@@ -726,7 +735,9 @@ function openImageModal(existingImage: HTMLElement | null = null) {
|
|
|
726
735
|
height: '',
|
|
727
736
|
credit: '',
|
|
728
737
|
figcaption: false,
|
|
729
|
-
align: 'full'
|
|
738
|
+
align: 'full',
|
|
739
|
+
link: '',
|
|
740
|
+
linkNewTab: false
|
|
730
741
|
}
|
|
731
742
|
}
|
|
732
743
|
|
|
@@ -791,6 +802,21 @@ function submitImage() {
|
|
|
791
802
|
return figcaption
|
|
792
803
|
}
|
|
793
804
|
|
|
805
|
+
// Wrap the image in an anchor when a link is provided, otherwise return the bare image
|
|
806
|
+
function createImageContent(): HTMLElement {
|
|
807
|
+
const link = imageForm.value.link?.trim()
|
|
808
|
+
if (!link) { return img }
|
|
809
|
+
|
|
810
|
+
const anchor = doc!.createElement('a')
|
|
811
|
+
anchor.setAttribute('href', link)
|
|
812
|
+
if (imageForm.value.linkNewTab) {
|
|
813
|
+
anchor.setAttribute('target', '_blank')
|
|
814
|
+
anchor.setAttribute('rel', 'noopener noreferrer')
|
|
815
|
+
}
|
|
816
|
+
anchor.appendChild(img)
|
|
817
|
+
return anchor
|
|
818
|
+
}
|
|
819
|
+
|
|
794
820
|
// Create or update figure/image - always wrap in figure
|
|
795
821
|
let elementToUpdate: HTMLElement
|
|
796
822
|
if (existingImage) {
|
|
@@ -801,7 +827,7 @@ function submitImage() {
|
|
|
801
827
|
if (imageForm.value.credit) {
|
|
802
828
|
figure.setAttribute('data-credit', imageForm.value.credit)
|
|
803
829
|
}
|
|
804
|
-
figure.appendChild(
|
|
830
|
+
figure.appendChild(createImageContent())
|
|
805
831
|
|
|
806
832
|
// Add caption if needed (alt with checkbox checked OR credit exists)
|
|
807
833
|
if ((imageForm.value.figcaption && imageForm.value.alt) || imageForm.value.credit) {
|
|
@@ -816,7 +842,7 @@ function submitImage() {
|
|
|
816
842
|
if (imageForm.value.credit) {
|
|
817
843
|
existingImage.setAttribute('data-credit', imageForm.value.credit)
|
|
818
844
|
}
|
|
819
|
-
existingImage.appendChild(
|
|
845
|
+
existingImage.appendChild(createImageContent())
|
|
820
846
|
|
|
821
847
|
// Add caption if needed (alt with checkbox checked OR credit exists)
|
|
822
848
|
if ((imageForm.value.figcaption && imageForm.value.alt) || imageForm.value.credit) {
|
|
@@ -831,7 +857,7 @@ function submitImage() {
|
|
|
831
857
|
if (imageForm.value.credit) {
|
|
832
858
|
figure.setAttribute('data-credit', imageForm.value.credit)
|
|
833
859
|
}
|
|
834
|
-
figure.appendChild(
|
|
860
|
+
figure.appendChild(createImageContent())
|
|
835
861
|
|
|
836
862
|
// Add caption if needed (alt with checkbox checked OR credit exists)
|
|
837
863
|
if ((imageForm.value.figcaption && imageForm.value.alt) || imageForm.value.credit) {
|
|
@@ -2931,6 +2957,13 @@ async function initEditor() {
|
|
|
2931
2957
|
width: 100%;
|
|
2932
2958
|
}
|
|
2933
2959
|
|
|
2960
|
+
/* Linked images: the wrapping <a> should behave like the image block.
|
|
2961
|
+
In the editor, disable navigation so clicks open the edit modal. */
|
|
2962
|
+
figure.image-figure > a {
|
|
2963
|
+
display: block;
|
|
2964
|
+
pointer-events: none;
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2934
2967
|
/* Selected state — applied via JS on click, stripped by getCleanHTML() */
|
|
2935
2968
|
.media-selected {
|
|
2936
2969
|
outline: 2px solid #007bff !important;
|
|
@@ -3352,7 +3385,7 @@ const imgTransform = computed({
|
|
|
3352
3385
|
/>
|
|
3353
3386
|
</div>
|
|
3354
3387
|
<CodeEditor
|
|
3355
|
-
v-if="editor.state.isSplitView" v-model="editor.state.content" language="html"
|
|
3388
|
+
v-if="editor.state.isSplitView" v-model="editor.state.content" language="html" wrap
|
|
3356
3389
|
:height="editor.state.isFullscreen ? 'calc(100vh - 4rem)' : editorHeight"
|
|
3357
3390
|
@update:modelValue="editor.updateState.content('html')"
|
|
3358
3391
|
/>
|
|
@@ -3537,6 +3570,13 @@ const imgTransform = computed({
|
|
|
3537
3570
|
help="Full width fills the container. Start/End float the image so text flows alongside."
|
|
3538
3571
|
/>
|
|
3539
3572
|
<TextInput v-model="imageForm.credit" label="Photo Credit" placeholder="Photographer name (optional)" />
|
|
3573
|
+
<TextInput
|
|
3574
|
+
v-model="imageForm.link" label="Link URL" type="url" placeholder="https://example.com (optional)"
|
|
3575
|
+
help="Make the image clickable by linking it to a URL"
|
|
3576
|
+
/>
|
|
3577
|
+
<CheckInput
|
|
3578
|
+
v-if="imageForm.link" v-model="imageForm.linkNewTab" label="Open link in new tab"
|
|
3579
|
+
/>
|
|
3540
3580
|
<CheckInput
|
|
3541
3581
|
v-model="imageForm.figcaption" label="Show alt text as caption"
|
|
3542
3582
|
help="Photo credit will always be shown if provided"
|
|
@@ -3889,9 +3929,21 @@ background: var(--bgl-richtext-color);
|
|
|
3889
3929
|
font-size: var(--richtext-font-size, 16px);
|
|
3890
3930
|
}
|
|
3891
3931
|
|
|
3892
|
-
|
|
3932
|
+
/* Split view: editor + code side by side, each taking half the width.
|
|
3933
|
+
Use a compound selector so it overrides the base `.editor-container`
|
|
3934
|
+
`display: flex` regardless of source order or other modifiers. */
|
|
3935
|
+
.editor-container.split-view {
|
|
3893
3936
|
display: grid;
|
|
3894
3937
|
grid-template-columns: 1fr 1fr;
|
|
3938
|
+
align-items: stretch;
|
|
3939
|
+
}
|
|
3940
|
+
|
|
3941
|
+
/* Prevent either pane from overflowing its track (the code editor has
|
|
3942
|
+
min-width: 100% internally, which would otherwise blow out the grid). */
|
|
3943
|
+
.editor-container.split-view > .content-area,
|
|
3944
|
+
.editor-container.split-view > :deep(.code-editor-container) {
|
|
3945
|
+
min-width: 0;
|
|
3946
|
+
overflow: hidden;
|
|
3895
3947
|
}
|
|
3896
3948
|
|
|
3897
3949
|
.editableContent {
|